1use std::collections::HashMap;
87use std::ops::Bound;
88use std::sync::Arc;
89
90use anyhow::Context as _;
91use prometheus::IntCounter;
92use prometheus::IntGauge;
93use prometheus::Registry;
94use prometheus::register_int_counter_with_registry;
95use prometheus::register_int_gauge_with_registry;
96use sui_consistent_store::Batch;
97use sui_consistent_store::Db;
98use sui_consistent_store::FrameworkSchema;
99use sui_consistent_store::PipelineTaskKey;
100use sui_indexer_alt_framework::service::Service;
101use sui_types::base_types::ObjectID;
102use sui_types::effects::TransactionEffects;
103use sui_types::effects::TransactionEffectsAPI;
104use sui_types::message_envelope::Message;
105use tokio::time::MissedTickBehavior;
106use tracing::debug;
107use tracing::info;
108use tracing::warn;
109
110use crate::RpcStoreSchema;
111use crate::config::PrunerConfig;
112use crate::indexer::Store;
113use crate::indexer::restore::HISTORY_COHORT;
114use crate::indexer::restore::LIVE_COHORT;
115use crate::schema::checkpoint_seq_by_digest;
116use crate::schema::event_bitmap;
117use crate::schema::object_version_by_checkpoint;
118use crate::schema::objects;
119use crate::schema::primitives::U64Be;
120use crate::schema::pruning_watermark;
121use crate::schema::pruning_watermark::Watermarks;
122use crate::schema::transaction_bitmap;
123use crate::schema::tx_seq_by_digest;
124
125pub struct PrunerMetrics {
127 pub checkpoint_lo: IntGauge,
130 pub tx_seq_lo: IntGauge,
133 pub chunks_committed: IntCounter,
135 pub objects_deleted: IntCounter,
137}
138
139impl PrunerMetrics {
140 pub fn new(prefix: Option<&str>, registry: &Registry) -> Arc<Self> {
141 let prefix = prefix.unwrap_or("rpc_store_pruner");
142 let name = |n| format!("{prefix}_{n}");
143
144 Arc::new(Self {
145 checkpoint_lo: register_int_gauge_with_registry!(
146 name("checkpoint_lo"),
147 "Lowest still-available checkpoint sequence number (pruning floor)",
148 registry,
149 )
150 .unwrap(),
151 tx_seq_lo: register_int_gauge_with_registry!(
152 name("tx_seq_lo"),
153 "Lowest still-available transaction sequence number (pruning floor)",
154 registry,
155 )
156 .unwrap(),
157 chunks_committed: register_int_counter_with_registry!(
158 name("chunks_committed"),
159 "Total pruning chunks committed",
160 registry,
161 )
162 .unwrap(),
163 objects_deleted: register_int_counter_with_registry!(
164 name("objects_deleted"),
165 "Total superseded object versions and tombstones deleted by the pruner",
166 registry,
167 )
168 .unwrap(),
169 })
170 }
171}
172
173#[derive(Default)]
196struct Retractions(HashMap<ObjectID, (u64, bool)>);
197
198impl Retractions {
199 fn record(&mut self, id: ObjectID, cp: u64, removed: bool) {
200 self.0
201 .entry(id)
202 .and_modify(|(recorded_cp, recorded_removed)| {
203 if cp > *recorded_cp {
204 *recorded_cp = cp;
205 *recorded_removed = removed;
206 } else if cp == *recorded_cp {
207 *recorded_removed |= removed;
208 }
209 })
210 .or_insert((cp, removed));
211 }
212
213 fn stage(self, batch: &mut Batch, schema: &RpcStoreSchema) -> anyhow::Result<()> {
214 for (id, (cp, removed)) in self.0 {
215 retract_object_version_by_checkpoint(batch, schema, id, cp, removed)?;
216 }
217 Ok(())
218 }
219}
220
221pub fn start_pruner(
228 store: Store,
229 config: PrunerConfig,
230 metrics: Arc<PrunerMetrics>,
231) -> anyhow::Result<Service> {
232 anyhow::ensure!(
233 config.retention_epochs >= 1,
234 "PrunerConfig::retention_epochs must be >= 1; 0 would prune the current epoch",
235 );
236 anyhow::ensure!(
237 config.max_checkpoints_per_tick >= 1,
238 "PrunerConfig::max_checkpoints_per_tick must be >= 1; 0 would never make progress",
239 );
240
241 let service = Service::new().spawn_aborting(async move {
242 let mut ticker = tokio::time::interval(config.interval());
243 ticker.set_missed_tick_behavior(MissedTickBehavior::Delay);
244
245 loop {
246 ticker.tick().await;
247
248 let store = store.clone();
249 let config = config.clone();
250 let metrics = metrics.clone();
251
252 let res = tokio::task::spawn_blocking(move || {
255 prune_once(store.db(), store.schema(), &config, &metrics)
256 })
257 .await;
258
259 match res {
260 Ok(Ok(())) => {}
261 Ok(Err(e)) => {
262 warn!("rpc-store pruner pass failed (will retry next interval): {e:#}")
263 }
264 Err(e) => warn!("rpc-store pruner task join error: {e}"),
265 }
266 }
267 });
268
269 Ok(service)
270}
271
272fn prune_once(
275 db: &Db,
276 schema: &RpcStoreSchema,
277 config: &PrunerConfig,
278 metrics: &PrunerMetrics,
279) -> anyhow::Result<()> {
280 let Some(current_epoch) = current_committed_epoch(db)? else {
281 debug!("rpc-store pruner: no committed watermark yet; nothing to prune");
282 return Ok(());
283 };
284
285 let Some(retention_lo) =
286 retention_checkpoint_floor(schema, current_epoch, config.retention_epochs)?
287 else {
288 debug!(
289 current_epoch,
290 "rpc-store pruner: retention floor not yet reached; nothing to prune"
291 );
292 return Ok(());
293 };
294
295 let target_lo = clamp_to_snapshot(retention_lo, db.snapshot_range().map(|r| *r.start()));
297
298 let mut cursor = schema.get_pruning_watermarks()?.unwrap_or_default();
299 if target_lo <= cursor.checkpoint_lo {
300 debug!(
301 target_lo,
302 current_lo = cursor.checkpoint_lo,
303 "rpc-store pruner: floor already at or beyond target"
304 );
305 return Ok(());
306 }
307
308 let tick_target = target_lo.min(cursor.checkpoint_lo + config.max_checkpoints_per_tick);
313
314 info!(
315 from = cursor.checkpoint_lo,
316 to = tick_target,
317 target = target_lo,
318 current_epoch,
319 "rpc-store pruner: advancing floor"
320 );
321
322 while cursor.checkpoint_lo < tick_target {
323 let chunk_ckpt_hi = (cursor.checkpoint_lo + config.max_chunk_checkpoints).min(tick_target);
324 cursor = prune_chunk(db, schema, cursor, chunk_ckpt_hi, metrics)?;
325 metrics.checkpoint_lo.set(cursor.checkpoint_lo as i64);
326 metrics.tx_seq_lo.set(cursor.tx_seq_lo as i64);
327 metrics.chunks_committed.inc();
328 }
329
330 if cursor.checkpoint_lo >= target_lo {
337 db.compact_range_cf(transaction_bitmap::NAME, None, None)
338 .context("Compacting transaction_bitmap after prune")?;
339 db.compact_range_cf(event_bitmap::NAME, None, None)
340 .context("Compacting event_bitmap after prune")?;
341 }
342
343 Ok(())
344}
345
346fn prune_chunk(
349 db: &Db,
350 schema: &RpcStoreSchema,
351 cursor: Watermarks,
352 chunk_ckpt_hi: u64,
353 metrics: &PrunerMetrics,
354) -> anyhow::Result<Watermarks> {
355 let ckpt_lo = cursor.checkpoint_lo;
356 let tx_lo = cursor.tx_seq_lo;
357
358 let last_ckpt = chunk_ckpt_hi - 1;
365 let tx_hi = schema
366 .get_checkpoint_summary(last_ckpt)?
367 .with_context(|| format!("checkpoint_summary missing for checkpoint {last_ckpt}"))?
368 .data()
369 .network_total_transactions;
370
371 let mut batch = db.batch();
372 let mut retractions = Retractions::default();
373 let mut objects_deleted: u64 = 0;
374 let mut tx_cursor = tx_lo;
383 for seq in ckpt_lo..chunk_ckpt_hi {
384 let summary = schema
389 .get_checkpoint_summary(seq)?
390 .with_context(|| format!("checkpoint_summary missing for checkpoint {seq}"))?;
391 let ckpt_tx_hi = summary.data().network_total_transactions;
392
393 for tx_seq in tx_cursor..ckpt_tx_hi {
394 let Some((effects, _unchanged)) = schema.get_effects(tx_seq)? else {
395 continue;
396 };
397 for (id, version) in effects.modified_at_versions() {
398 batch.delete(&schema.objects, &objects::Key { id, version })?;
399 retractions.record(id, seq, false);
404 objects_deleted += 1;
405 }
406 for (id, version) in effects.all_tombstones() {
407 batch.delete(&schema.objects, &objects::Key { id, version })?;
408 retractions.record(id, seq, true);
411 objects_deleted += 1;
412 }
413 batch.delete(
414 &schema.tx_seq_by_digest,
415 &tx_seq_by_digest::Key(*effects.transaction_digest()),
416 )?;
417 }
418 tx_cursor = ckpt_tx_hi;
419
420 batch.delete(
422 &schema.checkpoint_seq_by_digest,
423 &checkpoint_seq_by_digest::Key(summary.data().digest()),
424 )?;
425 }
426
427 batch.delete_range(&schema.transactions, &U64Be(tx_lo), &U64Be(tx_hi))?;
431 batch.delete_range(&schema.effects, &U64Be(tx_lo), &U64Be(tx_hi))?;
432 batch.delete_range(&schema.events, &U64Be(tx_lo), &U64Be(tx_hi))?;
433 batch.delete_range(&schema.tx_metadata_by_seq, &U64Be(tx_lo), &U64Be(tx_hi))?;
434 batch.delete_range(
435 &schema.checkpoint_summary,
436 &U64Be(ckpt_lo),
437 &U64Be(chunk_ckpt_hi),
438 )?;
439 batch.delete_range(
440 &schema.checkpoint_contents,
441 &U64Be(ckpt_lo),
442 &U64Be(chunk_ckpt_hi),
443 )?;
444
445 retractions.stage(&mut batch, schema)?;
446
447 let new = Watermarks {
449 tx_seq_lo: tx_hi,
450 checkpoint_lo: chunk_ckpt_hi,
451 };
452 let (k, v) = pruning_watermark::store(&new);
453 batch.put(&schema.pruning_watermark, &k, &v)?;
454
455 batch.commit()?;
456
457 schema.set_pruning_floor(new.tx_seq_lo);
460 metrics.objects_deleted.inc_by(objects_deleted);
461
462 Ok(new)
463}
464
465fn retract_object_version_by_checkpoint(
499 batch: &mut Batch,
500 schema: &RpcStoreSchema,
501 id: ObjectID,
502 cp: u64,
503 removed: bool,
504) -> anyhow::Result<()> {
505 let lo = object_version_by_checkpoint::Key { id, checkpoint: 0 };
506 let hi = object_version_by_checkpoint::Key { id, checkpoint: cp };
507 for entry in schema
508 .object_version_by_checkpoint
509 .iter((Bound::Included(lo), Bound::Excluded(hi)))?
510 {
511 let (key, _value) = entry?;
512 batch.delete(&schema.object_version_by_checkpoint, &key)?;
513 }
514 if removed {
515 batch.delete(&schema.object_version_by_checkpoint, &hi)?;
516 }
517 Ok(())
518}
519
520pub fn prune_history_cohort(
568 db: &Db,
569 schema: &RpcStoreSchema,
570 pruned_checkpoint_watermark: u64,
571 pruned_tx_seq_exclusive: u64,
572 effects: &[(u64, TransactionEffects)],
573) -> anyhow::Result<()> {
574 let cursor = schema.get_pruning_watermarks()?.unwrap_or_default();
575 let tx_lo = cursor.tx_seq_lo;
576 let tx_hi = pruned_tx_seq_exclusive;
577 let checkpoint_lo = pruned_checkpoint_watermark.saturating_add(1);
580
581 if tx_hi <= tx_lo && checkpoint_lo <= cursor.checkpoint_lo {
584 return Ok(());
585 }
586
587 let mut batch = db.batch();
588 let mut retractions = Retractions::default();
589 for entry in schema.iter_tx_seq_digests(tx_lo, tx_hi)? {
595 let (_tx_seq, digest) = entry?;
596 batch.delete(&schema.tx_seq_by_digest, &tx_seq_by_digest::Key(digest))?;
597 }
598 batch.delete_range(&schema.tx_metadata_by_seq, &U64Be(tx_lo), &U64Be(tx_hi))?;
599
600 for (checkpoint, effects) in effects {
610 for (id, _version) in effects.modified_at_versions() {
611 retractions.record(id, *checkpoint, false);
612 }
613 for (id, _version) in effects.all_tombstones() {
614 retractions.record(id, *checkpoint, true);
615 }
616 }
617 retractions.stage(&mut batch, schema)?;
618
619 let new = Watermarks {
623 tx_seq_lo: tx_hi.max(tx_lo),
624 checkpoint_lo: checkpoint_lo.max(cursor.checkpoint_lo),
625 };
626 let (k, v) = pruning_watermark::store(&new);
627 batch.put(&schema.pruning_watermark, &k, &v)?;
628 batch.commit()?;
629
630 schema.set_pruning_floor(new.tx_seq_lo);
636
637 Ok(())
638}
639
640pub fn embedded_prunable_checkpoint(db: &Db) -> anyhow::Result<Option<u64>> {
662 let framework = db.framework();
663 let mut min_hi: Option<u64> = None;
664 for name in LIVE_COHORT.iter().chain(HISTORY_COHORT) {
665 let key = PipelineTaskKey::new(*name);
666 let Some(watermark) = framework
667 .watermarks
668 .get(&key)
669 .with_context(|| format!("reading watermark for {name}"))?
670 else {
671 return Ok(None);
672 };
673 let hi = watermark.checkpoint_hi_inclusive;
674 min_hi = Some(min_hi.map_or(hi, |m| m.min(hi)));
675 }
676 Ok(min_hi)
677}
678
679fn current_committed_epoch(db: &Db) -> anyhow::Result<Option<u64>> {
687 let framework = FrameworkSchema::new(db.clone());
688 let mut min_epoch: Option<u64> = None;
689 for entry in framework.watermarks.iter(..)? {
690 let (_, watermark) = entry?;
691 let epoch = watermark.epoch_hi_inclusive;
692 min_epoch = Some(min_epoch.map_or(epoch, |m| m.min(epoch)));
693 }
694 Ok(min_epoch)
695}
696
697fn retention_checkpoint_floor(
704 schema: &RpcStoreSchema,
705 current_epoch: u64,
706 retention_epochs: u64,
707) -> anyhow::Result<Option<u64>> {
708 debug_assert!(retention_epochs >= 1, "validated in start_pruner");
709
710 let oldest_retained = current_epoch.saturating_sub(retention_epochs - 1);
712 if oldest_retained == 0 {
713 return Ok(None);
715 }
716
717 let Some(info) = schema.get_epoch(oldest_retained)? else {
718 return Ok(None);
719 };
720 Ok(info.start_checkpoint)
721}
722
723fn clamp_to_snapshot(retention_lo: u64, oldest_snapshot: Option<u64>) -> u64 {
730 match oldest_snapshot {
731 Some(snap) => retention_lo.min(snap),
732 None => retention_lo,
733 }
734}
735
736#[cfg(test)]
737mod tests {
738 use std::sync::Arc;
739
740 use prometheus::Registry;
741 use sui_consistent_store::Db;
742 use sui_consistent_store::DbOptions;
743 use sui_consistent_store::PipelineTaskKey;
744 use sui_consistent_store::Watermark;
745 use sui_indexer_alt_framework::pipeline::Processor;
746 use sui_types::test_checkpoint_data_builder::TestCheckpointBuilder;
747
748 use super::*;
749 use crate::schema::epochs;
750 use crate::schema::primitives::U64Varint;
751
752 fn fresh_db() -> (tempfile::TempDir, Db, RpcStoreSchema) {
753 let dir = tempfile::tempdir().unwrap();
754 let (db, schema) = Db::open::<RpcStoreSchema>(dir.path(), DbOptions::default()).unwrap();
755 (dir, db, schema)
756 }
757
758 fn stamp_watermarks(db: &Db, names: &[&str], hi: u64) {
760 let framework = FrameworkSchema::new(db.clone());
761 let mut batch = db.batch();
762 for name in names {
763 batch
764 .put(
765 &framework.watermarks,
766 &PipelineTaskKey::new(*name),
767 &Watermark::for_checkpoint(hi),
768 )
769 .unwrap();
770 }
771 batch.commit().unwrap();
772 }
773
774 #[test]
778 fn embedded_prunable_checkpoint_is_min_across_cohorts() {
779 let (_dir, db, _schema) = fresh_db();
780
781 assert_eq!(embedded_prunable_checkpoint(&db).unwrap(), None);
783
784 stamp_watermarks(&db, LIVE_COHORT, 1_000);
788 assert_eq!(embedded_prunable_checkpoint(&db).unwrap(), None);
789
790 stamp_watermarks(&db, HISTORY_COHORT, 40);
793 stamp_watermarks(&db, &[HISTORY_COHORT[0]], 25);
794 assert_eq!(embedded_prunable_checkpoint(&db).unwrap(), Some(25));
795
796 stamp_watermarks(&db, HISTORY_COHORT, 2_000);
799 assert_eq!(embedded_prunable_checkpoint(&db).unwrap(), Some(1_000));
800 }
801
802 async fn seed(
809 db: &Db,
810 schema: &RpcStoreSchema,
811 checkpoint: &Arc<sui_types::full_checkpoint_content::Checkpoint>,
812 ) {
813 let mut batch = db.batch();
814 for row in crate::indexer::objects::Objects
815 .process(checkpoint)
816 .await
817 .unwrap()
818 {
819 batch
820 .put(
821 &schema.objects,
822 &objects::Key {
823 id: row.id,
824 version: row.version,
825 },
826 &row.value,
827 )
828 .unwrap();
829 }
830 for row in crate::indexer::effects::Effects
831 .process(checkpoint)
832 .await
833 .unwrap()
834 {
835 batch
836 .put(&schema.effects, &U64Be(row.tx_seq), &row.value)
837 .unwrap();
838 }
839 for row in crate::indexer::checkpoint_summary::CheckpointSummary
840 .process(checkpoint)
841 .await
842 .unwrap()
843 {
844 batch
845 .put(&schema.checkpoint_summary, &U64Be(row.seq), &row.value)
846 .unwrap();
847 }
848 for row in crate::indexer::tx_seq_by_digest::TxSeqByDigest
849 .process(checkpoint)
850 .await
851 .unwrap()
852 {
853 batch
854 .put(
855 &schema.tx_seq_by_digest,
856 &tx_seq_by_digest::Key(row.digest),
857 &U64Varint(row.tx_seq),
858 )
859 .unwrap();
860 }
861 for row in crate::indexer::checkpoint_seq_by_digest::CheckpointSeqByDigest
862 .process(checkpoint)
863 .await
864 .unwrap()
865 {
866 batch
867 .put(
868 &schema.checkpoint_seq_by_digest,
869 &checkpoint_seq_by_digest::Key(row.digest),
870 &U64Varint(row.seq),
871 )
872 .unwrap();
873 }
874 batch.commit().unwrap();
875 }
876
877 fn seed_checkpoint_versions(
878 db: &Db,
879 schema: &RpcStoreSchema,
880 id: ObjectID,
881 rows: &[(u64, u64)],
882 ) {
883 let mut batch = db.batch();
884 for &(checkpoint, version) in rows {
885 let (k, v) = object_version_by_checkpoint::store(
886 id,
887 checkpoint,
888 sui_types::base_types::SequenceNumber::from_u64(version),
889 );
890 batch
891 .put(&schema.object_version_by_checkpoint, &k, &v)
892 .unwrap();
893 }
894 batch.commit().unwrap();
895 }
896
897 #[test]
898 fn retractions_stage_widest_range_per_object() {
899 let (_dir, db, schema) = fresh_db();
900 let obj = TestCheckpointBuilder::derive_object_id(0);
901 seed_checkpoint_versions(&db, &schema, obj, &[(0, 1), (1, 2), (2, 3)]);
902
903 let mut retractions = Retractions::default();
904 retractions.record(obj, 1, true);
905 retractions.record(obj, 2, false);
906 let mut batch = db.batch();
907 retractions.stage(&mut batch, &schema).unwrap();
908 batch.commit().unwrap();
909
910 assert_eq!(
911 schema.get_object_version_at_checkpoint(obj, 1).unwrap(),
912 None,
913 "the widest range must cover lower-checkpoint removals",
914 );
915 assert_eq!(
916 schema.get_object_version_at_checkpoint(obj, 2).unwrap(),
917 Some(sui_types::base_types::SequenceNumber::from_u64(3)),
918 "a lower removed=true retraction must not delete the latest anchor",
919 );
920 }
921
922 #[test]
923 fn retractions_or_removed_on_equal_checkpoint() {
924 let (_dir, db, schema) = fresh_db();
925 let obj = TestCheckpointBuilder::derive_object_id(0);
926 seed_checkpoint_versions(&db, &schema, obj, &[(0, 1), (2, 3)]);
927
928 let mut retractions = Retractions::default();
929 retractions.record(obj, 2, false);
930 retractions.record(obj, 2, true);
931 let mut batch = db.batch();
932 retractions.stage(&mut batch, &schema).unwrap();
933 batch.commit().unwrap();
934
935 assert_eq!(
936 schema.get_object_version_at_checkpoint(obj, 2).unwrap(),
937 None,
938 "same-checkpoint removals must drop the checkpoint row",
939 );
940 }
941
942 #[test]
949 fn retraction_point_deletes_deep_history() {
950 let (_dir, db, schema) = fresh_db();
951 let obj = TestCheckpointBuilder::derive_object_id(0);
952 let neighbor = TestCheckpointBuilder::derive_object_id(1);
953
954 let rows: Vec<(u64, u64)> = (0..1_000u64).map(|c| (c, c + 1)).collect();
958 seed_checkpoint_versions(&db, &schema, obj, &rows);
959 seed_checkpoint_versions(&db, &schema, neighbor, &[(10, 42)]);
960
961 let mut retractions = Retractions::default();
965 for (checkpoint, _) in &rows {
966 retractions.record(obj, *checkpoint, false);
967 }
968 let mut batch = db.batch();
969 retractions.stage(&mut batch, &schema).unwrap();
970 batch.commit().unwrap();
971
972 assert_eq!(
975 schema.get_object_version_at_checkpoint(obj, 998).unwrap(),
976 None,
977 "history below the coalesced checkpoint must be fully point-deleted",
978 );
979 assert_eq!(
980 schema.get_object_version_at_checkpoint(obj, 999).unwrap(),
981 Some(sui_types::base_types::SequenceNumber::from_u64(1_000)),
982 "the anchor at the coalesced checkpoint must survive",
983 );
984 let remaining: Vec<u64> = schema
985 .iter_object_versions_by_checkpoint(obj)
986 .unwrap()
987 .map(|r| r.unwrap().0.checkpoint)
988 .collect();
989 assert_eq!(remaining, vec![999], "only the anchor row remains");
990
991 assert_eq!(
993 schema
994 .get_object_version_at_checkpoint(neighbor, 10)
995 .unwrap(),
996 Some(sui_types::base_types::SequenceNumber::from_u64(42)),
997 "the bounded scan must not delete a neighboring object's rows",
998 );
999 }
1000
1001 #[test]
1002 fn clamp_to_snapshot_holds_floor_at_or_below_oldest_snapshot() {
1003 assert_eq!(clamp_to_snapshot(100, None), 100);
1005 assert_eq!(clamp_to_snapshot(100, Some(250)), 100);
1007 assert_eq!(clamp_to_snapshot(300, Some(250)), 250);
1009 assert_eq!(clamp_to_snapshot(250, Some(250)), 250);
1011 }
1012
1013 #[test]
1014 fn retention_floor_none_when_chain_younger_than_window() {
1015 let (_dir, _db, schema) = fresh_db();
1016 assert!(retention_checkpoint_floor(&schema, 2, 5).unwrap().is_none());
1019 }
1020
1021 #[test]
1022 fn retention_floor_is_start_checkpoint_of_oldest_retained_epoch() {
1023 let (_dir, db, schema) = fresh_db();
1024 let mut batch = db.batch();
1026 batch
1027 .merge(
1028 &schema.epochs,
1029 &U64Be(3),
1030 &epochs::start(1, 1, 0, Some(300), None),
1031 )
1032 .unwrap();
1033 batch.commit().unwrap();
1034 assert_eq!(
1037 retention_checkpoint_floor(&schema, 5, 3).unwrap(),
1038 Some(300)
1039 );
1040 }
1041
1042 #[test]
1043 fn retention_floor_none_when_oldest_epoch_row_missing() {
1044 let (_dir, _db, schema) = fresh_db();
1045 assert!(
1047 retention_checkpoint_floor(&schema, 10, 2)
1048 .unwrap()
1049 .is_none()
1050 );
1051 }
1052
1053 #[test]
1054 fn current_committed_epoch_takes_min_across_watermarks() {
1055 let (_dir, db, _schema) = fresh_db();
1056 let framework = FrameworkSchema::new(db.clone());
1057 let mut batch = db.batch();
1058 batch
1059 .put(
1060 &framework.watermarks,
1061 &PipelineTaskKey::new("a"),
1062 &Watermark {
1063 epoch_hi_inclusive: 7,
1064 ..Default::default()
1065 },
1066 )
1067 .unwrap();
1068 batch
1069 .put(
1070 &framework.watermarks,
1071 &PipelineTaskKey::new("b"),
1072 &Watermark {
1073 epoch_hi_inclusive: 5,
1074 ..Default::default()
1075 },
1076 )
1077 .unwrap();
1078 batch.commit().unwrap();
1079 assert_eq!(current_committed_epoch(&db).unwrap(), Some(5));
1080 }
1081
1082 #[test]
1083 fn current_committed_epoch_none_when_no_watermarks() {
1084 let (_dir, db, _schema) = fresh_db();
1085 assert!(current_committed_epoch(&db).unwrap().is_none());
1086 }
1087
1088 #[test]
1092 fn merge_written_bitmap_buckets_require_two_compactions_for_reclamation() {
1093 let (_dir, db, schema) = fresh_db();
1094 let dimension = b"sender:alice".to_vec();
1095 let floor = transaction_bitmap::TX_BUCKET_SIZE;
1096 let retained_tx_seq = floor + 5;
1097
1098 let (tx_low_key, tx_low_value) = transaction_bitmap::store_match(dimension.clone(), 5);
1099 let (tx_high_key, tx_high_value) =
1100 transaction_bitmap::store_match(dimension.clone(), retained_tx_seq);
1101 let (event_low_key, event_low_value) = event_bitmap::store_match(dimension.clone(), 5, 0);
1102 let (event_high_key, event_high_value) =
1103 event_bitmap::store_match(dimension.clone(), retained_tx_seq, 0);
1104
1105 let mut batch = db.batch();
1106 batch
1107 .merge(&schema.transaction_bitmap, &tx_low_key, &tx_low_value)
1108 .unwrap();
1109 batch
1110 .merge(&schema.transaction_bitmap, &tx_high_key, &tx_high_value)
1111 .unwrap();
1112 batch
1113 .merge(&schema.event_bitmap, &event_low_key, &event_low_value)
1114 .unwrap();
1115 batch
1116 .merge(&schema.event_bitmap, &event_high_key, &event_high_value)
1117 .unwrap();
1118 batch.commit().unwrap();
1119 db.flush().unwrap();
1120
1121 let (watermark_key, watermark_value) = pruning_watermark::store(&Watermarks {
1122 tx_seq_lo: floor,
1123 checkpoint_lo: 1,
1124 });
1125 let mut batch = db.batch();
1126 batch
1127 .put(&schema.pruning_watermark, &watermark_key, &watermark_value)
1128 .unwrap();
1129 batch.commit().unwrap();
1130 schema.set_pruning_floor(floor);
1131
1132 db.compact_range_cf(transaction_bitmap::NAME, None, None)
1133 .unwrap();
1134 db.compact_range_cf(event_bitmap::NAME, None, None).unwrap();
1135
1136 assert!(
1137 schema
1138 .get_transaction_bitmap(dimension.clone(), tx_low_key.bucket)
1139 .unwrap()
1140 .is_some()
1141 );
1142 assert!(
1143 schema
1144 .get_transaction_bitmap(dimension.clone(), tx_high_key.bucket)
1145 .unwrap()
1146 .is_some()
1147 );
1148 assert!(
1149 schema
1150 .get_event_bitmap(dimension.clone(), event_low_key.bucket)
1151 .unwrap()
1152 .is_some()
1153 );
1154 assert!(
1155 schema
1156 .get_event_bitmap(dimension.clone(), event_high_key.bucket)
1157 .unwrap()
1158 .is_some()
1159 );
1160
1161 db.compact_range_cf(transaction_bitmap::NAME, None, None)
1162 .unwrap();
1163 db.compact_range_cf(event_bitmap::NAME, None, None).unwrap();
1164
1165 assert!(
1166 schema
1167 .get_transaction_bitmap(dimension.clone(), tx_low_key.bucket)
1168 .unwrap()
1169 .is_none()
1170 );
1171 assert!(
1172 schema
1173 .get_transaction_bitmap(dimension.clone(), tx_high_key.bucket)
1174 .unwrap()
1175 .is_some()
1176 );
1177 assert!(
1178 schema
1179 .get_event_bitmap(dimension.clone(), event_low_key.bucket)
1180 .unwrap()
1181 .is_none()
1182 );
1183 assert!(
1184 schema
1185 .get_event_bitmap(dimension, event_high_key.bucket)
1186 .unwrap()
1187 .is_some()
1188 );
1189 }
1190
1191 #[tokio::test]
1194 async fn prune_chunk_publishes_the_db_local_bitmap_floor() {
1195 let (_dir, db, schema) = fresh_db();
1196 let checkpoint = Arc::new(
1197 TestCheckpointBuilder::new(0)
1198 .start_transaction(0)
1199 .create_owned_object(0)
1200 .finish_transaction()
1201 .start_transaction(0)
1202 .transfer_object(0, 1)
1203 .finish_transaction()
1204 .build_checkpoint(),
1205 );
1206 seed(&db, &schema, &checkpoint).await;
1207
1208 let metrics = PrunerMetrics::new(None, &Registry::new());
1209 let new = prune_chunk(&db, &schema, Watermarks::default(), 1, &metrics).unwrap();
1210
1211 assert_eq!(
1212 schema.current_pruning_floor(),
1213 new.tx_seq_lo,
1214 "the chunk must publish its committed tx_seq floor",
1215 );
1216 }
1217
1218 #[test]
1219 fn start_pruner_rejects_zero_retention() {
1220 let (_dir, db, schema) = fresh_db();
1221 let store = Store::new(db, Arc::new(schema));
1222 let config = PrunerConfig {
1223 retention_epochs: 0,
1224 ..PrunerConfig::default()
1225 };
1226 let err =
1227 start_pruner(store, config, PrunerMetrics::new(None, &Registry::new())).unwrap_err();
1228 assert!(
1229 format!("{err:#}").contains("retention_epochs"),
1230 "expected a retention_epochs validation error, got: {err:#}",
1231 );
1232 }
1233
1234 #[test]
1235 fn start_pruner_rejects_zero_checkpoints_per_tick() {
1236 let (_dir, db, schema) = fresh_db();
1237 let store = Store::new(db, Arc::new(schema));
1238 let config = PrunerConfig {
1239 max_checkpoints_per_tick: 0,
1240 ..PrunerConfig::default()
1241 };
1242 let err =
1243 start_pruner(store, config, PrunerMetrics::new(None, &Registry::new())).unwrap_err();
1244 assert!(
1245 format!("{err:#}").contains("max_checkpoints_per_tick"),
1246 "expected a max_checkpoints_per_tick validation error, got: {err:#}",
1247 );
1248 }
1249
1250 #[tokio::test]
1258 async fn prune_once_advances_at_most_the_per_tick_budget() {
1259 let (_dir, db, schema) = fresh_db();
1260
1261 let mut builder = TestCheckpointBuilder::new(0);
1265 let mut checkpoints = Vec::new();
1266 for i in 0..5u64 {
1267 builder = builder
1268 .start_transaction(0)
1269 .create_owned_object(i)
1270 .finish_transaction();
1271 checkpoints.push(Arc::new(builder.build_checkpoint()));
1272 }
1273 for cp in &checkpoints {
1274 seed(&db, &schema, cp).await;
1275 }
1276
1277 let framework = FrameworkSchema::new(db.clone());
1282 let mut batch = db.batch();
1283 batch
1284 .put(
1285 &framework.watermarks,
1286 &PipelineTaskKey::new("p"),
1287 &Watermark {
1288 epoch_hi_inclusive: 2,
1289 ..Default::default()
1290 },
1291 )
1292 .unwrap();
1293 batch
1294 .merge(
1295 &schema.epochs,
1296 &U64Be(2),
1297 &epochs::start(1, 1, 0, Some(5), None),
1298 )
1299 .unwrap();
1300 batch.commit().unwrap();
1301
1302 let config = PrunerConfig {
1303 retention_epochs: 1,
1304 interval_ms: 1,
1305 max_chunk_checkpoints: 2,
1306 max_checkpoints_per_tick: 2,
1307 };
1308 let metrics = PrunerMetrics::new(None, &Registry::new());
1309
1310 let floor = |schema: &RpcStoreSchema| {
1311 schema
1312 .get_pruning_watermarks()
1313 .unwrap()
1314 .unwrap_or_default()
1315 .checkpoint_lo
1316 };
1317
1318 prune_once(&db, &schema, &config, &metrics).unwrap();
1320 assert_eq!(floor(&schema), 2, "first tick advances by the budget");
1321 prune_once(&db, &schema, &config, &metrics).unwrap();
1322 assert_eq!(floor(&schema), 4, "second tick advances by the budget");
1323 prune_once(&db, &schema, &config, &metrics).unwrap();
1324 assert_eq!(floor(&schema), 5, "third tick reaches the target");
1325
1326 assert!(schema.get_effects(4).unwrap().is_none());
1329 assert!(schema.get_checkpoint_summary(4).unwrap().is_none());
1330 prune_once(&db, &schema, &config, &metrics).unwrap();
1331 assert_eq!(floor(&schema), 5, "a pass at the target is a no-op");
1332 }
1333
1334 #[tokio::test]
1341 async fn prune_chunk_deletes_history_and_preserves_live_object() {
1342 let (_dir, db, schema) = fresh_db();
1343
1344 let checkpoint = Arc::new(
1345 TestCheckpointBuilder::new(0)
1346 .start_transaction(0)
1347 .create_owned_object(0)
1348 .finish_transaction()
1349 .start_transaction(0)
1350 .transfer_object(0, 1)
1351 .finish_transaction()
1352 .build_checkpoint(),
1353 );
1354
1355 let obj0 = TestCheckpointBuilder::derive_object_id(0);
1356 let v_a = checkpoint.transactions[0].effects.lamport_version();
1357 let v_b = checkpoint.transactions[1].effects.lamport_version();
1358 assert_ne!(v_a, v_b, "the transfer must bump the object's version");
1359 let digest0 = *checkpoint.transactions[0].effects.transaction_digest();
1360 let digest1 = *checkpoint.transactions[1].effects.transaction_digest();
1361 let ckpt_digest = checkpoint.summary.data().digest();
1362
1363 seed(&db, &schema, &checkpoint).await;
1364
1365 assert!(schema.get_object_by_key(obj0, v_a).unwrap().is_some());
1367 assert!(schema.get_object_by_key(obj0, v_b).unwrap().is_some());
1368 assert!(schema.get_effects(0).unwrap().is_some());
1369 assert!(schema.get_effects(1).unwrap().is_some());
1370 assert!(schema.get_checkpoint_summary(0).unwrap().is_some());
1371
1372 let metrics = PrunerMetrics::new(None, &Registry::new());
1374 let new = prune_chunk(&db, &schema, Watermarks::default(), 1, &metrics).unwrap();
1375 assert_eq!(
1376 new,
1377 Watermarks {
1378 tx_seq_lo: 2,
1379 checkpoint_lo: 1,
1380 },
1381 );
1382
1383 assert!(
1385 schema.get_object_by_key(obj0, v_a).unwrap().is_none(),
1386 "superseded version v_a should be pruned",
1387 );
1388 assert!(
1389 schema.get_object_by_key(obj0, v_b).unwrap().is_some(),
1390 "live version v_b must be preserved",
1391 );
1392
1393 assert!(schema.get_effects(0).unwrap().is_none());
1395 assert!(schema.get_effects(1).unwrap().is_none());
1396 assert!(schema.get_checkpoint_summary(0).unwrap().is_none());
1397
1398 assert!(
1400 schema
1401 .tx_seq_by_digest
1402 .get(&tx_seq_by_digest::Key(digest0))
1403 .unwrap()
1404 .is_none()
1405 );
1406 assert!(
1407 schema
1408 .tx_seq_by_digest
1409 .get(&tx_seq_by_digest::Key(digest1))
1410 .unwrap()
1411 .is_none()
1412 );
1413 assert!(
1414 schema
1415 .checkpoint_seq_by_digest
1416 .get(&checkpoint_seq_by_digest::Key(ckpt_digest))
1417 .unwrap()
1418 .is_none()
1419 );
1420
1421 assert_eq!(
1423 schema.get_pruning_watermarks().unwrap().unwrap(),
1424 Watermarks {
1425 tx_seq_lo: 2,
1426 checkpoint_lo: 1,
1427 },
1428 );
1429 }
1430
1431 #[tokio::test]
1440 async fn prune_chunk_retracts_version_only_when_superseding_tx_is_pruned() {
1441 let (_dir, db, schema) = fresh_db();
1442
1443 let mut builder = TestCheckpointBuilder::new(0)
1446 .start_transaction(0)
1447 .create_owned_object(0)
1448 .finish_transaction();
1449 let cp0 = Arc::new(builder.build_checkpoint());
1450 builder = builder
1451 .start_transaction(0)
1452 .transfer_object(0, 1)
1453 .finish_transaction();
1454 let cp1 = Arc::new(builder.build_checkpoint());
1455
1456 let obj0 = TestCheckpointBuilder::derive_object_id(0);
1457 let v_a = cp0.transactions[0].effects.lamport_version();
1458 let v_b = cp1.transactions[0].effects.lamport_version();
1459 assert_ne!(v_a, v_b);
1460
1461 seed(&db, &schema, &cp0).await;
1462 seed(&db, &schema, &cp1).await;
1463 let metrics = PrunerMetrics::new(None, &Registry::new());
1464
1465 let after_first = prune_chunk(&db, &schema, Watermarks::default(), 1, &metrics).unwrap();
1468 assert_eq!(
1469 after_first,
1470 Watermarks {
1471 tx_seq_lo: 1,
1472 checkpoint_lo: 1,
1473 },
1474 );
1475 assert!(schema.get_effects(0).unwrap().is_none());
1476 assert!(schema.get_effects(1).unwrap().is_some());
1477 assert!(
1478 schema.get_object_by_key(obj0, v_a).unwrap().is_some(),
1479 "v_a must survive while its superseding tx is still retained",
1480 );
1481
1482 let after_second = prune_chunk(&db, &schema, after_first, 2, &metrics).unwrap();
1485 assert_eq!(
1486 after_second,
1487 Watermarks {
1488 tx_seq_lo: 2,
1489 checkpoint_lo: 2,
1490 },
1491 );
1492 assert!(schema.get_effects(1).unwrap().is_none());
1493 assert!(
1494 schema.get_object_by_key(obj0, v_a).unwrap().is_none(),
1495 "v_a must be retracted once its superseding tx is pruned",
1496 );
1497 assert!(
1498 schema.get_object_by_key(obj0, v_b).unwrap().is_some(),
1499 "live v_b must be preserved",
1500 );
1501 }
1502
1503 #[tokio::test]
1514 async fn prune_chunk_retracts_object_version_by_checkpoint() {
1515 use crate::indexer::object_version_by_checkpoint::ObjectVersionByCheckpoint;
1516
1517 let (_dir, db, schema) = fresh_db();
1518
1519 let mut builder = TestCheckpointBuilder::new(0)
1520 .start_transaction(0)
1521 .create_owned_object(0)
1522 .finish_transaction();
1523 let cp0 = Arc::new(builder.build_checkpoint());
1524 builder = builder
1525 .start_transaction(0)
1526 .transfer_object(0, 1)
1527 .finish_transaction();
1528 let cp1 = Arc::new(builder.build_checkpoint());
1529
1530 let obj0 = TestCheckpointBuilder::derive_object_id(0);
1531 let v_a = cp0.transactions[0].effects.lamport_version();
1532 let v_b = cp1.transactions[0].effects.lamport_version();
1533 assert_ne!(v_a, v_b);
1534
1535 for cp in [&cp0, &cp1] {
1540 seed(&db, &schema, cp).await;
1541 let mut batch = db.batch();
1542 for row in ObjectVersionByCheckpoint::default()
1543 .process(cp)
1544 .await
1545 .unwrap()
1546 {
1547 let crate::indexer::object_version_by_checkpoint::Row::Change {
1550 id,
1551 checkpoint,
1552 version,
1553 } = row
1554 else {
1555 continue;
1556 };
1557 let (k, v) = object_version_by_checkpoint::store(id, checkpoint, version);
1558 batch
1559 .put(&schema.object_version_by_checkpoint, &k, &v)
1560 .unwrap();
1561 }
1562 batch.commit().unwrap();
1563 }
1564
1565 assert_eq!(
1567 schema.get_object_version_at_checkpoint(obj0, 0).unwrap(),
1568 Some(v_a),
1569 );
1570 assert_eq!(
1571 schema.get_object_version_at_checkpoint(obj0, 1).unwrap(),
1572 Some(v_b),
1573 );
1574
1575 let metrics = PrunerMetrics::new(None, &Registry::new());
1576
1577 let after_first = prune_chunk(&db, &schema, Watermarks::default(), 1, &metrics).unwrap();
1580 assert_eq!(
1581 schema.get_object_version_at_checkpoint(obj0, 0).unwrap(),
1582 Some(v_a),
1583 "cp0-pinned entry must survive while its superseding tx is retained",
1584 );
1585
1586 prune_chunk(&db, &schema, after_first, 2, &metrics).unwrap();
1589 assert_eq!(
1590 schema.get_object_version_at_checkpoint(obj0, 0).unwrap(),
1591 None,
1592 "cp0-pinned entry must be retracted once its superseding tx is pruned",
1593 );
1594 assert_eq!(
1595 schema.get_object_version_at_checkpoint(obj0, 1).unwrap(),
1596 Some(v_b),
1597 "cp1-pinned floor entry must be preserved",
1598 );
1599 }
1600
1601 #[test]
1606 fn prune_history_cohort_retracts_object_version_by_checkpoint() {
1607 let (_dir, db, schema) = fresh_db();
1608
1609 let mut builder = TestCheckpointBuilder::new(0)
1612 .start_transaction(0)
1613 .create_owned_object(0)
1614 .create_owned_object(1)
1615 .finish_transaction();
1616 let cp0 = Arc::new(builder.build_checkpoint());
1617 builder = builder
1618 .start_transaction(0)
1619 .transfer_object(0, 1)
1620 .finish_transaction()
1621 .start_transaction(0)
1622 .delete_object(1)
1623 .finish_transaction();
1624 let cp1 = Arc::new(builder.build_checkpoint());
1625
1626 let obj0 = TestCheckpointBuilder::derive_object_id(0);
1627 let obj1 = TestCheckpointBuilder::derive_object_id(1);
1628
1629 let ver = |n: u64| sui_types::base_types::SequenceNumber::from_u64(n);
1633 let mut batch = db.batch();
1634 for (id, checkpoint, version) in [(obj0, 0, 1), (obj0, 1, 2), (obj1, 0, 1), (obj1, 1, 2)] {
1635 let (k, v) = object_version_by_checkpoint::store(id, checkpoint, ver(version));
1636 batch
1637 .put(&schema.object_version_by_checkpoint, &k, &v)
1638 .unwrap();
1639 }
1640 batch.commit().unwrap();
1641
1642 assert_eq!(
1644 schema.get_object_version_at_checkpoint(obj0, 0).unwrap(),
1645 Some(ver(1)),
1646 );
1647 assert_eq!(
1648 schema.get_object_version_at_checkpoint(obj1, 0).unwrap(),
1649 Some(ver(1)),
1650 );
1651
1652 let effects: Vec<(u64, TransactionEffects)> = cp0
1657 .transactions
1658 .iter()
1659 .map(|tx| (0u64, tx.effects.clone()))
1660 .chain(cp1.transactions.iter().map(|tx| (1u64, tx.effects.clone())))
1661 .collect();
1662 prune_history_cohort(&db, &schema, 1, 0, &effects).unwrap();
1663
1664 assert_eq!(
1667 schema.get_object_version_at_checkpoint(obj0, 0).unwrap(),
1668 None,
1669 "a superseded object's pre-anchor row must be retracted",
1670 );
1671 assert_eq!(
1672 schema.get_object_version_at_checkpoint(obj0, 2).unwrap(),
1673 Some(ver(2)),
1674 "the anchor a read at the floor resolves to must survive",
1675 );
1676
1677 assert_eq!(
1679 schema.get_object_version_at_checkpoint(obj1, 2).unwrap(),
1680 None,
1681 "a removed object's rows must be dropped entirely",
1682 );
1683
1684 assert_eq!(
1686 schema
1687 .get_pruning_watermarks()
1688 .unwrap()
1689 .unwrap()
1690 .checkpoint_lo,
1691 2,
1692 );
1693 }
1694
1695 #[test]
1701 fn prune_history_cohort_deletes_tx_metadata_and_advances_floor() {
1702 use sui_types::digests::TransactionDigest;
1703
1704 use crate::schema::tx_metadata_by_seq;
1705
1706 let (_dir, db, schema) = fresh_db();
1707
1708 let digests: Vec<TransactionDigest> =
1711 (0u8..6).map(|i| TransactionDigest::new([i; 32])).collect();
1712 let mut batch = db.batch();
1713 for (tx_seq, digest) in digests.iter().enumerate() {
1714 let tx_seq = tx_seq as u64;
1715 batch
1716 .put(
1717 &schema.tx_metadata_by_seq,
1718 &U64Be(tx_seq),
1719 &tx_metadata_by_seq::store(&tx_metadata_by_seq::Metadata {
1720 digest: *digest,
1721 checkpoint_seq: tx_seq,
1722 ckpt_position: 0,
1723 event_count: 0,
1724 timestamp_ms: 0,
1725 }),
1726 )
1727 .unwrap();
1728 batch
1729 .put(
1730 &schema.tx_seq_by_digest,
1731 &tx_seq_by_digest::Key(*digest),
1732 &U64Varint(tx_seq),
1733 )
1734 .unwrap();
1735 }
1736 batch.commit().unwrap();
1737
1738 prune_history_cohort(&db, &schema, 2, 3, &[]).unwrap();
1741
1742 for tx_seq in 0..3 {
1744 assert!(
1745 schema.get_tx_metadata_by_seq(tx_seq).unwrap().is_none(),
1746 "tx_metadata {tx_seq} should be pruned",
1747 );
1748 }
1749 for tx_seq in 3..6 {
1750 assert!(
1751 schema.get_tx_metadata_by_seq(tx_seq).unwrap().is_some(),
1752 "tx_metadata {tx_seq} should be retained",
1753 );
1754 }
1755
1756 for digest in &digests[0..3] {
1758 assert!(schema.get_tx_seq_by_digest(digest).unwrap().is_none());
1759 }
1760 for digest in &digests[3..6] {
1761 assert!(schema.get_tx_seq_by_digest(digest).unwrap().is_some());
1762 }
1763
1764 assert_eq!(
1766 schema.get_pruning_watermarks().unwrap(),
1767 Some(Watermarks {
1768 tx_seq_lo: 3,
1769 checkpoint_lo: 3,
1770 }),
1771 );
1772
1773 prune_history_cohort(&db, &schema, 2, 3, &[]).unwrap();
1775 assert_eq!(
1776 schema.get_pruning_watermarks().unwrap(),
1777 Some(Watermarks {
1778 tx_seq_lo: 3,
1779 checkpoint_lo: 3,
1780 }),
1781 );
1782 }
1783
1784 #[test]
1789 fn prune_history_cohort_handles_sparse_tx_seqs() {
1790 use sui_types::digests::TransactionDigest;
1791
1792 use crate::schema::tx_metadata_by_seq;
1793
1794 let (_dir, db, schema) = fresh_db();
1795
1796 let entries = [
1798 (0u64, [10u8; 32]),
1799 (500_000u64, [11u8; 32]),
1800 (999_999u64, [12u8; 32]),
1801 ];
1802 let mut batch = db.batch();
1803 for (tx_seq, digest_bytes) in entries {
1804 let digest = TransactionDigest::new(digest_bytes);
1805 batch
1806 .put(
1807 &schema.tx_metadata_by_seq,
1808 &U64Be(tx_seq),
1809 &tx_metadata_by_seq::store(&tx_metadata_by_seq::Metadata {
1810 digest,
1811 checkpoint_seq: tx_seq,
1812 ckpt_position: 0,
1813 event_count: 0,
1814 timestamp_ms: 0,
1815 }),
1816 )
1817 .unwrap();
1818 batch
1819 .put(
1820 &schema.tx_seq_by_digest,
1821 &tx_seq_by_digest::Key(digest),
1822 &U64Varint(tx_seq),
1823 )
1824 .unwrap();
1825 }
1826 batch.commit().unwrap();
1827
1828 prune_history_cohort(&db, &schema, 0, 600_000, &[]).unwrap();
1832
1833 assert!(schema.get_tx_metadata_by_seq(0).unwrap().is_none());
1834 assert!(schema.get_tx_metadata_by_seq(500_000).unwrap().is_none());
1835 assert!(schema.get_tx_metadata_by_seq(999_999).unwrap().is_some());
1836 assert!(
1837 schema
1838 .get_tx_seq_by_digest(&TransactionDigest::new([10u8; 32]))
1839 .unwrap()
1840 .is_none()
1841 );
1842 assert!(
1843 schema
1844 .get_tx_seq_by_digest(&TransactionDigest::new([11u8; 32]))
1845 .unwrap()
1846 .is_none()
1847 );
1848 assert!(
1849 schema
1850 .get_tx_seq_by_digest(&TransactionDigest::new([12u8; 32]))
1851 .unwrap()
1852 .is_some()
1853 );
1854 assert_eq!(
1855 schema.get_pruning_watermarks().unwrap(),
1856 Some(Watermarks {
1857 tx_seq_lo: 600_000,
1858 checkpoint_lo: 1,
1859 }),
1860 );
1861 }
1862}