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, 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::region::options::IndexOptions;
41use crate::sst::file_purger::FilePurgerRef;
42use crate::sst::file_ref::FileReferenceManagerRef;
43use crate::sst::index::fulltext_index::FULLTEXT_BLOOM_VERSION;
44use crate::sst::location;
45use crate::sst::parquet::SstInfo;
46use crate::sst::primary_key::PrimaryKeyRangeMapper;
47
48/// Custom serde functions for Bytes fields serialized as base64 strings.
49fn serialize_bytes_option<S>(bytes: &Option<Bytes>, serializer: S) -> Result<S::Ok, S::Error>
50where
51    S: serde::Serializer,
52{
53    match bytes {
54        None => serializer.serialize_none(),
55        Some(b) => serializer.serialize_some(&BASE64_STANDARD.encode(b)),
56    }
57}
58
59fn deserialize_bytes_option<'de, D>(deserializer: D) -> Result<Option<Bytes>, D::Error>
60where
61    D: serde::Deserializer<'de>,
62{
63    let opt: Option<String> = Option::deserialize(deserializer)?;
64    match opt {
65        None => Ok(None),
66        Some(s) => {
67            let decoded = BASE64_STANDARD
68                .decode(&s)
69                .map_err(serde::de::Error::custom)?;
70            Ok(Some(Bytes::from(decoded)))
71        }
72    }
73}
74
75/// Custom serde functions for partition_expr field in FileMeta
76fn serialize_partition_expr<S>(
77    partition_expr: &Option<PartitionExpr>,
78    serializer: S,
79) -> Result<S::Ok, S::Error>
80where
81    S: serde::Serializer,
82{
83    use serde::ser::Error;
84
85    match partition_expr {
86        None => serializer.serialize_none(),
87        Some(expr) => {
88            let json_str = expr.as_json_str().map_err(S::Error::custom)?;
89            serializer.serialize_some(&json_str)
90        }
91    }
92}
93
94fn deserialize_partition_expr<'de, D>(deserializer: D) -> Result<Option<PartitionExpr>, D::Error>
95where
96    D: serde::Deserializer<'de>,
97{
98    use serde::de::Error;
99
100    let opt_json_str: Option<String> = Option::deserialize(deserializer)?;
101    match opt_json_str {
102        None => Ok(None),
103        Some(json_str) => {
104            if json_str.is_empty() {
105                // Empty string represents explicit "single-region/no-partition" designation
106                Ok(None)
107            } else {
108                // Parse the JSON string to PartitionExpr
109                PartitionExpr::from_json_str(&json_str).map_err(D::Error::custom)
110            }
111        }
112    }
113}
114
115/// Type to store SST level.
116pub type Level = u8;
117/// Maximum level of SSTs.
118pub const MAX_LEVEL: Level = 2;
119/// Type to store index types for a column.
120pub type IndexTypes = SmallVec<[IndexType; 4]>;
121
122/// Cross-region file id.
123///
124/// It contains a region id and a file id. The string representation is `{region_id}/{file_id}`.
125#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
126pub struct RegionFileId {
127    /// The region that creates the file.
128    region_id: RegionId,
129    /// The id of the file.
130    file_id: FileId,
131}
132
133impl RegionFileId {
134    /// Creates a new [RegionFileId] from `region_id` and `file_id`.
135    pub fn new(region_id: RegionId, file_id: FileId) -> Self {
136        Self { region_id, file_id }
137    }
138
139    /// Gets the region id.
140    pub fn region_id(&self) -> RegionId {
141        self.region_id
142    }
143
144    /// Gets the file id.
145    pub fn file_id(&self) -> FileId {
146        self.file_id
147    }
148}
149
150impl fmt::Display for RegionFileId {
151    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
152        write!(f, "{}/{}", self.region_id, self.file_id)
153    }
154}
155
156/// Unique identifier for an index file, combining the SST file ID and the index version.
157#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
158pub struct RegionIndexId {
159    pub file_id: RegionFileId,
160    pub version: IndexVersion,
161}
162
163impl RegionIndexId {
164    pub fn new(file_id: RegionFileId, version: IndexVersion) -> Self {
165        Self { file_id, version }
166    }
167
168    pub fn region_id(&self) -> RegionId {
169        self.file_id.region_id
170    }
171
172    pub fn file_id(&self) -> FileId {
173        self.file_id.file_id
174    }
175}
176
177impl fmt::Display for RegionIndexId {
178    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
179        if self.version == 0 {
180            write!(f, "{}/{}", self.file_id.region_id, self.file_id.file_id)
181        } else {
182            write!(
183                f,
184                "{}/{}.{}",
185                self.file_id.region_id, self.file_id.file_id, self.version
186            )
187        }
188    }
189}
190
191/// Time range (min and max timestamps) of a SST file.
192/// Both min and max are inclusive.
193pub type FileTimeRange = (Timestamp, Timestamp);
194
195/// Checks if two inclusive timestamp ranges overlap with each other.
196pub(crate) fn overlaps(l: &FileTimeRange, r: &FileTimeRange) -> bool {
197    let (l, r) = if l.0 <= r.0 { (l, r) } else { (r, l) };
198    let (_, l_end) = l;
199    let (r_start, _) = r;
200
201    r_start <= l_end
202}
203
204/// Metadata of a SST file.
205#[derive(Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
206#[serde(default)]
207pub struct FileMeta {
208    /// Region that created the file. The region id may not be the id of the current region.
209    pub region_id: RegionId,
210    /// Compared to normal file names, FileId ignore the extension
211    pub file_id: FileId,
212    /// Timestamp range of file. The timestamps have the same time unit as the
213    /// data in the SST.
214    pub time_range: FileTimeRange,
215    /// SST level of the file.
216    pub level: Level,
217    /// Size of the file.
218    pub file_size: u64,
219    /// Maximum uncompressed row group size of the file. 0 means unknown.
220    pub max_row_group_uncompressed_size: u64,
221    /// Available indexes of the file.
222    pub available_indexes: IndexTypes,
223    /// Created indexes of the file for each column.
224    ///
225    /// This is essentially a more granular, column-level version of `available_indexes`,
226    /// primarily used for manual index building in the asynchronous index construction mode.
227    ///
228    /// For backward compatibility, older `FileMeta` versions might only contain `available_indexes`.
229    /// In such cases, we cannot deduce specific column index information from `available_indexes` alone.
230    /// Therefore, defaulting this `indexes` field to an empty list during deserialization is a
231    /// reasonable and necessary step to ensure column information consistency.
232    pub indexes: Vec<ColumnIndexMetadata>,
233    /// Size of the index file.
234    pub index_file_size: u64,
235    /// Version of the index file.
236    /// Used to generate the index file name: "{file_id}.{index_version}.puffin".
237    /// Default is 0 (which maps to "{file_id}.puffin" for compatibility).
238    pub index_version: u64,
239    /// Number of rows in the file.
240    ///
241    /// For historical reasons, this field might be missing in old files. Thus
242    /// the default value `0` doesn't means the file doesn't contains any rows,
243    /// but instead means the number of rows is unknown.
244    pub num_rows: u64,
245    /// Number of row groups in the file.
246    ///
247    /// For historical reasons, this field might be missing in old files. Thus
248    /// the default value `0` doesn't means the file doesn't contains any rows,
249    /// but instead means the number of rows is unknown.
250    pub num_row_groups: u64,
251    /// File-level sequence bound or admission marker in the target region.
252    ///
253    /// Flush records the maximum input row sequence. Compaction outputs inherit
254    /// the maximum input bound, or remain unknown if any input bound is unknown,
255    /// independently of per-row sequence trust. This does not imply that every
256    /// physical row has this sequence.
257    /// Readers also use it to normalize foreign and legacy all-zero files.
258    pub sequence: Option<NonZeroU64>,
259    /// Partition expression from the region metadata when the file is created.
260    ///
261    /// This is stored as a PartitionExpr object in memory for convenience,
262    /// but serialized as JSON string for manifest compatibility.
263    /// Compatibility behavior:
264    /// - None: no partition expr was set when the file was created (legacy files).
265    /// - Some(expr): partition expression from region metadata.
266    #[serde(
267        serialize_with = "serialize_partition_expr",
268        deserialize_with = "deserialize_partition_expr"
269    )]
270    pub partition_expr: Option<PartitionExpr>,
271    /// Number of series in the file.
272    ///
273    /// The number is 0 if the series number is not available.
274    pub num_series: u64,
275    /// Minimum primary key value in the file, encoded as bytes.
276    /// `None` if the primary key range is not available (e.g., legacy files).
277    #[serde(
278        default,
279        skip_serializing_if = "Option::is_none",
280        serialize_with = "serialize_bytes_option",
281        deserialize_with = "deserialize_bytes_option"
282    )]
283    pub primary_key_min: Option<Bytes>,
284    /// Maximum primary key value in the file, encoded as bytes.
285    /// `None` if the primary key range is not available (e.g., legacy files).
286    #[serde(
287        default,
288        skip_serializing_if = "Option::is_none",
289        serialize_with = "serialize_bytes_option",
290        deserialize_with = "deserialize_bytes_option"
291    )]
292    pub primary_key_max: Option<Bytes>,
293    /// Whether the file preserves per-row sequence numbers usable for exact
294    /// row-level sequence filtering.
295    /// Merely retaining physical input sequences during compaction does not
296    /// restore this capability for untrusted inputs.
297    #[serde(default, skip_serializing_if = "is_false")]
298    pub preserve_row_sequence: bool,
299}
300
301fn is_false(value: &bool) -> bool {
302    !*value
303}
304
305/// Formats a debug field with a custom closure, as a stable replacement for
306/// the unstable `DebugStruct::field_with`.
307struct DebugFmt<F: Fn(&mut Formatter<'_>) -> fmt::Result>(F);
308
309impl<F: Fn(&mut Formatter<'_>) -> fmt::Result> std::fmt::Debug for DebugFmt<F> {
310    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
311        (self.0)(f)
312    }
313}
314
315impl Debug for FileMeta {
316    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
317        let mut debug_struct = f.debug_struct("FileMeta");
318        debug_struct
319            .field("region_id", &self.region_id)
320            .field("file_id", &DebugFmt(|f| write!(f, "{} ", self.file_id)))
321            .field(
322                "time_range",
323                &DebugFmt(|f| {
324                    write!(
325                        f,
326                        "({}, {}) ",
327                        self.time_range.0.to_iso8601_string(),
328                        self.time_range.1.to_iso8601_string()
329                    )
330                }),
331            )
332            .field("level", &self.level)
333            .field("file_size", &ReadableSize(self.file_size))
334            .field(
335                "max_row_group_uncompressed_size",
336                &ReadableSize(self.max_row_group_uncompressed_size),
337            );
338        if !self.available_indexes.is_empty() {
339            debug_struct
340                .field("available_indexes", &self.available_indexes)
341                .field("indexes", &self.indexes)
342                .field("index_file_size", &ReadableSize(self.index_file_size));
343        }
344        debug_struct
345            .field("num_rows", &self.num_rows)
346            .field("num_row_groups", &self.num_row_groups)
347            .field(
348                "sequence",
349                &DebugFmt(|f| match self.sequence {
350                    None => {
351                        write!(f, "None")
352                    }
353                    Some(seq) => {
354                        write!(f, "{}", seq)
355                    }
356                }),
357            )
358            .field("partition_expr", &self.partition_expr)
359            .field("num_series", &self.num_series);
360        if self.primary_key_min.is_some() || self.primary_key_max.is_some() {
361            debug_struct
362                .field(
363                    "primary_key_min",
364                    &self.primary_key_min.as_ref().map(|b| b.len()),
365                )
366                .field(
367                    "primary_key_max",
368                    &self.primary_key_max.as_ref().map(|b| b.len()),
369                );
370        }
371        debug_struct.finish()
372    }
373}
374
375/// Type of index.
376#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
377pub enum IndexType {
378    /// Inverted index.
379    InvertedIndex,
380    /// Full-text index.
381    FulltextIndex,
382    /// Bloom Filter index
383    BloomFilterIndex,
384    /// Retained for manifests written with the removed experimental vector index.
385    VectorIndex,
386}
387
388/// Metadata of indexes created for a specific column in an SST file.
389///
390/// This structure tracks which index types have been successfully created for a column.
391/// It provides more granular, column-level index information compared to the file-level
392/// `available_indexes` field in [`FileMeta`].
393///
394/// This is primarily used for:
395/// - Manual index building in asynchronous index construction mode
396/// - Verifying index consistency between files and region metadata
397#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
398#[serde(default)]
399pub struct ColumnIndexMetadata {
400    /// The column ID this index metadata applies to.
401    pub column_id: ColumnId,
402    /// List of index types that have been successfully created for this column.
403    pub created_indexes: IndexTypes,
404    /// Version of the column's fulltext bloom index. `0` means not recorded: the column
405    /// has no bloom index, or its index is the legacy format, which `ssts_index_meta`
406    /// reports as version 1.
407    #[serde(skip_serializing_if = "is_zero")]
408    pub fulltext_bloom_version: u8,
409}
410
411fn is_zero(version: &u8) -> bool {
412    *version == 0
413}
414
415impl FileMeta {
416    /// Returns whether this file's row sequences are trusted in the target region.
417    ///
418    /// Foreign files use their target-local sequence barrier; local files require
419    /// the preserve marker because their physical sequences belong to this region.
420    pub(crate) fn is_effective_target_sequence_trusted(&self, target_region_id: RegionId) -> bool {
421        if self.region_id != target_region_id {
422            self.sequence.is_some()
423        } else {
424            self.preserve_row_sequence
425        }
426    }
427
428    /// Returns the primary key range if both min and max are present.
429    pub fn primary_key_range(&self) -> Option<(Bytes, Bytes)> {
430        match (&self.primary_key_min, &self.primary_key_max) {
431            (Some(min), Some(max)) => Some((min.clone(), max.clone())),
432            _ => None,
433        }
434    }
435
436    pub fn exists_index(&self) -> bool {
437        !self.available_indexes.is_empty()
438    }
439
440    pub fn index_version(&self) -> Option<IndexVersion> {
441        if self.exists_index() {
442            Some(self.index_version)
443        } else {
444            None
445        }
446    }
447
448    /// Whether the index file is up-to-date comparing to another file meta.
449    pub fn is_index_up_to_date(&self, other: &FileMeta) -> bool {
450        self.exists_index() && other.exists_index() && self.index_version >= other.index_version
451    }
452
453    /// Returns true if the file has an inverted index
454    pub fn inverted_index_available(&self) -> bool {
455        self.available_indexes.contains(&IndexType::InvertedIndex)
456    }
457
458    /// Returns true if the file has a fulltext index
459    pub fn fulltext_index_available(&self) -> bool {
460        self.available_indexes.contains(&IndexType::FulltextIndex)
461    }
462
463    /// Returns true if the file has a bloom filter index.
464    pub fn bloom_filter_index_available(&self) -> bool {
465        self.available_indexes
466            .contains(&IndexType::BloomFilterIndex)
467    }
468
469    pub fn index_file_size(&self) -> u64 {
470        self.index_file_size
471    }
472
473    /// Check whether the file index is consistent with the given region metadata
474    /// and index options.
475    pub fn is_index_consistent_with_region(
476        &self,
477        metadata: &[ColumnMetadata],
478        index_options: &IndexOptions,
479    ) -> bool {
480        let id_to_indexes = self
481            .indexes
482            .iter()
483            .map(|index| (index.column_id, index))
484            .collect::<std::collections::HashMap<_, _>>();
485        let inverted_options = &index_options.inverted_index;
486        for column in metadata {
487            let schema = &column.column_schema;
488            // The index builder never creates these inverted indexes, so expecting
489            // them would make every manual build rebuild the file again.
490            let expects_inverted = schema.is_inverted_indexed()
491                && inverted_options.segment_row_count > 0
492                && !inverted_options
493                    .ignore_column_ids
494                    .contains(&column.column_id);
495            let expects_fulltext = schema.is_fulltext_indexed();
496            let expects_skipping = schema.is_skipping_indexed();
497            if !expects_inverted && !expects_fulltext && !expects_skipping {
498                continue;
499            }
500            let missing_index = |index_type: IndexType| {
501                debug!(
502                    "File index is missing, region_id: {}, file_id: {}, column_id: {}, index_type: {:?}",
503                    self.region_id, self.file_id, column.column_id, index_type
504                );
505                false
506            };
507            let Some(index) = id_to_indexes.get(&column.column_id) else {
508                debug!(
509                    "File has no index for column, region_id: {}, file_id: {}, column_id: {}",
510                    self.region_id, self.file_id, column.column_id
511                );
512                return false;
513            };
514            let indexes = &index.created_indexes;
515            if expects_inverted && !indexes.contains(&IndexType::InvertedIndex) {
516                return missing_index(IndexType::InvertedIndex);
517            }
518            if expects_fulltext && !indexes.contains(&IndexType::FulltextIndex) {
519                return missing_index(IndexType::FulltextIndex);
520            }
521            if schema
522                .fulltext_options()
523                .ok()
524                .flatten()
525                .is_some_and(|options| options.enable && options.backend == FulltextBackend::Bloom)
526                && index.fulltext_bloom_version < FULLTEXT_BLOOM_VERSION
527            {
528                debug!(
529                    "Fulltext bloom index is outdated, region_id: {}, file_id: {}, column_id: {}, version: {}, current: {}",
530                    self.region_id,
531                    self.file_id,
532                    column.column_id,
533                    index.fulltext_bloom_version,
534                    FULLTEXT_BLOOM_VERSION
535                );
536                return false;
537            }
538            if expects_skipping && !indexes.contains(&IndexType::BloomFilterIndex) {
539                return missing_index(IndexType::BloomFilterIndex);
540            }
541        }
542        true
543    }
544
545    /// Returns the cross-region file id.
546    pub fn file_id(&self) -> RegionFileId {
547        RegionFileId::new(self.region_id, self.file_id)
548    }
549
550    /// Returns the RegionIndexId for this file.
551    pub fn index_id(&self) -> RegionIndexId {
552        RegionIndexId::new(self.file_id(), self.index_version)
553    }
554}
555
556/// Handle to a SST file.
557#[derive(Clone)]
558pub struct FileHandle {
559    inner: Arc<FileHandleInner>,
560}
561
562impl fmt::Debug for FileHandle {
563    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
564        f.debug_struct("FileHandle")
565            .field("meta", self.meta_ref())
566            .field("compacting", &self.compacting())
567            .field("deleted", &self.is_deleted())
568            .finish()
569    }
570}
571
572/// Folds file-level sequence bounds into the output sequence bound of a merge.
573///
574/// A bound can be a row maximum or an admission marker; compaction must
575/// preserve both for sequence-based pruning. Unknown bounds must remain
576/// unknown rather than being replaced with a partial maximum.
577pub(crate) fn known_max_input_sequence<'a>(
578    inputs: impl IntoIterator<Item = &'a FileMeta>,
579) -> Option<NonZeroU64> {
580    let mut max: Option<NonZeroU64> = None;
581    for input in inputs {
582        let sequence = input.sequence?;
583        max = Some(max.map_or(sequence, |current| current.max(sequence)));
584    }
585    max
586}
587
588impl FileHandle {
589    /// Creates a new logical SST reference. Index replacements must reuse its lifetime.
590    pub fn new(meta: FileMeta, file_purger: FilePurgerRef) -> FileHandle {
591        let pk_range = meta.primary_key_range();
592        FileHandle {
593            inner: Arc::new(FileHandleInner::new(meta, file_purger, pk_range)),
594        }
595    }
596
597    #[cfg(test)]
598    pub fn new_with_primary_key_range(
599        meta: FileMeta,
600        file_purger: FilePurgerRef,
601        primary_key_range: Option<(Bytes, Bytes)>,
602    ) -> FileHandle {
603        FileHandle {
604            inner: Arc::new(FileHandleInner::new(meta, file_purger, primary_key_range)),
605        }
606    }
607
608    /// Replaces an index metadata view without splitting the underlying SST lifetime.
609    pub(crate) fn with_updated_index(&self, meta: FileMeta) -> Self {
610        debug_assert_eq!(self.file_id(), meta.file_id());
611        let primary_key_range = meta
612            .primary_key_range()
613            .or_else(|| self.raw_primary_key_range());
614        let lifetime = self.inner.lifetime.clone();
615        *lifetime.meta.lock().unwrap() = meta.clone();
616        Self {
617            inner: Arc::new(FileHandleInner::with_lifetime(
618                meta,
619                lifetime,
620                primary_key_range,
621            )),
622        }
623    }
624
625    /// Returns the region id of the file.
626    pub fn region_id(&self) -> RegionId {
627        self.inner.meta.region_id
628    }
629
630    /// Returns whether this file's row sequences are trusted in the target region.
631    ///
632    /// Foreign files use their target-local sequence barrier; local files require
633    /// the preserve marker because their physical sequences belong to this region.
634    pub(crate) fn is_effective_target_sequence_trusted(&self, target_region_id: RegionId) -> bool {
635        self.meta_ref()
636            .is_effective_target_sequence_trusted(target_region_id)
637    }
638
639    /// Returns the cross-region file id.
640    pub fn file_id(&self) -> RegionFileId {
641        RegionFileId::new(self.inner.meta.region_id, self.inner.meta.file_id)
642    }
643
644    /// Returns the RegionIndexId for this file.
645    pub fn index_id(&self) -> RegionIndexId {
646        RegionIndexId::new(self.file_id(), self.inner.meta.index_version)
647    }
648
649    /// Returns the complete file path of the file.
650    pub fn file_path(&self, table_dir: &str, path_type: PathType) -> String {
651        location::sst_file_path(table_dir, self.file_id(), path_type)
652    }
653
654    /// Returns the time range of the file.
655    pub fn time_range(&self) -> FileTimeRange {
656        self.inner.meta.time_range
657    }
658
659    /// Mark the file as deleted and will delete it on drop asynchronously
660    pub fn mark_deleted(&self) {
661        if !self.inner.lifetime.deleted.swap(true, Ordering::Relaxed) {
662            self.inner
663                .lifetime
664                .file_purger
665                .invalidate_file(&self.inner.meta);
666        }
667    }
668
669    pub fn compacting(&self) -> bool {
670        self.inner.compacting.load(Ordering::Relaxed)
671    }
672
673    pub fn set_compacting(&self, compacting: bool) {
674        self.inner.compacting.store(compacting, Ordering::Relaxed);
675    }
676
677    /// Atomically marks this file as compacting if it is currently available.
678    pub fn try_set_compacting(&self) -> bool {
679        self.inner
680            .compacting
681            .compare_exchange(false, true, Ordering::Relaxed, Ordering::Relaxed)
682            .is_ok()
683    }
684
685    /// Returns a reference to the [FileMeta].
686    pub fn meta_ref(&self) -> &FileMeta {
687        &self.inner.meta
688    }
689
690    pub fn file_purger(&self) -> FilePurgerRef {
691        self.inner.lifetime.file_purger.clone()
692    }
693
694    pub fn size(&self) -> u64 {
695        self.inner.meta.file_size
696    }
697
698    pub fn index_size(&self) -> u64 {
699        self.inner.meta.index_file_size
700    }
701
702    pub fn num_rows(&self) -> usize {
703        self.inner.meta.num_rows as usize
704    }
705
706    pub fn level(&self) -> Level {
707        self.inner.meta.level
708    }
709
710    pub fn is_deleted(&self) -> bool {
711        self.inner.lifetime.deleted.load(Ordering::Relaxed)
712    }
713
714    /// Returns bounds aligned to the caller's pinned schema, before any comparison
715    /// or aggregation. Unknown and invalid statistics cannot exclude possible data.
716    pub(crate) fn primary_key_range(
717        &self,
718        mapper: &PrimaryKeyRangeMapper,
719    ) -> Option<(Bytes, Bytes)> {
720        debug_assert_eq!(self.region_id().table_id(), mapper.region_id().table_id());
721        if let Some(range) = self
722            .inner
723            .primary_key_range
724            .read()
725            .unwrap()
726            .aligned(mapper.schema_version())
727        {
728            return range.clone();
729        }
730        // Recheck under the write lock: another snapshot may have replaced the cached schema.
731        let aligned = self.inner.primary_key_range.write().unwrap().align(mapper);
732        match aligned {
733            Ok(range) => range,
734            Err(err) => {
735                warn!(err; "Invalid SST primary key range; using unknown bounds, region: {}, file: {}, schema version: {}",
736                    self.region_id(), self.file_id(), mapper.schema_version());
737                None
738            }
739        }
740    }
741
742    /// Returns original statistics for metadata hydration, never schema-aligned bounds.
743    pub fn raw_primary_key_range(&self) -> Option<(Bytes, Bytes)> {
744        self.inner.primary_key_range.read().unwrap().raw().cloned()
745    }
746
747    pub(crate) fn set_primary_key_range(&self, primary_key_range: (Bytes, Bytes)) {
748        // SST contents are immutable. Hydrate missing raw statistics without
749        // replacing the source of already cached schema views.
750        let mut range = self.inner.primary_key_range.write().unwrap();
751        if matches!(*range, PrimaryKeyRange::Missing) {
752            *range = PrimaryKeyRange::Raw(primary_key_range);
753        }
754    }
755}
756
757type PrimaryKeyBounds = (Bytes, Bytes);
758
759/// A single-slot cache shared by file handles. Always retain the source bounds so
760/// older snapshots and changed defaults can realign without interpreting padded values as stored.
761enum PrimaryKeyRange {
762    Missing,
763    Raw(PrimaryKeyBounds),
764    Aligned {
765        raw: PrimaryKeyBounds,
766        schema_version: u64,
767        bounds: Option<PrimaryKeyBounds>,
768    },
769}
770
771impl PrimaryKeyRange {
772    fn raw(&self) -> Option<&PrimaryKeyBounds> {
773        match self {
774            Self::Missing => None,
775            Self::Raw(raw) | Self::Aligned { raw, .. } => Some(raw),
776        }
777    }
778
779    fn aligned(&self, target_version: u64) -> Option<&Option<PrimaryKeyBounds>> {
780        match self {
781            Self::Aligned {
782                schema_version,
783                bounds,
784                ..
785            } if *schema_version == target_version => Some(bounds),
786            _ => None,
787        }
788    }
789
790    fn align(
791        &mut self,
792        mapper: &PrimaryKeyRangeMapper,
793    ) -> crate::error::Result<Option<PrimaryKeyBounds>> {
794        if let Some(bounds) = self.aligned(mapper.schema_version()) {
795            return Ok(bounds.clone());
796        }
797        let Some(raw) = self.raw().cloned() else {
798            return Ok(None);
799        };
800        let aligned = mapper.map(raw.clone());
801        // Failed mappings also occupy the cache, so repeated hits don't repeat the warning.
802        *self = Self::Aligned {
803            raw,
804            schema_version: mapper.schema_version(),
805            bounds: aligned.as_ref().ok().cloned().flatten(),
806        };
807        aligned
808    }
809}
810
811/// Inner data of [FileHandle].
812///
813/// Contains meta of the file, and other mutable info like whether the file is compacting.
814struct FileHandleInner {
815    meta: FileMeta,
816    compacting: AtomicBool,
817    primary_key_range: RwLock<PrimaryKeyRange>,
818    lifetime: Arc<SstLifetime>,
819}
820
821impl Drop for FileHandleInner {
822    fn drop(&mut self) {
823        if let Some(manager) = &self.lifetime.file_ref_manager {
824            manager.remove_file(&self.meta);
825        }
826    }
827}
828
829impl FileHandleInner {
830    fn new(
831        meta: FileMeta,
832        file_purger: FilePurgerRef,
833        primary_key_range: Option<(Bytes, Bytes)>,
834    ) -> Self {
835        let file_ref_manager = file_purger.file_ref_manager();
836        let lifetime = Arc::new(SstLifetime {
837            meta: Mutex::new(meta.clone()),
838            deleted: AtomicBool::new(false),
839            file_purger,
840            file_ref_manager,
841        });
842        Self::with_lifetime(meta, lifetime, primary_key_range)
843    }
844
845    fn with_lifetime(
846        meta: FileMeta,
847        lifetime: Arc<SstLifetime>,
848        primary_key_range: Option<(Bytes, Bytes)>,
849    ) -> Self {
850        if let Some(manager) = &lifetime.file_ref_manager {
851            manager.register_file(&meta);
852        }
853        Self {
854            meta,
855            compacting: AtomicBool::new(false),
856            primary_key_range: RwLock::new(
857                primary_key_range.map_or(PrimaryKeyRange::Missing, PrimaryKeyRange::Raw),
858            ),
859            lifetime,
860        }
861    }
862}
863
864/// Physical deletion must wait for readers of every index metadata version.
865/// Independent logical references (for example, copied regions) do not share this guard.
866struct SstLifetime {
867    /// Latest metadata ensures final purge includes indexes newer than the last reader's view.
868    meta: Mutex<FileMeta>,
869    deleted: AtomicBool,
870    file_purger: FilePurgerRef,
871    /// Tracks each metadata view's readers independently for object-store index GC.
872    file_ref_manager: Option<FileReferenceManagerRef>,
873}
874
875impl Drop for SstLifetime {
876    fn drop(&mut self) {
877        if self.deleted.load(Ordering::Acquire) {
878            self.file_purger
879                .purge_file(self.meta.get_mut().unwrap().clone());
880        }
881    }
882}
883
884/// Delete files for a region.
885/// - `region_id`: Region id.
886/// - `file_ids`: List of (file id, index version) tuples to delete.
887/// - `delete_index`: Whether to delete the index file from the cache.
888/// - `access_layer`: Access layer to delete files.
889/// - `cache_manager`: Cache manager to remove files from cache.
890pub async fn delete_files(
891    region_id: RegionId,
892    file_ids: &[(FileId, u64)],
893    delete_index: bool,
894    access_layer: &AccessLayerRef,
895    cache_manager: &Option<CacheManagerRef>,
896) -> crate::error::Result<()> {
897    // Remove cached metadata for these immutable files.
898    if let Some(cache) = &cache_manager {
899        for (file_id, _) in file_ids {
900            cache.invalidate_range_results(*file_id);
901            cache
902                .remove_parquet_meta_data(RegionFileId::new(region_id, *file_id))
903                .await;
904        }
905    }
906    let mut attempted_files = Vec::with_capacity(file_ids.len());
907    let mut index_ids = Vec::new();
908
909    for (file_id, index_version) in file_ids {
910        let region_file_id = RegionFileId::new(region_id, *file_id);
911        attempted_files.push(*file_id);
912        index_ids.extend(
913            (0..=*index_version).map(|version| RegionIndexId::new(region_file_id, version)),
914        );
915    }
916
917    access_layer
918        .delete_ssts(region_id, &attempted_files)
919        .await?;
920    access_layer.delete_indexes(&index_ids).await?;
921
922    debug!(
923        "Attempted to delete {} files for region {}: {:?}",
924        attempted_files.len(),
925        region_id,
926        attempted_files
927    );
928
929    for (file_id, index_version) in file_ids {
930        purge_index_cache_stager(
931            region_id,
932            delete_index,
933            access_layer,
934            cache_manager,
935            *file_id,
936            *index_version,
937        )
938        .await;
939    }
940    Ok(())
941}
942
943/// Tracks finalized SSTs until their manifest edit is committed.
944///
945/// Flush and local compaction jobs use this to remove files that were written successfully but
946/// abandoned by cancellation or a later failure.
947#[derive(Clone)]
948pub(crate) struct UncommittedSsts {
949    region_id: RegionId,
950    files: Arc<Mutex<HashMap<FileId, (u64, bool)>>>,
951    access_layer: AccessLayerRef,
952    cache_manager: Option<CacheManagerRef>,
953}
954
955impl UncommittedSsts {
956    pub(crate) fn new(
957        region_id: RegionId,
958        access_layer: AccessLayerRef,
959        cache_manager: Option<CacheManagerRef>,
960    ) -> Self {
961        Self {
962            region_id,
963            files: Arc::new(Mutex::new(HashMap::new())),
964            access_layer,
965            cache_manager,
966        }
967    }
968
969    /// Tracks newly finalized SSTs before they become visible in the manifest.
970    pub(crate) fn track(&self, ssts: &[SstInfo]) {
971        let mut files = self.files.lock().unwrap();
972        for sst in ssts {
973            files.insert(
974                sst.file_id,
975                (sst.index_metadata.version, sst.index_metadata.file_size > 0),
976            );
977        }
978    }
979
980    /// Tracks an SST before it is uploaded, so a cancelled upload is still removed.
981    pub(crate) fn track_before_upload(&self, file_id: FileId, with_index: bool) {
982        self.files.lock().unwrap().insert(file_id, (0, with_index));
983    }
984
985    /// Disarms cleanup after the manifest edit has committed or may have committed.
986    ///
987    /// A manifest update error does not guarantee that the edit was not persisted. Once an edit
988    /// may become visible, its SSTs must be retained to avoid deleting referenced files.
989    pub(crate) fn disarm_cleanup(&self) {
990        self.files.lock().unwrap().clear();
991    }
992
993    #[cfg(test)]
994    pub(crate) fn num_tracked_files(&self) -> usize {
995        self.files.lock().unwrap().len()
996    }
997
998    /// Removes all finalized SSTs still owned by this job.
999    pub(crate) async fn cleanup(&self) {
1000        if let Err(err) = self.try_cleanup().await {
1001            error!(err; "Failed to clean uncommitted SSTs for region {}", self.region_id);
1002        }
1003    }
1004
1005    async fn try_cleanup(&self) -> crate::error::Result<()> {
1006        let files = std::mem::take(&mut *self.files.lock().unwrap());
1007        if files.is_empty() {
1008            return Ok(());
1009        }
1010
1011        let delete_index = files.values().any(|(_, exists_index)| *exists_index);
1012        let file_ids = files
1013            .into_iter()
1014            .map(|(file_id, (index_version, _))| (file_id, index_version))
1015            .collect::<Vec<_>>();
1016        delete_files(
1017            self.region_id,
1018            &file_ids,
1019            delete_index,
1020            &self.access_layer,
1021            &self.cache_manager,
1022        )
1023        .await
1024    }
1025
1026    #[cfg(test)]
1027    pub(crate) async fn cleanup_for_test(&self) -> crate::error::Result<()> {
1028        self.try_cleanup().await
1029    }
1030}
1031
1032pub async fn delete_index(
1033    region_index_id: RegionIndexId,
1034    access_layer: &AccessLayerRef,
1035    cache_manager: &Option<CacheManagerRef>,
1036) -> crate::error::Result<()> {
1037    delete_index_and_purge(region_index_id, access_layer, cache_manager).await?;
1038
1039    Ok(())
1040}
1041
1042pub async fn delete_indexes(
1043    index_ids: &[RegionIndexId],
1044    access_layer: &AccessLayerRef,
1045    cache_manager: &Option<CacheManagerRef>,
1046) -> crate::error::Result<()> {
1047    if index_ids.is_empty() {
1048        return Ok(());
1049    }
1050
1051    if let Err(e) = access_layer.delete_indexes(index_ids).await {
1052        error!(e; "Failed to batch delete index files");
1053
1054        for index_id in index_ids {
1055            delete_index_and_purge(*index_id, access_layer, cache_manager).await?;
1056        }
1057
1058        return Ok(());
1059    }
1060
1061    purge_indexes(index_ids, access_layer, cache_manager).await;
1062
1063    Ok(())
1064}
1065
1066async fn delete_index_and_purge(
1067    index_id: RegionIndexId,
1068    access_layer: &AccessLayerRef,
1069    cache_manager: &Option<CacheManagerRef>,
1070) -> crate::error::Result<()> {
1071    access_layer.delete_index(index_id).await?;
1072    purge_index_cache_stager(
1073        index_id.region_id(),
1074        true,
1075        access_layer,
1076        cache_manager,
1077        index_id.file_id(),
1078        index_id.version,
1079    )
1080    .await;
1081    Ok(())
1082}
1083
1084async fn purge_indexes(
1085    index_ids: &[RegionIndexId],
1086    access_layer: &AccessLayerRef,
1087    cache_manager: &Option<CacheManagerRef>,
1088) {
1089    for index_id in index_ids {
1090        purge_index_cache_stager(
1091            index_id.region_id(),
1092            true,
1093            access_layer,
1094            cache_manager,
1095            index_id.file_id(),
1096            index_id.version,
1097        )
1098        .await;
1099    }
1100}
1101
1102async fn purge_index_cache_stager(
1103    region_id: RegionId,
1104    delete_index: bool,
1105    access_layer: &AccessLayerRef,
1106    cache_manager: &Option<CacheManagerRef>,
1107    file_id: FileId,
1108    index_version: u64,
1109) {
1110    if let Some(write_cache) = cache_manager.as_ref().and_then(|cache| cache.write_cache()) {
1111        // Removes index file from the cache.
1112        if delete_index {
1113            write_cache
1114                .remove(IndexKey::new(
1115                    region_id,
1116                    file_id,
1117                    FileType::Puffin(index_version),
1118                ))
1119                .await;
1120        }
1121
1122        // Remove the SST file from the cache.
1123        write_cache
1124            .remove(IndexKey::new(region_id, file_id, FileType::Parquet))
1125            .await;
1126    }
1127
1128    // Purges index content in the stager.
1129    if let Err(e) = access_layer
1130        .puffin_manager_factory()
1131        .purge_stager(RegionIndexId::new(
1132            RegionFileId::new(region_id, file_id),
1133            index_version,
1134        ))
1135        .await
1136    {
1137        error!(e; "Failed to purge stager with index file, file_id: {}, index_version: {}, region: {}",
1138                file_id, index_version, region_id);
1139    }
1140}
1141
1142#[cfg(test)]
1143mod tests {
1144    use std::str::FromStr;
1145
1146    use datatypes::prelude::ConcreteDataType;
1147    use datatypes::schema::{
1148        ColumnSchema, FulltextAnalyzer, FulltextBackend, FulltextOptions, SkippingIndexOptions,
1149    };
1150    use datatypes::value::Value;
1151    use partition::expr::{PartitionExpr, col};
1152
1153    use super::*;
1154
1155    fn create_file_meta(file_id: FileId, level: Level) -> FileMeta {
1156        FileMeta {
1157            region_id: 0.into(),
1158            file_id,
1159            time_range: FileTimeRange::default(),
1160            level,
1161            file_size: 0,
1162            max_row_group_uncompressed_size: 0,
1163            available_indexes: SmallVec::from_iter([IndexType::InvertedIndex]),
1164            indexes: vec![ColumnIndexMetadata {
1165                column_id: 0,
1166                created_indexes: SmallVec::from_iter([IndexType::InvertedIndex]),
1167                ..Default::default()
1168            }],
1169            index_file_size: 0,
1170            index_version: 0,
1171            num_rows: 0,
1172            num_row_groups: 0,
1173            sequence: None,
1174            partition_expr: None,
1175            num_series: 0,
1176            ..Default::default()
1177        }
1178    }
1179
1180    #[test]
1181    fn test_deserialize_file_meta() {
1182        let file_meta = create_file_meta(FileId::random(), 0);
1183        let serialized_file_meta = serde_json::to_string(&file_meta).unwrap();
1184        let deserialized_file_meta = serde_json::from_str(&serialized_file_meta);
1185        assert_eq!(file_meta, deserialized_file_meta.unwrap());
1186    }
1187
1188    #[test]
1189    fn test_deserialize_from_string() {
1190        let json_file_meta = "{\"region_id\":0,\"file_id\":\"bc5896ec-e4d8-4017-a80d-f2de73188d55\",\
1191        \"time_range\":[{\"value\":0,\"unit\":\"Millisecond\"},{\"value\":0,\"unit\":\"Millisecond\"}],\
1192        \"available_indexes\":[\"InvertedIndex\"],\"indexes\":[{\"column_id\": 0, \"created_indexes\": [\"InvertedIndex\"]}],\"level\":0}";
1193        let file_meta = create_file_meta(
1194            FileId::from_str("bc5896ec-e4d8-4017-a80d-f2de73188d55").unwrap(),
1195            0,
1196        );
1197        let deserialized_file_meta: FileMeta = serde_json::from_str(json_file_meta).unwrap();
1198        assert_eq!(file_meta, deserialized_file_meta);
1199    }
1200
1201    #[test]
1202    fn test_deserialize_legacy_vector_index() {
1203        let json = r#"{
1204            "region_id": 0,
1205            "file_id": "bc5896ec-e4d8-4017-a80d-f2de73188d55",
1206            "time_range": [{"value":0,"unit":"Millisecond"},{"value":0,"unit":"Millisecond"}],
1207            "available_indexes": ["VectorIndex", "InvertedIndex"],
1208            "indexes": [{"column_id": 1, "created_indexes": ["VectorIndex"]}],
1209            "level": 0
1210        }"#;
1211        let meta: FileMeta = serde_json::from_str(json).unwrap();
1212        assert!(meta.inverted_index_available());
1213        assert!(!meta.fulltext_index_available());
1214        assert!(!meta.bloom_filter_index_available());
1215        let encoded = serde_json::to_value(&meta).unwrap();
1216        assert_eq!(
1217            encoded["available_indexes"],
1218            serde_json::json!(["VectorIndex", "InvertedIndex"])
1219        );
1220        assert_eq!(
1221            encoded["indexes"][0]["created_indexes"],
1222            serde_json::json!(["VectorIndex"])
1223        );
1224    }
1225
1226    #[test]
1227    fn test_file_meta_with_partition_expr() {
1228        let file_id = FileId::random();
1229        let partition_expr = PartitionExpr::new(
1230            col("a"),
1231            partition::expr::RestrictedOp::GtEq,
1232            Value::UInt32(10).into(),
1233        );
1234
1235        let file_meta_with_partition = FileMeta {
1236            region_id: 0.into(),
1237            file_id,
1238            time_range: FileTimeRange::default(),
1239            level: 0,
1240            file_size: 0,
1241            max_row_group_uncompressed_size: 0,
1242            available_indexes: SmallVec::from_iter([IndexType::InvertedIndex]),
1243            indexes: vec![ColumnIndexMetadata {
1244                column_id: 0,
1245                created_indexes: SmallVec::from_iter([IndexType::InvertedIndex]),
1246                ..Default::default()
1247            }],
1248            index_file_size: 0,
1249            index_version: 0,
1250            num_rows: 0,
1251            num_row_groups: 0,
1252            sequence: None,
1253            partition_expr: Some(partition_expr.clone()),
1254            num_series: 0,
1255            ..Default::default()
1256        };
1257
1258        // Test serialization/deserialization
1259        let serialized = serde_json::to_string(&file_meta_with_partition).unwrap();
1260        let deserialized: FileMeta = serde_json::from_str(&serialized).unwrap();
1261        assert_eq!(file_meta_with_partition, deserialized);
1262
1263        // Verify the serialized JSON contains the expected partition expression string
1264        let serialized_value: serde_json::Value = serde_json::from_str(&serialized).unwrap();
1265        assert!(serialized_value["partition_expr"].as_str().is_some());
1266        let partition_expr_json = serialized_value["partition_expr"].as_str().unwrap();
1267        assert!(partition_expr_json.contains("\"Column\":\"a\""));
1268        assert!(partition_expr_json.contains("\"op\":\"GtEq\""));
1269
1270        // Test with None (legacy files)
1271        let file_meta_none = FileMeta {
1272            partition_expr: None,
1273            ..file_meta_with_partition.clone()
1274        };
1275        let serialized_none = serde_json::to_string(&file_meta_none).unwrap();
1276        let deserialized_none: FileMeta = serde_json::from_str(&serialized_none).unwrap();
1277        assert_eq!(file_meta_none, deserialized_none);
1278    }
1279
1280    #[test]
1281    fn test_file_meta_partition_expr_backward_compatibility() {
1282        // Test that we can deserialize old JSON format with partition_expr as string
1283        let json_with_partition_expr = r#"{
1284            "region_id": 0,
1285            "file_id": "bc5896ec-e4d8-4017-a80d-f2de73188d55",
1286            "time_range": [
1287                {"value": 0, "unit": "Millisecond"},
1288                {"value": 0, "unit": "Millisecond"}
1289            ],
1290            "level": 0,
1291            "file_size": 0,
1292            "available_indexes": ["InvertedIndex"],
1293            "index_file_size": 0,
1294            "num_rows": 0,
1295            "num_row_groups": 0,
1296            "sequence": null,
1297            "partition_expr": "{\"Expr\":{\"lhs\":{\"Column\":\"a\"},\"op\":\"GtEq\",\"rhs\":{\"Value\":{\"UInt32\":10}}}}"
1298        }"#;
1299
1300        let file_meta: FileMeta = serde_json::from_str(json_with_partition_expr).unwrap();
1301        assert!(file_meta.partition_expr.is_some());
1302        let expr = file_meta.partition_expr.unwrap();
1303        assert_eq!(format!("{}", expr), "a >= 10");
1304
1305        // Test empty partition expression string
1306        let json_with_empty_expr = r#"{
1307            "region_id": 0,
1308            "file_id": "bc5896ec-e4d8-4017-a80d-f2de73188d55",
1309            "time_range": [
1310                {"value": 0, "unit": "Millisecond"},
1311                {"value": 0, "unit": "Millisecond"}
1312            ],
1313            "level": 0,
1314            "file_size": 0,
1315            "available_indexes": [],
1316            "index_file_size": 0,
1317            "num_rows": 0,
1318            "num_row_groups": 0,
1319            "sequence": null,
1320            "partition_expr": ""
1321        }"#;
1322
1323        let file_meta_empty: FileMeta = serde_json::from_str(json_with_empty_expr).unwrap();
1324        assert!(file_meta_empty.partition_expr.is_none());
1325
1326        // Test null partition expression
1327        let json_with_null_expr = r#"{
1328            "region_id": 0,
1329            "file_id": "bc5896ec-e4d8-4017-a80d-f2de73188d55",
1330            "time_range": [
1331                {"value": 0, "unit": "Millisecond"},
1332                {"value": 0, "unit": "Millisecond"}
1333            ],
1334            "level": 0,
1335            "file_size": 0,
1336            "available_indexes": [],
1337            "index_file_size": 0,
1338            "num_rows": 0,
1339            "num_row_groups": 0,
1340            "sequence": null,
1341            "partition_expr": null
1342        }"#;
1343
1344        let file_meta_null: FileMeta = serde_json::from_str(json_with_null_expr).unwrap();
1345        assert!(file_meta_null.partition_expr.is_none());
1346
1347        // Test partition expression doesn't exist
1348        let json_with_empty_expr = r#"{
1349            "region_id": 0,
1350            "file_id": "bc5896ec-e4d8-4017-a80d-f2de73188d55",
1351            "time_range": [
1352                {"value": 0, "unit": "Millisecond"},
1353                {"value": 0, "unit": "Millisecond"}
1354            ],
1355            "level": 0,
1356            "file_size": 0,
1357            "available_indexes": [],
1358            "index_file_size": 0,
1359            "num_rows": 0,
1360            "num_row_groups": 0,
1361            "sequence": null
1362        }"#;
1363
1364        let file_meta_empty: FileMeta = serde_json::from_str(json_with_empty_expr).unwrap();
1365        assert!(file_meta_empty.partition_expr.is_none());
1366    }
1367
1368    #[test]
1369    fn test_file_meta_indexes_backward_compatibility() {
1370        // Old FileMeta format without the 'indexes' field
1371        let json_old_file_meta = r#"{
1372            "region_id": 0,
1373            "file_id": "bc5896ec-e4d8-4017-a80d-f2de73188d55",
1374            "time_range": [
1375                {"value": 0, "unit": "Millisecond"},
1376                {"value": 0, "unit": "Millisecond"}
1377            ],
1378            "available_indexes": ["InvertedIndex"],
1379            "level": 0,
1380            "file_size": 0,
1381            "index_file_size": 0,
1382            "num_rows": 0,
1383            "num_row_groups": 0
1384        }"#;
1385
1386        let deserialized_file_meta: FileMeta = serde_json::from_str(json_old_file_meta).unwrap();
1387
1388        // Verify backward compatibility: indexes field should default to empty vec
1389        assert_eq!(deserialized_file_meta.indexes, vec![]);
1390
1391        let expected_indexes: IndexTypes = SmallVec::from_iter([IndexType::InvertedIndex]);
1392        assert_eq!(deserialized_file_meta.available_indexes, expected_indexes);
1393
1394        assert_eq!(
1395            deserialized_file_meta.file_id,
1396            FileId::from_str("bc5896ec-e4d8-4017-a80d-f2de73188d55").unwrap()
1397        );
1398        assert!(!deserialized_file_meta.preserve_row_sequence);
1399    }
1400
1401    #[test]
1402    fn test_file_meta_preserve_row_sequence_serde() {
1403        let file_meta = FileMeta {
1404            preserve_row_sequence: true,
1405            ..Default::default()
1406        };
1407
1408        let serialized = serde_json::to_string(&file_meta).unwrap();
1409        let value: serde_json::Value = serde_json::from_str(&serialized).unwrap();
1410        assert_eq!(value["preserve_row_sequence"], true);
1411
1412        let deserialized: FileMeta = serde_json::from_str(&serialized).unwrap();
1413        assert_eq!(file_meta, deserialized);
1414
1415        let file_meta_false = FileMeta {
1416            preserve_row_sequence: false,
1417            ..file_meta.clone()
1418        };
1419        let serialized_false = serde_json::to_string(&file_meta_false).unwrap();
1420        let value_false: serde_json::Value = serde_json::from_str(&serialized_false).unwrap();
1421        assert!(value_false.get("preserve_row_sequence").is_none());
1422    }
1423    #[test]
1424    fn test_is_index_consistent_with_region() {
1425        fn new_column_meta(
1426            id: ColumnId,
1427            name: &str,
1428            inverted: bool,
1429            fulltext: bool,
1430            skipping: bool,
1431        ) -> ColumnMetadata {
1432            let mut column_schema =
1433                ColumnSchema::new(name, ConcreteDataType::string_datatype(), true);
1434            if inverted {
1435                column_schema = column_schema.with_inverted_index(true);
1436            }
1437            if fulltext {
1438                column_schema = column_schema
1439                    .with_fulltext_options(FulltextOptions::new_unchecked(
1440                        true,
1441                        FulltextAnalyzer::English,
1442                        false,
1443                        FulltextBackend::Bloom,
1444                        1000,
1445                        0.01,
1446                    ))
1447                    .unwrap();
1448            }
1449            if skipping {
1450                column_schema = column_schema
1451                    .with_skipping_options(SkippingIndexOptions::new_unchecked(
1452                        1024,
1453                        0.01,
1454                        datatypes::schema::SkippingIndexType::BloomFilter,
1455                    ))
1456                    .unwrap();
1457            }
1458
1459            ColumnMetadata {
1460                column_schema,
1461                semantic_type: api::v1::SemanticType::Tag,
1462                column_id: id,
1463            }
1464        }
1465
1466        // Case 1: Perfect match. File has exactly the required indexes.
1467        let mut file_meta = FileMeta {
1468            indexes: vec![ColumnIndexMetadata {
1469                column_id: 1,
1470                created_indexes: SmallVec::from_iter([IndexType::InvertedIndex]),
1471                ..Default::default()
1472            }],
1473            ..Default::default()
1474        };
1475        let region_meta = vec![new_column_meta(1, "tag1", true, false, false)];
1476        assert!(file_meta.is_index_consistent_with_region(&region_meta, &IndexOptions::default()));
1477
1478        // Case 2: Superset match. File has more indexes than required.
1479        file_meta.indexes = vec![ColumnIndexMetadata {
1480            column_id: 1,
1481            created_indexes: SmallVec::from_iter([
1482                IndexType::InvertedIndex,
1483                IndexType::BloomFilterIndex,
1484            ]),
1485            ..Default::default()
1486        }];
1487        let region_meta = vec![new_column_meta(1, "tag1", true, false, false)];
1488        assert!(file_meta.is_index_consistent_with_region(&region_meta, &IndexOptions::default()));
1489
1490        // Case 3: Missing index type. File has the column but lacks the required index type.
1491        file_meta.indexes = vec![ColumnIndexMetadata {
1492            column_id: 1,
1493            created_indexes: SmallVec::from_iter([IndexType::InvertedIndex]),
1494            ..Default::default()
1495        }];
1496        let region_meta = vec![new_column_meta(1, "tag1", true, true, false)]; // Requires fulltext too
1497        assert!(!file_meta.is_index_consistent_with_region(&region_meta, &IndexOptions::default()));
1498
1499        // Case 4: Missing column. Region requires an index on a column not in the file's index list.
1500        file_meta.indexes = vec![ColumnIndexMetadata {
1501            column_id: 2, // File only has index for column 2
1502            created_indexes: SmallVec::from_iter([IndexType::InvertedIndex]),
1503            ..Default::default()
1504        }];
1505        let region_meta = vec![new_column_meta(1, "tag1", true, false, false)]; // Requires index on column 1
1506        assert!(!file_meta.is_index_consistent_with_region(&region_meta, &IndexOptions::default()));
1507
1508        // Case 5: No indexes required by region. Should always be consistent.
1509        file_meta.indexes = vec![ColumnIndexMetadata {
1510            column_id: 1,
1511            created_indexes: SmallVec::from_iter([IndexType::InvertedIndex]),
1512            ..Default::default()
1513        }];
1514        let region_meta = vec![new_column_meta(1, "tag1", false, false, false)]; // No index required
1515        assert!(file_meta.is_index_consistent_with_region(&region_meta, &IndexOptions::default()));
1516
1517        // Case 6: Empty file indexes. Region requires an index.
1518        file_meta.indexes = vec![];
1519        let region_meta = vec![new_column_meta(1, "tag1", true, false, false)];
1520        assert!(!file_meta.is_index_consistent_with_region(&region_meta, &IndexOptions::default()));
1521
1522        // Case 7: Multiple columns, one is inconsistent.
1523        file_meta.indexes = vec![
1524            ColumnIndexMetadata {
1525                column_id: 1,
1526                created_indexes: SmallVec::from_iter([IndexType::InvertedIndex]),
1527                ..Default::default()
1528            },
1529            ColumnIndexMetadata {
1530                column_id: 2, // Column 2 is missing the required BloomFilterIndex
1531                created_indexes: SmallVec::from_iter([IndexType::FulltextIndex]),
1532                ..Default::default()
1533            },
1534        ];
1535        let region_meta = vec![
1536            new_column_meta(1, "tag1", true, false, false),
1537            new_column_meta(2, "tag2", false, true, true), // Requires Fulltext and BloomFilter
1538        ];
1539        assert!(!file_meta.is_index_consistent_with_region(&region_meta, &IndexOptions::default()));
1540
1541        // Case 8: Inverted indexes the builder skips by index options are not required.
1542        file_meta.indexes = vec![];
1543        let region_meta = vec![new_column_meta(1, "tag1", true, false, false)];
1544        let mut index_options = IndexOptions::default();
1545        index_options.inverted_index.ignore_column_ids = vec![1];
1546        assert!(file_meta.is_index_consistent_with_region(&region_meta, &index_options));
1547        let mut index_options = IndexOptions::default();
1548        index_options.inverted_index.segment_row_count = 0;
1549        assert!(file_meta.is_index_consistent_with_region(&region_meta, &index_options));
1550    }
1551
1552    #[test]
1553    fn test_index_consistency_requires_current_fulltext_bloom_version() {
1554        let fulltext_column = |backend| {
1555            let column_schema = ColumnSchema::new("msg", ConcreteDataType::string_datatype(), true)
1556                .with_fulltext_options(FulltextOptions::new_unchecked(
1557                    true,
1558                    FulltextAnalyzer::English,
1559                    false,
1560                    backend,
1561                    1000,
1562                    0.01,
1563                ))
1564                .unwrap();
1565            vec![ColumnMetadata {
1566                column_schema,
1567                semantic_type: api::v1::SemanticType::Field,
1568                column_id: 1,
1569            }]
1570        };
1571        let file_meta = |fulltext_bloom_version| FileMeta {
1572            indexes: vec![ColumnIndexMetadata {
1573                column_id: 1,
1574                created_indexes: SmallVec::from_iter([IndexType::FulltextIndex]),
1575                fulltext_bloom_version,
1576            }],
1577            ..Default::default()
1578        };
1579
1580        let bloom = fulltext_column(FulltextBackend::Bloom);
1581        // Written before the version was recorded, i.e. with the old bloom blob.
1582        assert!(!file_meta(0).is_index_consistent_with_region(&bloom, &IndexOptions::default()));
1583        assert!(
1584            file_meta(FULLTEXT_BLOOM_VERSION)
1585                .is_index_consistent_with_region(&bloom, &IndexOptions::default())
1586        );
1587        // Tantivy indexes carry no bloom version.
1588        let tantivy = fulltext_column(FulltextBackend::Tantivy);
1589        assert!(file_meta(0).is_index_consistent_with_region(&tantivy, &IndexOptions::default()));
1590    }
1591}