1use 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#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, Display)]
39pub enum RegionMetaAction {
40 Change(RegionChange),
42 PartitionExprChange(RegionPartitionExprChange),
44 Edit(RegionEdit),
46 Remove(RegionRemove),
48 Truncate(RegionTruncate),
50}
51
52#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
53pub struct RegionPartitionExprChange {
54 pub partition_expr: Option<String>,
56}
57
58impl RegionMetaAction {
59 pub fn is_change(&self) -> bool {
61 matches!(self, RegionMetaAction::Change(_))
62 }
63
64 pub fn is_edit(&self) -> bool {
66 matches!(self, RegionMetaAction::Edit(_))
67 }
68
69 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 pub metadata: RegionMetadataRef,
79 #[serde(default)]
81 pub sst_format: FormatType,
82 #[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 #[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#[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq, Eq)]
105pub struct WalRecoveryEdit {
106 #[serde(default, skip_serializing_if = "Option::is_none")]
109 pub pending_tails: Option<Vec<PendingTail>>,
110 #[serde(default, skip_serializing_if = "Option::is_none")]
112 pub handoff_floor: Option<HandoffFloor>,
113}
114
115#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
117pub struct PendingTail {
118 pub node_id: u64,
120 pub after: EntryId,
121 pub upto: EntryId,
122 pub upto_kind: UptoKind,
123}
124
125#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq)]
127#[serde(rename_all = "snake_case")]
128pub enum UptoKind {
129 Reported,
131 Listed,
133}
134
135#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq)]
138pub struct HandoffFloor {
139 pub node_id: u64,
140 pub entry_id: EntryId,
141}
142
143pub(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 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#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
190pub struct RegionTruncate {
191 pub region_id: RegionId,
192 #[serde(flatten)]
193 pub kind: TruncateKind,
194 #[serde(default)]
196 pub timestamp_ms: Option<i64>,
197}
198
199#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
201#[serde(untagged)]
202pub enum TruncateKind {
203 All {
205 truncated_entry_id: EntryId,
207 truncated_sequence: SequenceNumber,
209 },
210 Partial { files_to_remove: Vec<FileMeta> },
212}
213
214#[derive(Serialize, Deserialize, Clone, Debug)]
216#[cfg_attr(test, derive(Eq))]
217pub struct RegionManifest {
218 pub metadata: RegionMetadataRef,
220 pub files: HashMap<FileId, FileMetaRef>,
222 #[serde(default)]
232 pub removed_files: RemovedFilesRecord,
233 pub flushed_entry_id: EntryId,
235 pub flushed_sequence: SequenceNumber,
237 pub committed_sequence: Option<SequenceNumber>,
238 pub manifest_version: ManifestVersion,
240 pub truncated_entry_id: Option<EntryId>,
242 #[serde(with = "humantime_serde")]
244 pub compaction_time_window: Option<Duration>,
245 #[serde(default)]
247 pub sst_format: FormatType,
248 #[serde(default)]
250 pub append_mode: Option<bool>,
251 #[serde(default, skip_serializing_if = "Vec::is_empty")]
260 pub pending_tails: Vec<PendingTail>,
261 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
263 pub handoff_floors: BTreeMap<u64, EntryId>,
264}
265
266impl RegionManifest {
267 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 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 pub(crate) fn new(file_metadata_interner: FileMetadataInternerRef) -> Self {
331 Self {
332 file_metadata_interner,
333 ..Default::default()
334 }
335 }
336
337 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 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 self.append_mode = change.append_mode.or(self.append_mode);
391 }
392
393 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 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 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 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 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#[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq, Eq)]
547pub struct RemovedFilesRecord {
548 pub removed_files: Vec<RemovedFiles>,
551}
552
553impl RemovedFilesRecord {
554 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 pub removed_at: i64,
583 #[serde(default)]
585 pub files: HashSet<RemovedFile>,
586}
587
588#[derive(Serialize, Hash, Clone, Debug, PartialEq, Eq)]
590pub enum RemovedFile {
591 File(FileId, Option<IndexVersion>),
592 Index(FileId, IndexVersion),
593}
594
595impl<'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 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 self.removed_files.clear();
662 return Ok(());
663 }
664
665 Ok(())
668 }
669}
670
671#[derive(Serialize, Deserialize, Debug, Clone)]
673#[cfg_attr(test, derive(PartialEq, Eq))]
674pub struct RegionCheckpoint {
675 pub last_version: ManifestVersion,
677 pub compacted_actions: usize,
679 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 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 edit.files_to_add.extend(region_edit.files_to_add);
748 edit.files_to_remove.extend(region_edit.files_to_remove);
749 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 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 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 #[test]
804 fn test_sketch_index_manifest_fixture() {
805 use crate::sst::file::IndexType;
806
807 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 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 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 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 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 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 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 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]
1223 fn test_old_region_manifest_compat() {
1224 #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
1225 pub struct RegionManifestV1 {
1226 pub metadata: RegionMetadataRef,
1228 pub files: HashMap<FileId, FileMeta>,
1230 pub flushed_entry_id: EntryId,
1232 pub flushed_sequence: SequenceNumber,
1234 pub manifest_version: ManifestVersion,
1236 pub truncated_entry_id: Option<EntryId>,
1238 #[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 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 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 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 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(®ion_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 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 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 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 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 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 #[test]
1503 fn test_removed_files_backward_compatibility() {
1504 #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
1506 struct OldRemovedFiles {
1507 pub removed_at: i64,
1508 pub file_ids: HashSet<FileId>,
1509 }
1510
1511 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 let old_json = serde_json::to_string(&old_removed_files).unwrap();
1523
1524 let result: Result<RemovedFiles, _> = serde_json::from_str(&old_json);
1526
1527 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 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 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 #[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}