Skip to main content

mito2/manifest/
action.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//! Defines [RegionMetaAction] related structs and [RegionCheckpoint].
16
17use std::collections::{BTreeMap, HashMap, HashSet};
18use std::time::Duration;
19
20use chrono::Utc;
21use common_telemetry::warn;
22use serde::{Deserialize, Serialize};
23use snafu::{OptionExt, ResultExt};
24use store_api::ManifestVersion;
25use store_api::metadata::RegionMetadataRef;
26use store_api::storage::{FileId, IndexVersion, RegionId, SequenceNumber};
27use strum::Display;
28
29use crate::error::{RegionMetadataNotFoundSnafu, Result, SerdeJsonSnafu, Utf8Snafu};
30use crate::manifest::manager::RemoveFileOptions;
31use crate::region::ManifestStats;
32use crate::sst::FormatType;
33use crate::sst::file::{self, FileMeta, FileMetaRef};
34use crate::sst::file_metadata::FileMetadataInternerRef;
35use crate::wal::EntryId;
36
37/// Actions that can be applied to region manifest.
38#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, Display)]
39pub enum RegionMetaAction {
40    /// Change region's metadata for request like ALTER
41    Change(RegionChange),
42    /// Change only region partition expression metadata.
43    PartitionExprChange(RegionPartitionExprChange),
44    /// Edit region's state for changing options or file list.
45    Edit(RegionEdit),
46    /// Remove the region.
47    Remove(RegionRemove),
48    /// Truncate the region.
49    Truncate(RegionTruncate),
50}
51
52#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
53pub struct RegionPartitionExprChange {
54    /// Partition expression serialized as JSON.
55    pub partition_expr: Option<String>,
56}
57
58impl RegionMetaAction {
59    /// Returns true if the action is a change action.
60    pub fn is_change(&self) -> bool {
61        matches!(self, RegionMetaAction::Change(_))
62    }
63
64    /// Returns true if the action is an edit action.
65    pub fn is_edit(&self) -> bool {
66        matches!(self, RegionMetaAction::Edit(_))
67    }
68
69    /// Returns true if the action is a partition expr change action.
70    pub fn is_partition_expr_change(&self) -> bool {
71        matches!(self, RegionMetaAction::PartitionExprChange(_))
72    }
73}
74
75#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
76pub struct RegionChange {
77    /// The metadata after changed.
78    pub metadata: RegionMetadataRef,
79    /// Format of the SST.
80    #[serde(default)]
81    pub sst_format: FormatType,
82    /// Whether the region is in append mode.
83    #[serde(default)]
84    pub append_mode: Option<bool>,
85}
86
87#[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq, Eq)]
88pub struct RegionEdit {
89    pub files_to_add: Vec<FileMeta>,
90    pub files_to_remove: Vec<FileMeta>,
91    /// event unix timestamp in milliseconds, help to determine file deletion time.
92    #[serde(default)]
93    pub timestamp_ms: Option<i64>,
94    #[serde(with = "humantime_serde")]
95    pub compaction_time_window: Option<Duration>,
96    pub flushed_entry_id: Option<EntryId>,
97    pub flushed_sequence: Option<SequenceNumber>,
98    pub committed_sequence: Option<SequenceNumber>,
99    #[serde(flatten)]
100    pub wal_recovery: WalRecoveryEdit,
101}
102
103/// Changes an edit makes to the WAL ranges the region still has to recover.
104#[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq, Eq)]
105pub struct WalRecoveryEdit {
106    /// The new [RegionManifest::pending_tails], whose segments of each node keep the
107    /// order and bounds described there. `None` keeps them unchanged.
108    #[serde(default, skip_serializing_if = "Option::is_none")]
109    pub pending_tails: Option<Vec<PendingTail>>,
110    /// Replaces the handoff floor of one node.
111    #[serde(default, skip_serializing_if = "Option::is_none")]
112    pub handoff_floor: Option<HandoffFloor>,
113}
114
115/// WAL entries of one node in `(after, upto]` that the region still has to recover.
116#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
117pub struct PendingTail {
118    /// Id of the datanode whose WAL holds the entries.
119    pub node_id: u64,
120    pub after: EntryId,
121    pub upto: EntryId,
122    pub upto_kind: UptoKind,
123}
124
125/// Where the upper bound of a [PendingTail] comes from.
126#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq)]
127#[serde(rename_all = "snake_case")]
128pub enum UptoKind {
129    /// The last entry id the node reported, which names an entry that can be replayed.
130    Reported,
131    /// An upper bound found by listing the WAL objects of the node, which may name no entry.
132    Listed,
133}
134
135/// Entries of a node at or below `entry_id` are never recovered from its WAL, except
136/// those in a pending tail of the node.
137#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq)]
138pub struct HandoffFloor {
139    pub node_id: u64,
140    pub entry_id: EntryId,
141}
142
143/// Caps a release of the WAL of node `node_id` at `entry_id`, so that it keeps every
144/// pending tail of the node.
145pub(crate) fn cap_wal_release(
146    pending_tails: &[PendingTail],
147    node_id: Option<u64>,
148    entry_id: EntryId,
149) -> EntryId {
150    pending_tails
151        .iter()
152        .filter(|tail| Some(tail.node_id) == node_id)
153        .map(|tail| tail.after)
154        .fold(entry_id, EntryId::min)
155}
156
157impl RegionEdit {
158    /// Shared reclassification must be add-only for a file ID: the manifest removes
159    /// by ID, while the in-memory Version removes from the specified level.
160    pub(crate) fn validate_shared_reclassification(&self, region_id: RegionId) -> Result<()> {
161        let shared_ids: HashSet<_> = self
162            .files_to_add
163            .iter()
164            .filter(|file| file.level == file::LEVEL_SHARED)
165            .map(|file| file.file_id)
166            .collect();
167        snafu::ensure!(
168            shared_ids.is_empty()
169                || self
170                    .files_to_remove
171                    .iter()
172                    .all(|file| !shared_ids.contains(&file.file_id)),
173            crate::error::InvalidRequestSnafu {
174                region_id,
175                reason: "Cannot add and remove the same shared SST in one edit",
176            }
177        );
178        Ok(())
179    }
180}
181
182#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
183pub struct RegionRemove {
184    pub region_id: RegionId,
185}
186
187/// Last data truncated in the region.
188///
189#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
190pub struct RegionTruncate {
191    pub region_id: RegionId,
192    #[serde(flatten)]
193    pub kind: TruncateKind,
194    /// event unix timestamp in milliseconds, help to determine file deletion time.
195    #[serde(default)]
196    pub timestamp_ms: Option<i64>,
197}
198
199/// The kind of truncate operation.
200#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
201#[serde(untagged)]
202pub enum TruncateKind {
203    /// Truncate all data in the region, marked by all data before the given entry id&sequence.
204    All {
205        /// Last WAL entry id of truncated data.
206        truncated_entry_id: EntryId,
207        // Last sequence number of truncated data.
208        truncated_sequence: SequenceNumber,
209    },
210    /// Only remove certain files in the region.
211    Partial { files_to_remove: Vec<FileMeta> },
212}
213
214/// The region manifest data.
215#[derive(Serialize, Deserialize, Clone, Debug)]
216#[cfg_attr(test, derive(Eq))]
217pub struct RegionManifest {
218    /// Metadata of the region.
219    pub metadata: RegionMetadataRef,
220    /// SST files.
221    pub files: HashMap<FileId, FileMetaRef>,
222    /// Removed files, which are not in the current manifest but may still be kept for a while.
223    /// This is a list of (set of files, timestamp) pairs, where the timestamp is the time when
224    /// the files are removed from manifest. The timestamp is in milliseconds since unix epoch.
225    ///
226    /// Using same checkpoint files and action files, the recovered manifest may differ in this
227    /// `removed_files` field, because the checkpointer may evict some removed files using
228    /// current machine time. This is acceptable because the removed files are not used in normal
229    /// read/write path.
230    ///
231    #[serde(default)]
232    pub removed_files: RemovedFilesRecord,
233    /// Last WAL entry id of flushed data.
234    pub flushed_entry_id: EntryId,
235    /// Last sequence of flushed data.
236    pub flushed_sequence: SequenceNumber,
237    pub committed_sequence: Option<SequenceNumber>,
238    /// Current manifest version.
239    pub manifest_version: ManifestVersion,
240    /// Last WAL entry id of truncated data.
241    pub truncated_entry_id: Option<EntryId>,
242    /// Inferred compaction time window.
243    #[serde(with = "humantime_serde")]
244    pub compaction_time_window: Option<Duration>,
245    /// Format of the SST file.
246    #[serde(default)]
247    pub sst_format: FormatType,
248    /// Whether the region is in append mode.
249    #[serde(default)]
250    pub append_mode: Option<bool>,
251    /// WAL ranges the region still has to recover, in the order they were added.
252    ///
253    /// The segments of one node are increasing and do not overlap, and each has
254    /// `after < upto`: a new segment of a node takes as `after` the largest of the
255    /// flushed entry id, the `upto`s of the node's existing segments and its handoff
256    /// floor, and is added only if that is below its `upto`. So the entries of a node
257    /// between two of its segments are flushed or at or below a handoff floor of the
258    /// node, and need no replay.
259    #[serde(default, skip_serializing_if = "Vec::is_empty")]
260    pub pending_tails: Vec<PendingTail>,
261    /// The [HandoffFloor] of each node, removed once the flushed entry id reaches it.
262    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
263    pub handoff_floors: BTreeMap<u64, EntryId>,
264}
265
266impl RegionManifest {
267    /// Returns the entry id after which node `node_id` replays its WAL when it opens
268    /// the region. Entries up to it are flushed, left to the pending tails, or below
269    /// the handoff floor of the node.
270    ///
271    /// Entries of the node in `(after, upto]` of one of its tails belong to that tail.
272    /// Whoever adds a tail ensures that every entry id the node allocates afterwards is
273    /// above the tail's `upto`, so no later write of the node falls in a skipped range.
274    pub(crate) fn wal_replay_start(&self, node_id: Option<u64>) -> EntryId {
275        let floor = node_id.and_then(|node_id| self.handoff_floors.get(&node_id).copied());
276        self.pending_tails
277            .iter()
278            .filter(|tail| Some(tail.node_id) == node_id)
279            .map(|tail| tail.upto)
280            .chain(floor)
281            .fold(self.flushed_entry_id, EntryId::max)
282    }
283}
284
285#[cfg(test)]
286impl PartialEq for RegionManifest {
287    fn eq(&self, other: &Self) -> bool {
288        self.metadata == other.metadata
289            && self.files == other.files
290            && self.flushed_entry_id == other.flushed_entry_id
291            && self.flushed_sequence == other.flushed_sequence
292            && self.manifest_version == other.manifest_version
293            && self.truncated_entry_id == other.truncated_entry_id
294            && self.compaction_time_window == other.compaction_time_window
295            && self.committed_sequence == other.committed_sequence
296            && self.pending_tails == other.pending_tails
297            && self.handoff_floors == other.handoff_floors
298    }
299}
300
301impl RegionManifest {
302    /// Rejects prototype task state before deserializing, so callers cannot
303    /// silently drop recovery evidence that this build cannot interpret.
304    pub(crate) fn decode(bytes: &[u8]) -> Result<Self> {
305        crate::manifest::compat::validate_manifest(bytes)?;
306        serde_json::from_slice(bytes).context(SerdeJsonSnafu)
307    }
308}
309
310#[derive(Debug, Default)]
311pub struct RegionManifestBuilder {
312    file_metadata_interner: FileMetadataInternerRef,
313    metadata: Option<RegionMetadataRef>,
314    files: HashMap<FileId, FileMetaRef>,
315    pub removed_files: RemovedFilesRecord,
316    flushed_entry_id: EntryId,
317    flushed_sequence: SequenceNumber,
318    manifest_version: ManifestVersion,
319    truncated_entry_id: Option<EntryId>,
320    compaction_time_window: Option<Duration>,
321    committed_sequence: Option<SequenceNumber>,
322    sst_format: FormatType,
323    append_mode: Option<bool>,
324    pending_tails: Vec<PendingTail>,
325    handoff_floors: BTreeMap<u64, EntryId>,
326}
327
328impl RegionManifestBuilder {
329    /// Creates an empty builder with the owner's metadata interner.
330    pub(crate) fn new(file_metadata_interner: FileMetadataInternerRef) -> Self {
331        Self {
332            file_metadata_interner,
333            ..Default::default()
334        }
335    }
336
337    /// Canonicalizes independently decoded checkpoint entries before publication.
338    pub(crate) fn with_checkpoint(
339        checkpoint: Option<RegionManifest>,
340        file_metadata_interner: FileMetadataInternerRef,
341    ) -> Self {
342        let mut builder = Self::from_shared_manifest(checkpoint, file_metadata_interner);
343        for meta in builder.files.values_mut() {
344            builder.file_metadata_interner.intern_in_place(meta);
345        }
346        builder
347    }
348
349    fn removed_file(&self, file: &FileMeta) -> RemovedFile {
350        let index_version = self
351            .files
352            .get(&file.file_id)
353            .and_then(|meta| meta.index_version())
354            .or_else(|| file.index_version());
355        RemovedFile::File(file.file_id, index_version)
356    }
357
358    /// Reuses entries from a snapshot already canonicalized by the same owner.
359    pub(crate) fn from_shared_manifest(
360        checkpoint: Option<RegionManifest>,
361        file_metadata_interner: FileMetadataInternerRef,
362    ) -> Self {
363        if let Some(s) = checkpoint {
364            Self {
365                file_metadata_interner,
366                metadata: Some(s.metadata),
367                files: s.files,
368                removed_files: s.removed_files,
369                flushed_entry_id: s.flushed_entry_id,
370                manifest_version: s.manifest_version,
371                flushed_sequence: s.flushed_sequence,
372                truncated_entry_id: s.truncated_entry_id,
373                compaction_time_window: s.compaction_time_window,
374                committed_sequence: s.committed_sequence,
375                sst_format: s.sst_format,
376                append_mode: s.append_mode,
377                pending_tails: s.pending_tails,
378                handoff_floors: s.handoff_floors,
379            }
380        } else {
381            Self::new(file_metadata_interner)
382        }
383    }
384
385    pub fn apply_change(&mut self, manifest_version: ManifestVersion, change: RegionChange) {
386        self.metadata = Some(change.metadata);
387        self.manifest_version = manifest_version;
388        self.sst_format = change.sst_format;
389        // Only update append_mode if the change specifies a value.
390        self.append_mode = change.append_mode.or(self.append_mode);
391    }
392
393    /// Applies a partition-expression-only metadata change.
394    ///
395    /// This path updates only `partition_expr` (and its derived
396    /// `partition_rule_version`) on current metadata and does not touch
397    /// `sst_format`.
398    pub fn apply_partition_expr_change(
399        &mut self,
400        manifest_version: ManifestVersion,
401        change: RegionPartitionExprChange,
402    ) {
403        if let Some(metadata) = &self.metadata {
404            let mut metadata = metadata.as_ref().clone();
405            metadata.set_partition_expr(change.partition_expr);
406            self.metadata = Some(metadata.into());
407            self.manifest_version = manifest_version;
408        } else {
409            warn!(
410                "metadata is not set in region manifest builder, ignore partition expr change: {:?}",
411                change
412            );
413        }
414    }
415
416    pub fn apply_edit(&mut self, manifest_version: ManifestVersion, edit: RegionEdit) {
417        self.manifest_version = manifest_version;
418
419        let mut removed_files = vec![];
420        for file in edit.files_to_add {
421            let file = FileMetaRef::from(file);
422            let file = self.file_metadata_interner.intern(file);
423            if let Some(old_file) = self.files.insert(file.file_id, file.clone())
424                && let Some(old_index) = old_file.index_version()
425                && !old_file.is_index_up_to_date(&file)
426            {
427                // The old file has an index that is now outdated.
428                removed_files.push(RemovedFile::Index(old_file.file_id, old_index));
429            }
430        }
431        removed_files.extend(
432            edit.files_to_remove
433                .iter()
434                .map(|file| self.removed_file(file)),
435        );
436        let at = edit
437            .timestamp_ms
438            .unwrap_or_else(|| Utc::now().timestamp_millis());
439        self.removed_files.add_removed_files(removed_files, at);
440
441        for file in edit.files_to_remove {
442            self.files.remove(&file.file_id);
443        }
444        if let Some(flushed_entry_id) = edit.flushed_entry_id {
445            self.flushed_entry_id = self.flushed_entry_id.max(flushed_entry_id);
446        }
447        if let Some(flushed_sequence) = edit.flushed_sequence {
448            self.flushed_sequence = self.flushed_sequence.max(flushed_sequence);
449        }
450
451        if let Some(committed_sequence) = edit.committed_sequence {
452            self.committed_sequence = Some(
453                self.committed_sequence
454                    .map_or(committed_sequence, |exist| exist.max(committed_sequence)),
455            );
456        }
457        if let Some(window) = edit.compaction_time_window {
458            self.compaction_time_window = Some(window);
459        }
460        if let Some(pending_tails) = edit.wal_recovery.pending_tails {
461            self.pending_tails = pending_tails;
462        }
463        if let Some(floor) = edit.wal_recovery.handoff_floor {
464            self.handoff_floors.insert(floor.node_id, floor.entry_id);
465        }
466    }
467
468    pub fn apply_truncate(&mut self, manifest_version: ManifestVersion, truncate: RegionTruncate) {
469        self.manifest_version = manifest_version;
470        match truncate.kind {
471            TruncateKind::All {
472                truncated_entry_id,
473                truncated_sequence,
474            } => {
475                self.flushed_entry_id = truncated_entry_id;
476                self.flushed_sequence = truncated_sequence;
477                self.truncated_entry_id = Some(truncated_entry_id);
478                self.removed_files.add_removed_files(
479                    self.files
480                        .values()
481                        .map(|f| RemovedFile::File(f.file_id, f.index_version()))
482                        .collect(),
483                    truncate
484                        .timestamp_ms
485                        .unwrap_or_else(|| Utc::now().timestamp_millis()),
486                );
487                self.files.clear();
488            }
489            TruncateKind::Partial { files_to_remove } => {
490                // With GC disabled, VersionControl may still hold an older
491                // FileMeta and LocalFilePurger can miss a just-committed newer
492                // index generation. We accept this narrow local-mode orphan
493                // window; object-store deployments enable GC and collect the
494                // generation recorded here.
495                self.removed_files.add_removed_files(
496                    files_to_remove
497                        .iter()
498                        .map(|file| self.removed_file(file))
499                        .collect(),
500                    truncate
501                        .timestamp_ms
502                        .unwrap_or_else(|| Utc::now().timestamp_millis()),
503                );
504                for file in files_to_remove {
505                    self.files.remove(&file.file_id);
506                }
507            }
508        }
509    }
510
511    pub fn files(&self) -> &HashMap<FileId, FileMetaRef> {
512        &self.files
513    }
514
515    /// Check if the builder keeps a [RegionMetadata](store_api::metadata::RegionMetadata).
516    pub fn contains_metadata(&self) -> bool {
517        self.metadata.is_some()
518    }
519
520    pub fn try_build(mut self) -> Result<RegionManifest> {
521        let metadata = self.metadata.context(RegionMetadataNotFoundSnafu)?;
522        // A floor at or below the flushed entry id no longer raises the replay start.
523        self.handoff_floors
524            .retain(|_, floor| *floor > self.flushed_entry_id);
525        Ok(RegionManifest {
526            metadata,
527            files: self.files,
528            removed_files: self.removed_files,
529            flushed_entry_id: self.flushed_entry_id,
530            flushed_sequence: self.flushed_sequence,
531            committed_sequence: self.committed_sequence,
532            manifest_version: self.manifest_version,
533            truncated_entry_id: self.truncated_entry_id,
534            compaction_time_window: self.compaction_time_window,
535            sst_format: self.sst_format,
536            append_mode: self.append_mode,
537            pending_tails: self.pending_tails,
538            handoff_floors: self.handoff_floors,
539        })
540    }
541}
542
543/// A record of removed files in the region manifest.
544/// This is used to keep track of files that have been removed from the manifest but may still
545/// be kept for a while
546#[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq, Eq)]
547pub struct RemovedFilesRecord {
548    /// a list of `(FileIds, timestamp)` pairs, where the timestamp is the time when
549    /// the files are removed from manifest. The timestamp is in milliseconds since unix epoch.
550    pub removed_files: Vec<RemovedFiles>,
551}
552
553impl RemovedFilesRecord {
554    /// Clear the actually deleted files from the list of removed files
555    pub fn clear_deleted_files(&mut self, deleted_files: Vec<RemovedFile>) {
556        let deleted_file_set: HashSet<_> = HashSet::from_iter(deleted_files);
557        for files in self.removed_files.iter_mut() {
558            files
559                .files
560                .retain(|removed| !deleted_file_set.contains(removed));
561        }
562
563        self.removed_files.retain(|fs| !fs.files.is_empty());
564    }
565
566    pub fn update_file_removed_cnt_to_stats(&self, stats: &ManifestStats) {
567        let cnt = self
568            .removed_files
569            .iter()
570            .map(|r| r.files.len() as u64)
571            .sum();
572        stats
573            .file_removed_cnt
574            .store(cnt, std::sync::atomic::Ordering::Relaxed);
575    }
576}
577
578#[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq, Eq)]
579pub struct RemovedFiles {
580    /// The timestamp is the time when
581    /// the files are removed from manifest. The timestamp is in milliseconds since unix epoch.
582    pub removed_at: i64,
583    /// The set of file ids that are removed.
584    #[serde(default)]
585    pub files: HashSet<RemovedFile>,
586}
587
588/// A removed file, which can be a data file(optional paired with a index file) or an outdated index file.
589#[derive(Serialize, Hash, Clone, Debug, PartialEq, Eq)]
590pub enum RemovedFile {
591    File(FileId, Option<IndexVersion>),
592    Index(FileId, IndexVersion),
593}
594
595/// Support deserialize from old format(just FileId as string) for backward compatibility
596/// into current format(RemovedFile enum).
597/// This is needed just in case there are old manifests with removed files recorded.
598impl<'de> Deserialize<'de> for RemovedFile {
599    fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
600    where
601        D: serde::Deserializer<'de>,
602    {
603        #[derive(Deserialize)]
604        #[serde(untagged)]
605        enum CompatRemovedFile {
606            Enum(RemovedFileEnum),
607            FileId(FileId),
608        }
609
610        #[derive(Deserialize)]
611        enum RemovedFileEnum {
612            File(FileId, Option<IndexVersion>),
613            Index(FileId, IndexVersion),
614        }
615
616        let compat = CompatRemovedFile::deserialize(deserializer)?;
617        match compat {
618            CompatRemovedFile::FileId(file_id) => Ok(RemovedFile::File(file_id, None)),
619            CompatRemovedFile::Enum(e) => match e {
620                RemovedFileEnum::File(file_id, version) => Ok(RemovedFile::File(file_id, version)),
621                RemovedFileEnum::Index(file_id, version) => {
622                    Ok(RemovedFile::Index(file_id, version))
623                }
624            },
625        }
626    }
627}
628
629impl RemovedFile {
630    pub fn file_id(&self) -> FileId {
631        match self {
632            RemovedFile::File(file_id, _) => *file_id,
633            RemovedFile::Index(file_id, _) => *file_id,
634        }
635    }
636
637    pub fn index_version(&self) -> Option<IndexVersion> {
638        match self {
639            RemovedFile::File(_, index_version) => *index_version,
640            RemovedFile::Index(_, index_version) => Some(*index_version),
641        }
642    }
643}
644
645impl RemovedFilesRecord {
646    /// Add a record of removed files with the current timestamp.
647    pub fn add_removed_files(&mut self, removed: Vec<RemovedFile>, at: i64) {
648        if removed.is_empty() {
649            return;
650        }
651        let files = removed.into_iter().collect();
652        self.removed_files.push(RemovedFiles {
653            removed_at: at,
654            files,
655        });
656    }
657
658    pub fn evict_old_removed_files(&mut self, opt: &RemoveFileOptions) -> Result<()> {
659        if !opt.enable_gc {
660            // If GC is not enabled, always keep removed files empty.
661            self.removed_files.clear();
662            return Ok(());
663        }
664
665        // if GC is enabled, rely on gc worker to delete files, and evict removed files based on options.
666
667        Ok(())
668    }
669}
670
671// The checkpoint of region manifest, generated by checkpointer.
672#[derive(Serialize, Deserialize, Debug, Clone)]
673#[cfg_attr(test, derive(PartialEq, Eq))]
674pub struct RegionCheckpoint {
675    /// The last manifest version that this checkpoint compacts(inclusive).
676    pub last_version: ManifestVersion,
677    // The number of manifest actions that this checkpoint compacts.
678    pub compacted_actions: usize,
679    // The checkpoint data
680    pub checkpoint: Option<RegionManifest>,
681}
682
683impl RegionCheckpoint {
684    pub fn last_version(&self) -> ManifestVersion {
685        self.last_version
686    }
687
688    pub fn encode(&self) -> Result<Vec<u8>> {
689        let json = serde_json::to_string(&self).context(SerdeJsonSnafu)?;
690
691        Ok(json.into_bytes())
692    }
693
694    pub fn decode(bytes: &[u8]) -> Result<Self> {
695        let data = std::str::from_utf8(bytes).context(Utf8Snafu)?;
696        crate::manifest::compat::validate_checkpoint(data)?;
697        serde_json::from_str(data).context(SerdeJsonSnafu)
698    }
699}
700
701#[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq, Eq)]
702pub struct RegionMetaActionList {
703    pub actions: Vec<RegionMetaAction>,
704}
705
706impl RegionMetaActionList {
707    pub fn with_action(action: RegionMetaAction) -> Self {
708        Self {
709            actions: vec![action],
710        }
711    }
712
713    pub fn new(actions: Vec<RegionMetaAction>) -> Self {
714        Self { actions }
715    }
716
717    /// Split the actions into a partition expr change, a region change and an edit.
718    pub fn split_region_change_and_edit(
719        self,
720    ) -> (
721        Option<RegionPartitionExprChange>,
722        Option<RegionChange>,
723        RegionEdit,
724    ) {
725        let mut edit = RegionEdit {
726            files_to_add: Vec::new(),
727            files_to_remove: Vec::new(),
728            timestamp_ms: None,
729            compaction_time_window: None,
730            flushed_entry_id: None,
731            flushed_sequence: None,
732            committed_sequence: None,
733            wal_recovery: Default::default(),
734        };
735        let mut partition_expr_change = None;
736        let mut region_change = None;
737        for action in self.actions {
738            match action {
739                RegionMetaAction::PartitionExprChange(change) => {
740                    partition_expr_change = Some(change);
741                }
742                RegionMetaAction::Change(change) => {
743                    region_change = Some(change);
744                }
745                RegionMetaAction::Edit(region_edit) => {
746                    // Merge file adds/removes
747                    edit.files_to_add.extend(region_edit.files_to_add);
748                    edit.files_to_remove.extend(region_edit.files_to_remove);
749                    // Max of flushed entry id / sequence
750                    if let Some(eid) = region_edit.flushed_entry_id {
751                        edit.flushed_entry_id =
752                            Some(edit.flushed_entry_id.map_or(eid, |v| v.max(eid)));
753                    }
754                    if let Some(seq) = region_edit.flushed_sequence {
755                        edit.flushed_sequence =
756                            Some(edit.flushed_sequence.map_or(seq, |v| v.max(seq)));
757                    }
758                    if let Some(seq) = region_edit.committed_sequence {
759                        edit.committed_sequence =
760                            Some(edit.committed_sequence.map_or(seq, |v| v.max(seq)));
761                    }
762                    // Prefer the latest non-none time window
763                    if region_edit.compaction_time_window.is_some() {
764                        edit.compaction_time_window = region_edit.compaction_time_window;
765                    }
766                }
767                _ => {}
768            }
769        }
770
771        (partition_expr_change, region_change, edit)
772    }
773}
774
775impl RegionMetaActionList {
776    /// Encode self into json in the form of string lines.
777    pub fn encode(&self) -> Result<Vec<u8>> {
778        let json = serde_json::to_string(&self).context(SerdeJsonSnafu)?;
779
780        Ok(json.into_bytes())
781    }
782
783    pub fn decode(bytes: &[u8]) -> Result<Self> {
784        let data = std::str::from_utf8(bytes).context(Utf8Snafu)?;
785        crate::manifest::compat::validate_actions(data)?;
786        serde_json::from_str(data).context(SerdeJsonSnafu)
787    }
788}
789
790#[cfg(test)]
791mod tests {
792    use std::sync::Arc;
793
794    use common_time::Timestamp;
795
796    use crate::sst::file_metadata::MetadataRef;
797
798    use super::*;
799
800    // These tests are used to ensure backward compatibility of manifest files.
801    // DO NOT modify the serialized string when they fail, check if your
802    // modification to manifest-related structs is compatible with older manifests.
803    #[test]
804    fn test_sketch_index_manifest_fixture() {
805        use crate::sst::file::IndexType;
806
807        // Frozen persisted names. Keep this fixture unchanged when extending the format.
808        let fixture = r#"{"actions":[{"Edit":{"files_to_add":[{
809                "region_id":0,"file_id":"bc5896ec-e4d8-4017-a80d-f2de73188d55",
810                "time_range":[{"value":0,"unit":"Millisecond"},{"value":1,"unit":"Millisecond"}],
811                "level":0,"index_version":7,"index_file_size":128,
812                "available_indexes":["InvertedIndex","HllIndex","KllIndex","MgIndex"],
813                "indexes":[{"column_id":3,"created_indexes":["InvertedIndex","HllIndex","KllIndex","MgIndex"]}]
814            }],"files_to_remove":[],"compaction_time_window":null}}]}"#;
815        let actions = RegionMetaActionList::decode(fixture.as_bytes()).unwrap();
816        let RegionMetaAction::Edit(edit) = &actions.actions[0] else {
817            panic!("expected a file edit");
818        };
819        assert_eq!(actions.actions.len(), 1);
820        assert_eq!(edit.files_to_add.len(), 1);
821        let file = &edit.files_to_add[0];
822        let expected = [
823            IndexType::InvertedIndex,
824            IndexType::HllIndex,
825            IndexType::KllIndex,
826            IndexType::MgIndex,
827        ];
828        assert_eq!(file.available_indexes.as_slice(), expected);
829        assert_eq!(file.indexes.len(), 1);
830        assert_eq!(file.indexes[0].column_id, 3);
831        assert_eq!(file.indexes[0].created_indexes.as_slice(), expected);
832        assert_eq!(file.index_version(), Some(7));
833        assert_eq!(file.index_file_size, 128);
834        let encoded = actions.encode().unwrap();
835        assert_eq!(RegionMetaActionList::decode(&encoded).unwrap(), actions);
836        let encoded: serde_json::Value = serde_json::from_slice(&encoded).unwrap();
837        let names = serde_json::json!(["InvertedIndex", "HllIndex", "KllIndex", "MgIndex"]);
838        assert_eq!(
839            encoded["actions"][0]["Edit"]["files_to_add"][0]["available_indexes"],
840            names
841        );
842        assert_eq!(
843            encoded["actions"][0]["Edit"]["files_to_add"][0]["indexes"][0]["created_indexes"],
844            names
845        );
846    }
847
848    #[test]
849    fn test_region_action_compatibility() {
850        let region_edit = r#"{
851            "flushed_entry_id":null,
852            "compaction_time_window":null,
853            "files_to_add":[
854            {"region_id":4402341478400,"file_id":"4b220a70-2b03-4641-9687-b65d94641208","time_range":[{"value":1451609210000,"unit":"Millisecond"},{"value":1451609520000,"unit":"Millisecond"}],"level":1,"file_size":100}
855            ],
856            "files_to_remove":[
857            {"region_id":4402341478400,"file_id":"34b6ebb9-b8a5-4a4b-b744-56f67defad02","time_range":[{"value":1451609210000,"unit":"Millisecond"},{"value":1451609520000,"unit":"Millisecond"}],"level":0,"file_size":100}
858            ]
859        }"#;
860        let _ = serde_json::from_str::<RegionEdit>(region_edit).unwrap();
861
862        let region_edit = r#"{
863            "flushed_entry_id":10,
864            "flushed_sequence":10,
865            "compaction_time_window":null,
866            "files_to_add":[
867            {"region_id":4402341478400,"file_id":"4b220a70-2b03-4641-9687-b65d94641208","time_range":[{"value":1451609210000,"unit":"Millisecond"},{"value":1451609520000,"unit":"Millisecond"}],"level":1,"file_size":100}
868            ],
869            "files_to_remove":[
870            {"region_id":4402341478400,"file_id":"34b6ebb9-b8a5-4a4b-b744-56f67defad02","time_range":[{"value":1451609210000,"unit":"Millisecond"},{"value":1451609520000,"unit":"Millisecond"}],"level":0,"file_size":100}
871            ]
872        }"#;
873        let _ = serde_json::from_str::<RegionEdit>(region_edit).unwrap();
874
875        // Note: For backward compatibility, the test accepts a RegionChange without sst_format
876        let region_change = r#" {
877            "metadata":{
878                "column_metadatas":[
879                {"column_schema":{"name":"a","data_type":{"Int64":{}},"is_nullable":false,"is_time_index":false,"default_constraint":null,"metadata":{}},"semantic_type":"Tag","column_id":1},{"column_schema":{"name":"b","data_type":{"Float64":{}},"is_nullable":false,"is_time_index":false,"default_constraint":null,"metadata":{}},"semantic_type":"Field","column_id":2},{"column_schema":{"name":"c","data_type":{"Timestamp":{"Millisecond":null}},"is_nullable":false,"is_time_index":false,"default_constraint":null,"metadata":{}},"semantic_type":"Timestamp","column_id":3}
880                ],
881                "primary_key":[1],
882                "region_id":5299989648942,
883                "schema_version":0
884            }
885            }"#;
886        let _ = serde_json::from_str::<RegionChange>(region_change).unwrap();
887
888        let region_remove = r#"{"region_id":42}"#;
889        let _ = serde_json::from_str::<RegionRemove>(region_remove).unwrap();
890
891        let region_partition_expr_change = r#"{
892            "partition_expr": "{\"expr\":\"x < 100\"}"
893        }"#;
894        let _ = serde_json::from_str::<RegionPartitionExprChange>(region_partition_expr_change)
895            .unwrap();
896    }
897
898    #[test]
899    fn test_region_manifest_compatibility() {
900        // Test deserializing RegionManifest from old schema where FileId is a UUID string
901        let region_manifest_json = r#"{
902            "metadata": {
903                "column_metadatas": [
904                    {
905                        "column_schema": {
906                            "name": "a",
907                            "data_type": {"Int64": {}},
908                            "is_nullable": false,
909                            "is_time_index": false,
910                            "default_constraint": null,
911                            "metadata": {}
912                        },
913                        "semantic_type": "Tag",
914                        "column_id": 1
915                    },
916                    {
917                        "column_schema": {
918                            "name": "b",
919                            "data_type": {"Float64": {}},
920                            "is_nullable": false,
921                            "is_time_index": false,
922                            "default_constraint": null,
923                            "metadata": {}
924                        },
925                        "semantic_type": "Field",
926                        "column_id": 2
927                    },
928                    {
929                        "column_schema": {
930                            "name": "c",
931                            "data_type": {"Timestamp": {"Millisecond": null}},
932                            "is_nullable": false,
933                            "is_time_index": false,
934                            "default_constraint": null,
935                            "metadata": {}
936                        },
937                        "semantic_type": "Timestamp",
938                        "column_id": 3
939                    }
940                ],
941                "primary_key": [1],
942                "region_id": 4402341478400,
943                "schema_version": 0
944            },
945            "files": {
946                "4b220a70-2b03-4641-9687-b65d94641208": {
947                    "region_id": 4402341478400,
948                    "file_id": "4b220a70-2b03-4641-9687-b65d94641208",
949                    "time_range": [
950                        {"value": 1451609210000, "unit": "Millisecond"},
951                        {"value": 1451609520000, "unit": "Millisecond"}
952                    ],
953                    "level": 1,
954                    "file_size": 100
955                },
956                "34b6ebb9-b8a5-4a4b-b744-56f67defad02": {
957                    "region_id": 4402341478400,
958                    "file_id": "34b6ebb9-b8a5-4a4b-b744-56f67defad02",
959                    "time_range": [
960                        {"value": 1451609210000, "unit": "Millisecond"},
961                        {"value": 1451609520000, "unit": "Millisecond"}
962                    ],
963                    "level": 0,
964                    "file_size": 100
965                }
966            },
967            "flushed_entry_id": 10,
968            "flushed_sequence": 20,
969            "manifest_version": 1,
970            "truncated_entry_id": null,
971            "compaction_time_window": null
972        }"#;
973
974        let manifest = serde_json::from_str::<RegionManifest>(region_manifest_json).unwrap();
975
976        // Verify that the files were correctly deserialized
977        assert_eq!(manifest.files.len(), 2);
978        assert_eq!(manifest.flushed_entry_id, 10);
979        assert_eq!(manifest.flushed_sequence, 20);
980        assert_eq!(manifest.manifest_version, 1);
981
982        // Verify that FileIds were correctly parsed from UUID strings
983        let mut file_ids: Vec<String> = manifest.files.keys().map(|id| id.to_string()).collect();
984        file_ids.sort_unstable();
985        assert_eq!(
986            file_ids,
987            vec![
988                "34b6ebb9-b8a5-4a4b-b744-56f67defad02",
989                "4b220a70-2b03-4641-9687-b65d94641208",
990            ]
991        );
992
993        // Roundtrip test with current FileId format
994        let serialized_manifest = serde_json::to_string(&manifest).unwrap();
995        let deserialized_manifest: RegionManifest =
996            serde_json::from_str(&serialized_manifest).unwrap();
997        assert_eq!(manifest, deserialized_manifest);
998        assert_ne!(serialized_manifest, region_manifest_json);
999    }
1000
1001    #[test]
1002    fn test_region_truncate_compat() {
1003        // Test deserializing RegionTruncate from old schema
1004        let region_truncate_json = r#"{
1005            "region_id": 4402341478400,
1006            "truncated_entry_id": 10,
1007            "truncated_sequence": 20
1008        }"#;
1009
1010        let truncate_v1: RegionTruncate = serde_json::from_str(region_truncate_json).unwrap();
1011        assert_eq!(truncate_v1.region_id, 4402341478400);
1012        assert_eq!(
1013            truncate_v1.kind,
1014            TruncateKind::All {
1015                truncated_entry_id: 10,
1016                truncated_sequence: 20,
1017            }
1018        );
1019
1020        // Test deserializing RegionTruncate from new schema
1021        let region_truncate_v2_json = r#"{
1022    "region_id": 4402341478400,
1023    "files_to_remove": [
1024        {
1025            "region_id": 4402341478400,
1026            "file_id": "4b220a70-2b03-4641-9687-b65d94641208",
1027            "time_range": [
1028                {
1029                    "value": 1451609210000,
1030                    "unit": "Millisecond"
1031                },
1032                {
1033                    "value": 1451609520000,
1034                    "unit": "Millisecond"
1035                }
1036            ],
1037            "level": 1,
1038            "file_size": 100
1039        }
1040    ]
1041}"#;
1042
1043        let truncate_v2: RegionTruncate = serde_json::from_str(region_truncate_v2_json).unwrap();
1044        assert_eq!(truncate_v2.region_id, 4402341478400);
1045        assert_eq!(
1046            truncate_v2.kind,
1047            TruncateKind::Partial {
1048                files_to_remove: vec![FileMeta {
1049                    region_id: RegionId::from_u64(4402341478400),
1050                    file_id: FileId::parse_str("4b220a70-2b03-4641-9687-b65d94641208").unwrap(),
1051                    time_range: (
1052                        Timestamp::new_millisecond(1451609210000),
1053                        Timestamp::new_millisecond(1451609520000)
1054                    ),
1055                    level: 1,
1056                    file_size: 100,
1057                    ..Default::default()
1058                }]
1059            }
1060        );
1061    }
1062
1063    #[test]
1064    fn test_region_manifest_removed_files() {
1065        let region_metadata = r#"{
1066                "column_metadatas": [
1067                    {
1068                        "column_schema": {
1069                            "name": "a",
1070                            "data_type": {"Int64": {}},
1071                            "is_nullable": false,
1072                            "is_time_index": false,
1073                            "default_constraint": null,
1074                            "metadata": {}
1075                        },
1076                        "semantic_type": "Tag",
1077                        "column_id": 1
1078                    },
1079                    {
1080                        "column_schema": {
1081                            "name": "b",
1082                            "data_type": {"Float64": {}},
1083                            "is_nullable": false,
1084                            "is_time_index": false,
1085                            "default_constraint": null,
1086                            "metadata": {}
1087                        },
1088                        "semantic_type": "Field",
1089                        "column_id": 2
1090                    },
1091                    {
1092                        "column_schema": {
1093                            "name": "c",
1094                            "data_type": {"Timestamp": {"Millisecond": null}},
1095                            "is_nullable": false,
1096                            "is_time_index": false,
1097                            "default_constraint": null,
1098                            "metadata": {}
1099                        },
1100                        "semantic_type": "Timestamp",
1101                        "column_id": 3
1102                    }
1103                ],
1104                "primary_key": [1],
1105                "region_id": 4402341478400,
1106                "schema_version": 0
1107            }"#;
1108
1109        let metadata: RegionMetadataRef =
1110            serde_json::from_str(region_metadata).expect("Failed to parse region metadata");
1111        let manifest = RegionManifest {
1112            metadata: metadata.clone(),
1113            files: HashMap::new(),
1114            flushed_entry_id: 0,
1115            flushed_sequence: 0,
1116            committed_sequence: None,
1117            manifest_version: 0,
1118            truncated_entry_id: None,
1119            compaction_time_window: None,
1120            removed_files: RemovedFilesRecord {
1121                removed_files: vec![RemovedFiles {
1122                    removed_at: 0,
1123                    files: HashSet::from([RemovedFile::File(
1124                        FileId::parse_str("4b220a70-2b03-4641-9687-b65d94641208").unwrap(),
1125                        None,
1126                    )]),
1127                }],
1128            },
1129            sst_format: FormatType::PrimaryKey,
1130            append_mode: None,
1131            pending_tails: Default::default(),
1132            handoff_floors: Default::default(),
1133        };
1134
1135        let json = serde_json::to_string(&manifest).unwrap();
1136        let new: RegionManifest = serde_json::from_str(&json).unwrap();
1137
1138        assert_eq!(manifest, new);
1139    }
1140
1141    #[test]
1142    fn test_remove_tracks_current_manifest_index_version() {
1143        let file_id = FileId::random();
1144        let current = FileMeta {
1145            region_id: RegionId::new(1, 1),
1146            file_id,
1147            available_indexes: smallvec::smallvec![crate::sst::file::IndexType::InvertedIndex],
1148            index_file_size: 1024,
1149            index_version: 2,
1150            ..Default::default()
1151        };
1152        let mut stale = current.clone();
1153        stale.available_indexes.clear();
1154        stale.index_file_size = 0;
1155        stale.index_version = 0;
1156
1157        let mut builder = RegionManifestBuilder::default();
1158        builder.apply_edit(
1159            1,
1160            RegionEdit {
1161                files_to_add: vec![current.clone()],
1162                files_to_remove: Vec::new(),
1163                timestamp_ms: None,
1164                compaction_time_window: None,
1165                flushed_entry_id: None,
1166                flushed_sequence: None,
1167                committed_sequence: None,
1168                wal_recovery: Default::default(),
1169            },
1170        );
1171        builder.apply_edit(
1172            2,
1173            RegionEdit {
1174                files_to_add: Vec::new(),
1175                files_to_remove: vec![stale.clone()],
1176                timestamp_ms: Some(42),
1177                compaction_time_window: None,
1178                flushed_entry_id: None,
1179                flushed_sequence: None,
1180                committed_sequence: None,
1181                wal_recovery: Default::default(),
1182            },
1183        );
1184
1185        assert_eq!(
1186            builder.removed_files.removed_files[0].files,
1187            HashSet::from([RemovedFile::File(file_id, Some(2))])
1188        );
1189
1190        let mut builder = RegionManifestBuilder::default();
1191        builder.apply_edit(
1192            1,
1193            RegionEdit {
1194                files_to_add: vec![current],
1195                files_to_remove: Vec::new(),
1196                timestamp_ms: None,
1197                compaction_time_window: None,
1198                flushed_entry_id: None,
1199                flushed_sequence: None,
1200                committed_sequence: None,
1201                wal_recovery: Default::default(),
1202            },
1203        );
1204        builder.apply_truncate(
1205            2,
1206            RegionTruncate {
1207                region_id: RegionId::new(1, 1),
1208                kind: TruncateKind::Partial {
1209                    files_to_remove: vec![stale],
1210                },
1211                timestamp_ms: Some(42),
1212            },
1213        );
1214
1215        assert_eq!(
1216            builder.removed_files.removed_files[0].files,
1217            HashSet::from([RemovedFile::File(file_id, Some(2))])
1218        );
1219    }
1220
1221    /// Test if old version can still be deserialized then serialized to the new version.
1222    #[test]
1223    fn test_old_region_manifest_compat() {
1224        #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
1225        pub struct RegionManifestV1 {
1226            /// Metadata of the region.
1227            pub metadata: RegionMetadataRef,
1228            /// SST files.
1229            pub files: HashMap<FileId, FileMeta>,
1230            /// Last WAL entry id of flushed data.
1231            pub flushed_entry_id: EntryId,
1232            /// Last sequence of flushed data.
1233            pub flushed_sequence: SequenceNumber,
1234            /// Current manifest version.
1235            pub manifest_version: ManifestVersion,
1236            /// Last WAL entry id of truncated data.
1237            pub truncated_entry_id: Option<EntryId>,
1238            /// Inferred compaction time window.
1239            #[serde(with = "humantime_serde")]
1240            pub compaction_time_window: Option<Duration>,
1241        }
1242
1243        let region_metadata = r#"{
1244                "column_metadatas": [
1245                    {
1246                        "column_schema": {
1247                            "name": "a",
1248                            "data_type": {"Int64": {}},
1249                            "is_nullable": false,
1250                            "is_time_index": false,
1251                            "default_constraint": null,
1252                            "metadata": {}
1253                        },
1254                        "semantic_type": "Tag",
1255                        "column_id": 1
1256                    },
1257                    {
1258                        "column_schema": {
1259                            "name": "b",
1260                            "data_type": {"Float64": {}},
1261                            "is_nullable": false,
1262                            "is_time_index": false,
1263                            "default_constraint": null,
1264                            "metadata": {}
1265                        },
1266                        "semantic_type": "Field",
1267                        "column_id": 2
1268                    },
1269                    {
1270                        "column_schema": {
1271                            "name": "c",
1272                            "data_type": {"Timestamp": {"Millisecond": null}},
1273                            "is_nullable": false,
1274                            "is_time_index": false,
1275                            "default_constraint": null,
1276                            "metadata": {}
1277                        },
1278                        "semantic_type": "Timestamp",
1279                        "column_id": 3
1280                    }
1281                ],
1282                "primary_key": [1],
1283                "region_id": 4402341478400,
1284                "schema_version": 0
1285            }"#;
1286
1287        let metadata: RegionMetadataRef =
1288            serde_json::from_str(region_metadata).expect("Failed to parse region metadata");
1289
1290        // first test v1 empty to new
1291        let v1 = RegionManifestV1 {
1292            metadata: metadata.clone(),
1293            files: HashMap::new(),
1294            flushed_entry_id: 0,
1295            flushed_sequence: 0,
1296            manifest_version: 0,
1297            truncated_entry_id: None,
1298            compaction_time_window: None,
1299        };
1300        let json = serde_json::to_string(&v1).unwrap();
1301        let new_from_old: RegionManifest = serde_json::from_str(&json).unwrap();
1302        assert_eq!(
1303            new_from_old,
1304            RegionManifest {
1305                metadata: metadata.clone(),
1306                files: HashMap::new(),
1307                removed_files: Default::default(),
1308                flushed_entry_id: 0,
1309                flushed_sequence: 0,
1310                committed_sequence: None,
1311                manifest_version: 0,
1312                truncated_entry_id: None,
1313                compaction_time_window: None,
1314                sst_format: FormatType::PrimaryKey,
1315                append_mode: None,
1316                pending_tails: Default::default(),
1317                handoff_floors: Default::default(),
1318            }
1319        );
1320
1321        let new_manifest = RegionManifest {
1322            metadata: metadata.clone(),
1323            files: HashMap::new(),
1324            removed_files: Default::default(),
1325            flushed_entry_id: 0,
1326            flushed_sequence: 0,
1327            committed_sequence: None,
1328            manifest_version: 0,
1329            truncated_entry_id: None,
1330            compaction_time_window: None,
1331            sst_format: FormatType::PrimaryKey,
1332            append_mode: None,
1333            pending_tails: Default::default(),
1334            handoff_floors: Default::default(),
1335        };
1336        let json = serde_json::to_string(&new_manifest).unwrap();
1337        let old_from_new: RegionManifestV1 = serde_json::from_str(&json).unwrap();
1338        assert_eq!(
1339            old_from_new,
1340            RegionManifestV1 {
1341                metadata: metadata.clone(),
1342                files: HashMap::new(),
1343                flushed_entry_id: 0,
1344                flushed_sequence: 0,
1345                manifest_version: 0,
1346                truncated_entry_id: None,
1347                compaction_time_window: None,
1348            }
1349        );
1350
1351        #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
1352        pub struct RegionEditV1 {
1353            pub files_to_add: Vec<FileMeta>,
1354            pub files_to_remove: Vec<FileMeta>,
1355            #[serde(with = "humantime_serde")]
1356            pub compaction_time_window: Option<Duration>,
1357            pub flushed_entry_id: Option<EntryId>,
1358            pub flushed_sequence: Option<SequenceNumber>,
1359        }
1360
1361        let json = serde_json::to_string(&RegionEditV1 {
1362            files_to_add: vec![],
1363            files_to_remove: vec![],
1364            compaction_time_window: None,
1365            flushed_entry_id: None,
1366            flushed_sequence: None,
1367        })
1368        .unwrap();
1369        let new_from_old: RegionEdit = serde_json::from_str(&json).unwrap();
1370        assert_eq!(
1371            RegionEdit {
1372                files_to_add: vec![],
1373                files_to_remove: vec![],
1374                timestamp_ms: None,
1375                compaction_time_window: None,
1376                flushed_entry_id: None,
1377                flushed_sequence: None,
1378                committed_sequence: None,
1379                wal_recovery: Default::default(),
1380            },
1381            new_from_old
1382        );
1383
1384        // test new version with timestamp_ms set can deserialize to old version
1385        let new = RegionEdit {
1386            files_to_add: vec![],
1387            files_to_remove: vec![],
1388            timestamp_ms: Some(42),
1389            compaction_time_window: None,
1390            flushed_entry_id: None,
1391            flushed_sequence: None,
1392            committed_sequence: None,
1393            wal_recovery: Default::default(),
1394        };
1395
1396        let new_json = serde_json::to_string(&new).unwrap();
1397
1398        let old_from_new: RegionEditV1 = serde_json::from_str(&new_json).unwrap();
1399        assert_eq!(
1400            RegionEditV1 {
1401                files_to_add: vec![],
1402                files_to_remove: vec![],
1403                compaction_time_window: None,
1404                flushed_entry_id: None,
1405                flushed_sequence: None,
1406            },
1407            old_from_new
1408        );
1409    }
1410
1411    #[test]
1412    fn test_region_change_backward_compatibility() {
1413        // Test that we can deserialize a RegionChange without sst_format
1414        let region_change_json = r#"{
1415            "metadata": {
1416                "column_metadatas": [
1417                    {"column_schema":{"name":"a","data_type":{"Int64":{}},"is_nullable":false,"is_time_index":false,"default_constraint":null,"metadata":{}},"semantic_type":"Tag","column_id":1},
1418                    {"column_schema":{"name":"b","data_type":{"Int64":{}},"is_nullable":false,"is_time_index":false,"default_constraint":null,"metadata":{}},"semantic_type":"Field","column_id":2},
1419                    {"column_schema":{"name":"c","data_type":{"Timestamp":{"Millisecond":null}},"is_nullable":false,"is_time_index":false,"default_constraint":null,"metadata":{}},"semantic_type":"Timestamp","column_id":3}
1420                ],
1421                "primary_key": [
1422                    1
1423                ],
1424                "region_id": 42,
1425                "schema_version": 0
1426            }
1427        }"#;
1428
1429        let region_change: RegionChange = serde_json::from_str(region_change_json).unwrap();
1430        assert_eq!(region_change.sst_format, FormatType::PrimaryKey);
1431
1432        // Test serialization and deserialization with sst_format
1433        let region_change = RegionChange {
1434            metadata: region_change.metadata.clone(),
1435            sst_format: FormatType::Flat,
1436            append_mode: None,
1437        };
1438
1439        let serialized = serde_json::to_string(&region_change).unwrap();
1440        let deserialized: RegionChange = serde_json::from_str(&serialized).unwrap();
1441        assert_eq!(deserialized.sst_format, FormatType::Flat);
1442    }
1443
1444    #[test]
1445    fn test_removed_file_compatibility() {
1446        let file_id = FileId::random();
1447        // Case 1: Deserialize from FileId string (Legacy format)
1448        let json_str = format!("\"{}\"", file_id);
1449        let removed_file: RemovedFile = serde_json::from_str(&json_str).unwrap();
1450        assert_eq!(removed_file, RemovedFile::File(file_id, None));
1451
1452        // Case 2: Deserialize from new format (File)
1453        let removed_file_v2 = RemovedFile::File(file_id, Some(10));
1454        let json_v2 = serde_json::to_string(&removed_file_v2).unwrap();
1455        let deserialized_v2: RemovedFile = serde_json::from_str(&json_v2).unwrap();
1456        assert_eq!(removed_file_v2, deserialized_v2);
1457
1458        // Case 3: Deserialize from new format (Index)
1459        let removed_index = RemovedFile::Index(file_id, 20);
1460        let json_index = serde_json::to_string(&removed_index).unwrap();
1461        let deserialized_index: RemovedFile = serde_json::from_str(&json_index).unwrap();
1462        assert_eq!(removed_index, deserialized_index);
1463
1464        // Case 4: Round-trip serialization/deserialization of new enum format with None as index version
1465        let removed_file = RemovedFile::File(file_id, None);
1466        let json = serde_json::to_string(&removed_file).unwrap();
1467        let deserialized: RemovedFile = serde_json::from_str(&json).unwrap();
1468        assert_eq!(removed_file, deserialized);
1469
1470        // Case 5: Deserialize mixed set in RemovedFilesRecord
1471        // This simulates a Set<RemovedFile> which might contain old strings or new objects if manually constructed or from old versions.
1472        // Actually, if it was HashSet<FileId>, the JSON is ["id1", "id2"].
1473        // If it is HashSet<RemovedFile>, the JSON is [{"File":...}, "id2"] if mixed (which shouldn't happen usually but good to test).
1474
1475        let json_set = format!("[\"{}\"]", file_id);
1476        let removed_files_set: HashSet<RemovedFile> = serde_json::from_str(&json_set).unwrap();
1477        assert!(removed_files_set.contains(&RemovedFile::File(file_id, None)));
1478    }
1479
1480    /// It is intentionally acceptable to ignore the legacy `file_ids` field when
1481    /// deserializing [`RemovedFiles`].
1482    ///
1483    /// In older manifests, `file_ids` recorded the set of SSTable files that were
1484    /// candidates for garbage collection at a given `removed_at` timestamp. The
1485    /// newer format stores this information in the `files` field instead. When we
1486    /// deserialize an old manifest entry into the new struct, we *drop* the
1487    /// `file_ids` field instead of trying to recover or merge it.
1488    ///
1489    /// Dropping `file_ids` does **not** risk deleting live data: a file is only
1490    /// physically removed when it is both (a) no longer referenced by any region
1491    /// metadata and (b) selected by the GC worker as safe to delete. Losing the
1492    /// historical list of candidate `file_ids` merely means some obsolete files
1493    /// may stay on disk longer than strictly necessary.
1494    ///
1495    /// The GC worker periodically scans storage (e.g. by walking the data
1496    /// directories and/or consulting the latest manifest) to discover files that
1497    /// are no longer referenced anywhere. Any files that were only referenced via
1498    /// the dropped `file_ids` field will be rediscovered during these scans and
1499    /// eventually deleted. Thus the system converges to a correct, fully-collected
1500    /// state without relying on `file_ids`, and the only potential impact of
1501    /// ignoring it is temporary disk space overhead, not data loss.
1502    #[test]
1503    fn test_removed_files_backward_compatibility() {
1504        // Define the old version struct with file_ids field
1505        #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
1506        struct OldRemovedFiles {
1507            pub removed_at: i64,
1508            pub file_ids: HashSet<FileId>,
1509        }
1510
1511        // Create an old version instance
1512        let mut file_ids = HashSet::new();
1513        file_ids.insert(FileId::random());
1514        file_ids.insert(FileId::random());
1515
1516        let old_removed_files = OldRemovedFiles {
1517            removed_at: 1234567890,
1518            file_ids,
1519        };
1520
1521        // Serialize the old version
1522        let old_json = serde_json::to_string(&old_removed_files).unwrap();
1523
1524        // Try to deserialize into new version - file_ids should be ignored
1525        let result: Result<RemovedFiles, _> = serde_json::from_str(&old_json);
1526
1527        // This should succeed and create a default RemovedFiles (empty files set)
1528        assert!(result.is_ok(), "{:?}", result);
1529        let removed_files = result.unwrap();
1530        assert_eq!(removed_files.removed_at, 1234567890);
1531        assert!(removed_files.files.is_empty());
1532
1533        // Test that new format still works
1534        let file_id = FileId::random();
1535        let new_json = format!(
1536            r#"{{
1537            "removed_at": 1234567890,
1538            "files": ["{}"]
1539        }}"#,
1540            file_id
1541        );
1542
1543        let result: Result<RemovedFiles, _> = serde_json::from_str(&new_json);
1544        assert!(result.is_ok());
1545        let removed_files = result.unwrap();
1546        assert_eq!(removed_files.removed_at, 1234567890);
1547        assert_eq!(removed_files.files.len(), 1);
1548        assert!(
1549            removed_files
1550                .files
1551                .contains(&RemovedFile::File(file_id, None))
1552        );
1553    }
1554
1555    fn tail(node_id: u64, after: EntryId, upto: EntryId) -> PendingTail {
1556        PendingTail {
1557            node_id,
1558            after,
1559            upto,
1560            upto_kind: UptoKind::Reported,
1561        }
1562    }
1563
1564    fn wal_recovery_edit(
1565        flushed_entry_id: Option<EntryId>,
1566        wal_recovery: WalRecoveryEdit,
1567    ) -> RegionEdit {
1568        RegionEdit {
1569            files_to_add: vec![],
1570            files_to_remove: vec![],
1571            timestamp_ms: None,
1572            compaction_time_window: None,
1573            flushed_entry_id,
1574            flushed_sequence: None,
1575            committed_sequence: None,
1576            wal_recovery,
1577        }
1578    }
1579
1580    fn manifest_builder() -> RegionManifestBuilder {
1581        let mut builder = RegionManifestBuilder::default();
1582        builder.apply_change(
1583            0,
1584            RegionChange {
1585                metadata: Arc::new(crate::manifest::tests::utils::basic_region_metadata()),
1586                sst_format: FormatType::PrimaryKey,
1587                append_mode: None,
1588            },
1589        );
1590        builder
1591    }
1592
1593    #[test]
1594    fn test_checkpoint_sharing_preserves_owned_allocations() {
1595        let mut checkpoint = manifest_builder().try_build().unwrap();
1596        let expr = partition::expr::col("tag").lt(datatypes::value::Value::UInt32(10));
1597        let mut allocations = HashMap::new();
1598        for _ in 0..4 {
1599            let meta = FileMetaRef::from(FileMeta {
1600                region_id: RegionId::new(1, 1),
1601                file_id: FileId::random(),
1602                indexes: vec![crate::sst::file::ColumnIndexMetadata::default()],
1603                partition_expr: Some(expr.clone().into()),
1604                ..Default::default()
1605            });
1606            allocations.insert(
1607                meta.file_id,
1608                (std::ptr::from_ref(meta.as_ref()), meta.indexes.as_ptr()),
1609            );
1610            checkpoint.files.insert(meta.file_id, meta);
1611        }
1612        let interner = Arc::new(crate::sst::file_metadata::FileMetadataInterner::default());
1613        let manifest = RegionManifestBuilder::with_checkpoint(Some(checkpoint), interner.clone())
1614            .try_build()
1615            .unwrap();
1616        let expression = manifest
1617            .files
1618            .values()
1619            .next()
1620            .unwrap()
1621            .partition_expr
1622            .as_ref()
1623            .unwrap();
1624        for meta in manifest.files.values() {
1625            assert_eq!(
1626                allocations[&meta.file_id],
1627                (std::ptr::from_ref(meta.as_ref()), meta.indexes.as_ptr())
1628            );
1629            assert!(MetadataRef::ptr_eq(
1630                expression,
1631                meta.partition_expr.as_ref().unwrap()
1632            ));
1633        }
1634        let decoded = serde_json::from_slice(&serde_json::to_vec(&manifest).unwrap()).unwrap();
1635        let recovered = RegionManifestBuilder::with_checkpoint(Some(decoded), interner)
1636            .try_build()
1637            .unwrap();
1638        for (id, meta) in &manifest.files {
1639            assert!(MetadataRef::ptr_eq(meta, &recovered.files[id]));
1640        }
1641    }
1642
1643    #[test]
1644    fn test_pending_tails_change_only_when_set() {
1645        let mut builder = manifest_builder();
1646        builder.apply_edit(
1647            1,
1648            wal_recovery_edit(
1649                None,
1650                WalRecoveryEdit {
1651                    pending_tails: Some(vec![tail(1, 10, 20)]),
1652                    handoff_floor: None,
1653                },
1654            ),
1655        );
1656        builder.apply_edit(2, wal_recovery_edit(Some(5), WalRecoveryEdit::default()));
1657        assert_eq!(builder.pending_tails, vec![tail(1, 10, 20)]);
1658
1659        builder.apply_edit(
1660            3,
1661            wal_recovery_edit(
1662                None,
1663                WalRecoveryEdit {
1664                    pending_tails: Some(vec![]),
1665                    handoff_floor: None,
1666                },
1667            ),
1668        );
1669        assert!(builder.pending_tails.is_empty());
1670    }
1671
1672    #[test]
1673    fn test_flushed_entry_id_drops_reached_floors_and_keeps_tails() {
1674        let mut builder = manifest_builder();
1675        for (node_id, entry_id) in [(1, 20), (2, 30)] {
1676            builder.apply_edit(
1677                1,
1678                wal_recovery_edit(
1679                    None,
1680                    WalRecoveryEdit {
1681                        pending_tails: Some(vec![tail(1, 10, 20)]),
1682                        handoff_floor: Some(HandoffFloor { node_id, entry_id }),
1683                    },
1684                ),
1685            );
1686        }
1687        builder.apply_edit(2, wal_recovery_edit(Some(20), WalRecoveryEdit::default()));
1688
1689        let manifest = builder.try_build().unwrap();
1690        assert_eq!(manifest.pending_tails, vec![tail(1, 10, 20)]);
1691        assert_eq!(manifest.handoff_floors, BTreeMap::from([(2, 30)]));
1692    }
1693
1694    #[test]
1695    fn test_truncate_drops_reached_floors() {
1696        let mut builder = manifest_builder();
1697        builder.apply_edit(
1698            1,
1699            wal_recovery_edit(
1700                None,
1701                WalRecoveryEdit {
1702                    pending_tails: None,
1703                    handoff_floor: Some(HandoffFloor {
1704                        node_id: 1,
1705                        entry_id: 20,
1706                    }),
1707                },
1708            ),
1709        );
1710        builder.apply_truncate(
1711            2,
1712            RegionTruncate {
1713                region_id: RegionId::new(1, 1),
1714                kind: TruncateKind::All {
1715                    truncated_entry_id: 20,
1716                    truncated_sequence: 20,
1717                },
1718                timestamp_ms: None,
1719            },
1720        );
1721
1722        assert!(builder.try_build().unwrap().handoff_floors.is_empty());
1723    }
1724
1725    #[test]
1726    fn test_wal_replay_start() {
1727        let mut builder = manifest_builder();
1728        // Node 1 was handed off at 40 after its first tail, so its second tail starts
1729        // there and entries in (20, 40] belong to no tail.
1730        builder.apply_edit(
1731            1,
1732            wal_recovery_edit(
1733                Some(10),
1734                WalRecoveryEdit {
1735                    pending_tails: Some(vec![tail(1, 10, 20), tail(2, 30, 60), tail(1, 40, 50)]),
1736                    handoff_floor: Some(HandoffFloor {
1737                        node_id: 1,
1738                        entry_id: 40,
1739                    }),
1740                },
1741            ),
1742        );
1743        builder.apply_edit(
1744            2,
1745            wal_recovery_edit(
1746                None,
1747                WalRecoveryEdit {
1748                    pending_tails: None,
1749                    handoff_floor: Some(HandoffFloor {
1750                        node_id: 3,
1751                        entry_id: 40,
1752                    }),
1753                },
1754            ),
1755        );
1756        let manifest = builder.try_build().unwrap();
1757
1758        assert_eq!(manifest.wal_replay_start(Some(1)), 50);
1759        assert_eq!(manifest.wal_replay_start(Some(2)), 60);
1760        assert_eq!(manifest.wal_replay_start(Some(3)), 40);
1761        assert_eq!(manifest.wal_replay_start(Some(4)), 10);
1762        assert_eq!(manifest.wal_replay_start(None), 10);
1763    }
1764
1765    #[test]
1766    fn test_cap_wal_release() {
1767        let tails = [tail(1, 20, 25), tail(2, 5, 60), tail(1, 30, 40)];
1768
1769        assert_eq!(cap_wal_release(&tails, Some(1), 50), 20);
1770        assert_eq!(cap_wal_release(&tails, Some(1), 10), 10);
1771        assert_eq!(cap_wal_release(&tails, Some(3), 50), 50);
1772        assert_eq!(cap_wal_release(&tails, None, 50), 50);
1773    }
1774
1775    const PENDING_TAILS_DELTA: &str =
1776        include_str!("../../../../tests/compatibility/manifest/pending_tails/delta.json");
1777    const PENDING_TAILS_CHECKPOINT: &str =
1778        include_str!("../../../../tests/compatibility/manifest/pending_tails/checkpoint.json");
1779
1780    fn fixture_pending_tails() -> Vec<PendingTail> {
1781        vec![
1782            tail(1, 1 << 32, (1 << 32) + 5),
1783            PendingTail {
1784                upto_kind: UptoKind::Listed,
1785                ..tail(2, 2 << 32, 3 << 32)
1786            },
1787        ]
1788    }
1789
1790    #[test]
1791    fn test_pending_tails_fixtures() {
1792        let delta = PENDING_TAILS_DELTA.trim_end();
1793        let actions = RegionMetaActionList::decode(delta.as_bytes()).unwrap();
1794        let [RegionMetaAction::Edit(edit)] = actions.actions.as_slice() else {
1795            panic!("unexpected actions: {actions:?}");
1796        };
1797        let expected = WalRecoveryEdit {
1798            pending_tails: Some(fixture_pending_tails()),
1799            handoff_floor: Some(HandoffFloor {
1800                node_id: 3,
1801                entry_id: 4 << 32,
1802            }),
1803        };
1804        assert_eq!(edit.wal_recovery, expected);
1805        assert_eq!(actions.encode().unwrap(), delta.as_bytes());
1806
1807        let checkpoint = PENDING_TAILS_CHECKPOINT.trim_end();
1808        let decoded = RegionCheckpoint::decode(checkpoint.as_bytes()).unwrap();
1809        let manifest = decoded.checkpoint.as_ref().unwrap();
1810        assert_eq!(manifest.pending_tails, fixture_pending_tails());
1811        assert_eq!(manifest.handoff_floors, BTreeMap::from([(3, 4 << 32)]));
1812        let encoded: serde_json::Value =
1813            serde_json::from_slice(&decoded.encode().unwrap()).unwrap();
1814        let frozen: serde_json::Value = serde_json::from_str(checkpoint).unwrap();
1815        for key in ["pending_tails", "handoff_floors"] {
1816            assert_eq!(encoded["checkpoint"][key], frozen["checkpoint"][key]);
1817        }
1818    }
1819
1820    /// Checks the fixtures against the shapes before pending tails were added.
1821    #[test]
1822    fn test_pending_tails_old_shape_compat() {
1823        #[derive(Serialize, Deserialize)]
1824        struct OldRegionEdit {
1825            files_to_add: Vec<FileMeta>,
1826            files_to_remove: Vec<FileMeta>,
1827            #[serde(default)]
1828            timestamp_ms: Option<i64>,
1829            #[serde(with = "humantime_serde")]
1830            compaction_time_window: Option<Duration>,
1831            flushed_entry_id: Option<EntryId>,
1832            flushed_sequence: Option<SequenceNumber>,
1833            committed_sequence: Option<SequenceNumber>,
1834        }
1835
1836        #[derive(Serialize, Deserialize)]
1837        struct OldRegionManifest {
1838            metadata: RegionMetadataRef,
1839            files: HashMap<FileId, FileMeta>,
1840            #[serde(default)]
1841            removed_files: RemovedFilesRecord,
1842            flushed_entry_id: EntryId,
1843            flushed_sequence: SequenceNumber,
1844            committed_sequence: Option<SequenceNumber>,
1845            manifest_version: ManifestVersion,
1846            truncated_entry_id: Option<EntryId>,
1847            #[serde(with = "humantime_serde")]
1848            compaction_time_window: Option<Duration>,
1849            #[serde(default)]
1850            sst_format: FormatType,
1851            #[serde(default)]
1852            append_mode: Option<bool>,
1853        }
1854
1855        let delta: serde_json::Value = serde_json::from_str(PENDING_TAILS_DELTA).unwrap();
1856        let old_edit: OldRegionEdit =
1857            serde_json::from_value(delta["actions"][0]["Edit"].clone()).unwrap();
1858        assert_eq!(old_edit.committed_sequence, Some((1 << 40) + 5));
1859        let old_json = serde_json::to_string(&old_edit).unwrap();
1860        let edit: RegionEdit = serde_json::from_str(&old_json).unwrap();
1861        assert_eq!(edit.wal_recovery, WalRecoveryEdit::default());
1862        assert_eq!(serde_json::to_string(&edit).unwrap(), old_json);
1863
1864        let checkpoint: serde_json::Value = serde_json::from_str(PENDING_TAILS_CHECKPOINT).unwrap();
1865        let old_manifest: OldRegionManifest =
1866            serde_json::from_value(checkpoint["checkpoint"].clone()).unwrap();
1867        assert_eq!(old_manifest.flushed_entry_id, 1 << 32);
1868        let old_json = serde_json::to_string(&old_manifest).unwrap();
1869        let manifest: RegionManifest = serde_json::from_str(&old_json).unwrap();
1870        assert!(manifest.pending_tails.is_empty() && manifest.handoff_floors.is_empty());
1871        assert_eq!(serde_json::to_string(&manifest).unwrap(), old_json);
1872    }
1873}