LCOV - code coverage report
Current view: top level - pageserver/src/tenant/remote_timeline_client - index.rs (source / functions) Coverage Total Hit
Test: c8f8d331b83562868d9054d9e0e68f866772aeaa.info Lines: 92.0 % 1062 977
Test Date: 2025-07-26 17:20:05 Functions: 38.7 % 62 24

            Line data    Source code
       1              : //! In-memory index to track the tenant files on the remote storage.
       2              : //!
       3              : //! Able to restore itself from the storage index parts, that are located in every timeline's remote directory and contain all data about
       4              : //! remote timeline layers and its metadata.
       5              : 
       6              : use std::collections::HashMap;
       7              : 
       8              : use chrono::NaiveDateTime;
       9              : use pageserver_api::models::AuxFilePolicy;
      10              : use pageserver_api::models::RelSizeMigration;
      11              : use pageserver_api::shard::ShardIndex;
      12              : use serde::{Deserialize, Serialize};
      13              : use utils::id::TimelineId;
      14              : use utils::lsn::Lsn;
      15              : 
      16              : use super::is_same_remote_layer_path;
      17              : use crate::tenant::Generation;
      18              : use crate::tenant::metadata::TimelineMetadata;
      19              : use crate::tenant::storage_layer::LayerName;
      20              : use crate::tenant::timeline::import_pgdata;
      21              : 
      22              : /// In-memory representation of an `index_part.json` file
      23              : ///
      24              : /// Contains the data about all files in the timeline, present remotely and its metadata.
      25              : ///
      26              : /// This type needs to be backwards and forwards compatible. When changing the fields,
      27              : /// remember to add a test case for the changed version.
      28              : #[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
      29              : pub struct IndexPart {
      30              :     /// Debugging aid describing the version of this type.
      31              :     #[serde(default)]
      32              :     version: usize,
      33              : 
      34              :     #[serde(default)]
      35              :     #[serde(skip_serializing_if = "Option::is_none")]
      36              :     pub deleted_at: Option<NaiveDateTime>,
      37              : 
      38              :     #[serde(default)]
      39              :     #[serde(skip_serializing_if = "Option::is_none")]
      40              :     pub archived_at: Option<NaiveDateTime>,
      41              : 
      42              :     /// This field supports import-from-pgdata ("fast imports" platform feature).
      43              :     /// We don't currently use fast imports, so, this field is None for all production timelines.
      44              :     /// See <https://github.com/neondatabase/neon/pull/9218> for more information.
      45              :     #[serde(default)]
      46              :     #[serde(skip_serializing_if = "Option::is_none")]
      47              :     pub import_pgdata: Option<import_pgdata::index_part_format::Root>,
      48              : 
      49              :     /// Layer filenames and metadata. For an index persisted in remote storage, all layers must
      50              :     /// exist in remote storage.
      51              :     pub layer_metadata: HashMap<LayerName, LayerFileMetadata>,
      52              : 
      53              :     /// Because of the trouble of eyeballing the legacy "metadata" field, we copied the
      54              :     /// "disk_consistent_lsn" out. After version 7 this is no longer needed, but the name cannot be
      55              :     /// reused.
      56              :     pub(super) disk_consistent_lsn: Lsn,
      57              : 
      58              :     // TODO: rename as "metadata" next week, keep the alias = "metadata_bytes", bump version Adding
      59              :     // the "alias = metadata" was forgotten in #7693, so we have to use "rewrite = metadata_bytes"
      60              :     // for backwards compatibility.
      61              :     #[serde(
      62              :         rename = "metadata_bytes",
      63              :         alias = "metadata",
      64              :         with = "crate::tenant::metadata::modern_serde"
      65              :     )]
      66              :     pub metadata: TimelineMetadata,
      67              : 
      68              :     #[serde(default)]
      69              :     pub(crate) lineage: Lineage,
      70              : 
      71              :     #[serde(skip_serializing_if = "Option::is_none", default)]
      72              :     pub(crate) gc_blocking: Option<GcBlocking>,
      73              : 
      74              :     /// Describes the kind of aux files stored in the timeline.
      75              :     ///
      76              :     /// The value is modified during file ingestion when the latest wanted value communicated via tenant config is applied if it is acceptable.
      77              :     /// A V1 setting after V2 files have been committed is not accepted.
      78              :     ///
      79              :     /// None means no aux files have been written to the storage before the point
      80              :     /// when this flag is introduced.
      81              :     ///
      82              :     /// This flag is not used any more as all tenants have been transitioned to the new aux file policy.
      83              :     #[serde(skip_serializing_if = "Option::is_none", default)]
      84              :     pub(crate) last_aux_file_policy: Option<AuxFilePolicy>,
      85              : 
      86              :     #[serde(skip_serializing_if = "Option::is_none", default)]
      87              :     pub(crate) rel_size_migration: Option<RelSizeMigration>,
      88              : 
      89              :     /// Not used anymore -- kept here for backwards compatibility. Merged into the `gc_compaction` field.
      90              :     #[serde(skip_serializing_if = "Option::is_none", default)]
      91              :     l2_lsn: Option<Lsn>,
      92              : 
      93              :     /// State for the garbage-collecting compaction pass.
      94              :     ///
      95              :     /// Garbage-collecting compaction (gc-compaction) prunes `Value`s that are outside
      96              :     /// the PITR window and not needed by child timelines.
      97              :     ///
      98              :     /// A commonly used synonym for this compaction pass is
      99              :     /// "bottommost-compaction"  because the affected LSN range
     100              :     /// is the "bottom" of the (key,lsn) map.
     101              :     ///
     102              :     /// Gc-compaction is a quite expensive operation; that's why we use
     103              :     /// trigger condition.
     104              :     /// This field here holds the state pertaining to that trigger condition
     105              :     /// and (in future) to the progress of the gc-compaction, so that it's
     106              :     /// resumable across restarts & migrations.
     107              :     ///
     108              :     /// Note that the underlying algorithm is _also_ called `gc-compaction`
     109              :     /// in most places & design docs; but in fact it is more flexible than
     110              :     /// just the specific use case here; it needs a new name.
     111              :     #[serde(skip_serializing_if = "Option::is_none", default)]
     112              :     pub(crate) gc_compaction: Option<GcCompactionState>,
     113              : 
     114              :     /// The timestamp when the timeline was marked invisible in synthetic size calculations.
     115              :     #[serde(skip_serializing_if = "Option::is_none", default)]
     116              :     pub(crate) marked_invisible_at: Option<NaiveDateTime>,
     117              : 
     118              :     /// The LSN at which we started the rel size migration. Accesses below this LSN should be
     119              :     /// processed with the v1 read path. Usually this LSN should be set together with `rel_size_migration`.
     120              :     #[serde(skip_serializing_if = "Option::is_none", default)]
     121              :     pub(crate) rel_size_migrated_at: Option<Lsn>,
     122              : }
     123              : 
     124            0 : #[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
     125              : pub struct GcCompactionState {
     126              :     /// The upper bound of the last completed garbage-collecting compaction, aka. L2 LSN.
     127              :     pub(crate) last_completed_lsn: Lsn,
     128              : }
     129              : 
     130              : impl IndexPart {
     131              :     /// When adding or modifying any parts of `IndexPart`, increment the version so that it can be
     132              :     /// used to understand later versions.
     133              :     ///
     134              :     /// Version is currently informative only.
     135              :     /// Version history
     136              :     /// - 2: added `deleted_at`
     137              :     /// - 3: no longer deserialize `timeline_layers` (serialized format is the same, but timeline_layers
     138              :     ///   is always generated from the keys of `layer_metadata`)
     139              :     /// - 4: timeline_layers is fully removed.
     140              :     /// - 5: lineage was added
     141              :     /// - 6: last_aux_file_policy is added.
     142              :     /// - 7: metadata_bytes is no longer written, but still read
     143              :     /// - 8: added `archived_at`
     144              :     /// - 9: +gc_blocking
     145              :     /// - 10: +import_pgdata
     146              :     /// - 11: +rel_size_migration
     147              :     /// - 12: +l2_lsn
     148              :     /// - 13: +gc_compaction
     149              :     /// - 14: +marked_invisible_at
     150              :     /// - 15: +rel_size_migrated_at
     151              :     const LATEST_VERSION: usize = 15;
     152              : 
     153              :     // Versions we may see when reading from a bucket.
     154              :     pub const KNOWN_VERSIONS: &'static [usize] =
     155              :         &[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15];
     156              : 
     157              :     pub const FILE_NAME: &'static str = "index_part.json";
     158              : 
     159          274 :     pub fn empty(metadata: TimelineMetadata) -> Self {
     160          274 :         IndexPart {
     161          274 :             version: Self::LATEST_VERSION,
     162          274 :             layer_metadata: Default::default(),
     163          274 :             disk_consistent_lsn: metadata.disk_consistent_lsn(),
     164          274 :             metadata,
     165          274 :             deleted_at: None,
     166          274 :             archived_at: None,
     167          274 :             lineage: Default::default(),
     168          274 :             gc_blocking: None,
     169          274 :             last_aux_file_policy: None,
     170          274 :             import_pgdata: None,
     171          274 :             rel_size_migration: None,
     172          274 :             l2_lsn: None,
     173          274 :             gc_compaction: None,
     174          274 :             marked_invisible_at: None,
     175          274 :             rel_size_migrated_at: None,
     176          274 :         }
     177          274 :     }
     178              : 
     179            0 :     pub fn version(&self) -> usize {
     180            0 :         self.version
     181            0 :     }
     182              : 
     183              :     /// If you want this under normal operations, read it from self.metadata:
     184              :     /// this method is just for the scrubber to use when validating an index.
     185            0 :     pub fn duplicated_disk_consistent_lsn(&self) -> Lsn {
     186            0 :         self.disk_consistent_lsn
     187            0 :     }
     188              : 
     189           15 :     pub fn from_json_bytes(bytes: &[u8]) -> Result<Self, serde_json::Error> {
     190           15 :         serde_json::from_slice::<IndexPart>(bytes)
     191           15 :     }
     192              : 
     193          772 :     pub fn to_json_bytes(&self) -> serde_json::Result<Vec<u8>> {
     194          772 :         serde_json::to_vec(self)
     195          772 :     }
     196              : 
     197              :     #[cfg(test)]
     198           14 :     pub(crate) fn example() -> Self {
     199           14 :         Self::empty(TimelineMetadata::example())
     200           14 :     }
     201              : 
     202              :     /// Returns true if the index contains a reference to the given layer (i.e. file path).
     203              :     ///
     204              :     /// TODO: there should be a variant of LayerName for the physical remote path that contains
     205              :     /// information about the shard and generation, to avoid passing in metadata.
     206        25680 :     pub fn references(&self, name: &LayerName, metadata: &LayerFileMetadata) -> bool {
     207        25680 :         let Some(index_metadata) = self.layer_metadata.get(name) else {
     208        12775 :             return false;
     209              :         };
     210        12905 :         is_same_remote_layer_path(name, metadata, name, index_metadata)
     211        25680 :     }
     212              : 
     213              :     /// Check for invariants in the index: this is useful when uploading an index to ensure that if
     214              :     /// we encounter a bug, we do not persist buggy metadata.
     215         1395 :     pub(crate) fn validate(&self) -> Result<(), String> {
     216         1395 :         if self.import_pgdata.is_none()
     217         1395 :             && self.metadata.ancestor_timeline().is_none()
     218         1039 :             && self.layer_metadata.is_empty()
     219              :         {
     220              :             // Unless we're in the middle of a raw pgdata import, or this is a child timeline,the index must
     221              :             // always have at least one layer.
     222            0 :             return Err("Index has no ancestor and no layers".to_string());
     223         1395 :         }
     224              : 
     225         1395 :         Ok(())
     226         1395 :     }
     227              : }
     228              : 
     229              : /// Metadata gathered for each of the layer files.
     230              : ///
     231              : /// Fields have to be `Option`s because remote [`IndexPart`]'s can be from different version, which
     232              : /// might have less or more metadata depending if upgrading or rolling back an upgrade.
     233            0 : #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
     234              : pub struct LayerFileMetadata {
     235              :     pub file_size: u64,
     236              : 
     237              :     #[serde(default = "Generation::none")]
     238              :     #[serde(skip_serializing_if = "Generation::is_none")]
     239              :     pub generation: Generation,
     240              : 
     241              :     #[serde(default = "ShardIndex::unsharded")]
     242              :     #[serde(skip_serializing_if = "ShardIndex::is_unsharded")]
     243              :     pub shard: ShardIndex,
     244              : }
     245              : 
     246              : impl LayerFileMetadata {
     247         1612 :     pub fn new(file_size: u64, generation: Generation, shard: ShardIndex) -> Self {
     248         1612 :         LayerFileMetadata {
     249         1612 :             file_size,
     250         1612 :             generation,
     251         1612 :             shard,
     252         1612 :         }
     253         1612 :     }
     254              :     /// Helper to get both generation and file size in a tuple
     255            0 :     pub fn generation_file_size(&self) -> (Generation, u64) {
     256            0 :         (self.generation, self.file_size)
     257            0 :     }
     258              : }
     259              : 
     260              : /// Limited history of earlier ancestors.
     261              : ///
     262              : /// A timeline can have more than 1 earlier ancestor, in the rare case that it was repeatedly
     263              : /// reparented by having an later timeline be detached from it's ancestor.
     264            0 : #[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize, Default)]
     265              : pub(crate) struct Lineage {
     266              :     /// Has the `reparenting_history` been truncated to [`Lineage::REMEMBER_AT_MOST`].
     267              :     #[serde(skip_serializing_if = "is_false", default)]
     268              :     reparenting_history_truncated: bool,
     269              : 
     270              :     /// Earlier ancestors, truncated when [`Self::reparenting_history_truncated`]
     271              :     ///
     272              :     /// These are stored in case we want to support WAL based DR on the timeline. There can be many
     273              :     /// of these and at most one [`Self::original_ancestor`]. There cannot be more reparentings
     274              :     /// after [`Self::original_ancestor`] has been set.
     275              :     #[serde(skip_serializing_if = "Vec::is_empty", default)]
     276              :     reparenting_history: Vec<TimelineId>,
     277              : 
     278              :     /// The ancestor from which this timeline has been detached from and when.
     279              :     ///
     280              :     /// If you are adding support for detaching from a hierarchy, consider changing the ancestry
     281              :     /// into a `Vec<(TimelineId, Lsn)>` to be a path instead.
     282              :     // FIXME: this is insufficient even for path of two timelines for future wal recovery
     283              :     // purposes:
     284              :     //
     285              :     // assuming a "old main" which has received most of the WAL, and has a branch "new main",
     286              :     // starting a bit before "old main" last_record_lsn. the current version works fine,
     287              :     // because we will know to replay wal and branch at the recorded Lsn to do wal recovery.
     288              :     //
     289              :     // then assuming "new main" would similarly receive a branch right before its last_record_lsn,
     290              :     // "new new main". the current implementation would just store ("new main", ancestor_lsn, _)
     291              :     // here. however, we cannot recover from WAL using only that information, we would need the
     292              :     // whole ancestry here:
     293              :     //
     294              :     // ```json
     295              :     // [
     296              :     //   ["old main", ancestor_lsn("new main"), _],
     297              :     //   ["new main", ancestor_lsn("new new main"), _]
     298              :     // ]
     299              :     // ```
     300              :     #[serde(skip_serializing_if = "Option::is_none", default)]
     301              :     original_ancestor: Option<(TimelineId, Lsn, NaiveDateTime)>,
     302              : }
     303              : 
     304         3150 : fn is_false(b: &bool) -> bool {
     305         3150 :     !b
     306         3150 : }
     307              : 
     308              : impl Lineage {
     309              :     const REMEMBER_AT_MOST: usize = 100;
     310              : 
     311            0 :     pub(crate) fn record_previous_ancestor(&mut self, old_ancestor: &TimelineId) -> bool {
     312            0 :         if self.reparenting_history.last() == Some(old_ancestor) {
     313              :             // do not re-record it
     314            0 :             false
     315              :         } else {
     316              :             #[cfg(feature = "testing")]
     317              :             {
     318            0 :                 let existing = self
     319            0 :                     .reparenting_history
     320            0 :                     .iter()
     321            0 :                     .position(|x| x == old_ancestor);
     322            0 :                 assert_eq!(
     323              :                     existing, None,
     324            0 :                     "we cannot reparent onto and off and onto the same timeline twice"
     325              :                 );
     326              :             }
     327            0 :             let drop_oldest = self.reparenting_history.len() + 1 >= Self::REMEMBER_AT_MOST;
     328              : 
     329            0 :             self.reparenting_history_truncated |= drop_oldest;
     330            0 :             if drop_oldest {
     331            0 :                 self.reparenting_history.remove(0);
     332            0 :             }
     333            0 :             self.reparenting_history.push(*old_ancestor);
     334            0 :             true
     335              :         }
     336            0 :     }
     337              : 
     338              :     /// Returns true if anything changed.
     339            0 :     pub(crate) fn record_detaching(&mut self, branchpoint: &(TimelineId, Lsn)) -> bool {
     340            0 :         if let Some((id, lsn, _)) = self.original_ancestor {
     341            0 :             assert_eq!(
     342            0 :                 &(id, lsn),
     343              :                 branchpoint,
     344            0 :                 "detaching attempt has to be for the same ancestor we are already detached from"
     345              :             );
     346            0 :             false
     347              :         } else {
     348            0 :             self.original_ancestor =
     349            0 :                 Some((branchpoint.0, branchpoint.1, chrono::Utc::now().naive_utc()));
     350            0 :             true
     351              :         }
     352            0 :     }
     353              : 
     354              :     /// The queried lsn is most likely the basebackup lsn, and this answers question "is it allowed
     355              :     /// to start a read/write primary at this lsn".
     356              :     ///
     357              :     /// Returns true if the Lsn was previously our branch point.
     358            0 :     pub(crate) fn is_previous_ancestor_lsn(&self, lsn: Lsn) -> bool {
     359            0 :         self.original_ancestor
     360            0 :             .is_some_and(|(_, ancestor_lsn, _)| ancestor_lsn == lsn)
     361            0 :     }
     362              : 
     363              :     /// Returns true if the timeline originally had an ancestor, and no longer has one.
     364            0 :     pub(crate) fn is_detached_from_ancestor(&self) -> bool {
     365            0 :         self.original_ancestor.is_some()
     366            0 :     }
     367              : 
     368              :     /// Returns original ancestor timeline id and lsn that this timeline has been detached from.
     369            0 :     pub(crate) fn detached_previous_ancestor(&self) -> Option<(TimelineId, Lsn)> {
     370            0 :         self.original_ancestor.map(|(id, lsn, _)| (id, lsn))
     371            0 :     }
     372              : 
     373            0 :     pub(crate) fn is_reparented(&self) -> bool {
     374            0 :         !self.reparenting_history.is_empty()
     375            0 :     }
     376              : }
     377              : 
     378            0 : #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
     379              : pub(crate) struct GcBlocking {
     380              :     pub(crate) started_at: NaiveDateTime,
     381              :     pub(crate) reasons: enumset::EnumSet<GcBlockingReason>,
     382              : }
     383              : 
     384            0 : #[derive(Debug, enumset::EnumSetType, serde::Serialize, serde::Deserialize)]
     385              : #[enumset(serialize_repr = "list")]
     386              : pub(crate) enum GcBlockingReason {
     387              :     Manual,
     388              :     DetachAncestor,
     389              : }
     390              : 
     391              : impl GcBlocking {
     392            0 :     pub(super) fn started_now_for(reason: GcBlockingReason) -> Self {
     393            0 :         GcBlocking {
     394            0 :             started_at: chrono::Utc::now().naive_utc(),
     395            0 :             reasons: enumset::EnumSet::only(reason),
     396            0 :         }
     397            0 :     }
     398              : 
     399              :     /// Returns true if the given reason is one of the reasons why the gc is blocked.
     400            0 :     pub(crate) fn blocked_by(&self, reason: GcBlockingReason) -> bool {
     401            0 :         self.reasons.contains(reason)
     402            0 :     }
     403              : 
     404              :     /// Returns a version of self with the given reason.
     405            0 :     pub(super) fn with_reason(&self, reason: GcBlockingReason) -> Self {
     406            0 :         assert!(!self.blocked_by(reason));
     407            0 :         let mut reasons = self.reasons;
     408            0 :         reasons.insert(reason);
     409              : 
     410            0 :         Self {
     411            0 :             started_at: self.started_at,
     412            0 :             reasons,
     413            0 :         }
     414            0 :     }
     415              : 
     416              :     /// Returns a version of self without the given reason. Assumption is that if
     417              :     /// there are no more reasons, we can unblock the gc by returning `None`.
     418            0 :     pub(super) fn without_reason(&self, reason: GcBlockingReason) -> Option<Self> {
     419            0 :         assert!(self.blocked_by(reason));
     420              : 
     421            0 :         if self.reasons.len() == 1 {
     422            0 :             None
     423              :         } else {
     424            0 :             let mut reasons = self.reasons;
     425            0 :             assert!(reasons.remove(reason));
     426            0 :             assert!(!reasons.is_empty());
     427              : 
     428            0 :             Some(Self {
     429            0 :                 started_at: self.started_at,
     430            0 :                 reasons,
     431            0 :             })
     432              :         }
     433            0 :     }
     434              : }
     435              : 
     436              : #[cfg(test)]
     437              : mod tests {
     438              :     use postgres_ffi::PgMajorVersion;
     439              :     use std::str::FromStr;
     440              :     use utils::id::TimelineId;
     441              : 
     442              :     use super::*;
     443              : 
     444              :     #[test]
     445            1 :     fn v1_indexpart_is_parsed() {
     446            1 :         let example = r#"{
     447            1 :             "version":1,
     448            1 :             "timeline_layers":["000000000000000000000000000000000000-FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF__0000000001696070-00000000016960E9"],
     449            1 :             "layer_metadata":{
     450            1 :                 "000000000000000000000000000000000000-FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF__0000000001696070-00000000016960E9": { "file_size": 25600000 },
     451            1 :                 "000000000000000000000000000000000000-FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF__00000000016B59D8-00000000016B5A51": { "file_size": 9007199254741001 }
     452            1 :             },
     453            1 :             "disk_consistent_lsn":"0/16960E8",
     454            1 :             "metadata_bytes":[113,11,159,210,0,54,0,4,0,0,0,0,1,105,96,232,1,0,0,0,0,1,105,96,112,0,0,0,0,0,0,0,0,0,0,0,0,0,1,105,96,112,0,0,0,0,1,105,96,112,0,0,0,14,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]
     455            1 :         }"#;
     456              : 
     457            1 :         let expected = IndexPart {
     458            1 :             // note this is not verified, could be anything, but exists for humans debugging.. could be the git version instead?
     459            1 :             version: 1,
     460            1 :             layer_metadata: HashMap::from([
     461            1 :                 ("000000000000000000000000000000000000-FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF__0000000001696070-00000000016960E9".parse().unwrap(), LayerFileMetadata {
     462            1 :                     file_size: 25600000,
     463            1 :                     generation: Generation::none(),
     464            1 :                     shard: ShardIndex::unsharded()
     465            1 :                 }),
     466            1 :                 ("000000000000000000000000000000000000-FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF__00000000016B59D8-00000000016B5A51".parse().unwrap(), LayerFileMetadata {
     467            1 :                     // serde_json should always parse this but this might be a double with jq for
     468            1 :                     // example.
     469            1 :                     file_size: 9007199254741001,
     470            1 :                     generation: Generation::none(),
     471            1 :                     shard: ShardIndex::unsharded()
     472            1 :                 })
     473            1 :             ]),
     474            1 :             disk_consistent_lsn: "0/16960E8".parse::<Lsn>().unwrap(),
     475            1 :             metadata: TimelineMetadata::from_bytes(&[113,11,159,210,0,54,0,4,0,0,0,0,1,105,96,232,1,0,0,0,0,1,105,96,112,0,0,0,0,0,0,0,0,0,0,0,0,0,1,105,96,112,0,0,0,0,1,105,96,112,0,0,0,14,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]).unwrap(),
     476            1 :             deleted_at: None,
     477            1 :             archived_at: None,
     478            1 :             lineage: Lineage::default(),
     479            1 :             gc_blocking: None,
     480            1 :             last_aux_file_policy: None,
     481            1 :             import_pgdata: None,
     482            1 :             rel_size_migration: None,
     483            1 :             l2_lsn: None,
     484            1 :             gc_compaction: None,
     485            1 :             marked_invisible_at: None,
     486            1 :             rel_size_migrated_at: None,
     487            1 :         };
     488              : 
     489            1 :         let part = IndexPart::from_json_bytes(example.as_bytes()).unwrap();
     490            1 :         assert_eq!(part, expected);
     491            1 :     }
     492              : 
     493              :     #[test]
     494            1 :     fn v1_indexpart_is_parsed_with_optional_missing_layers() {
     495            1 :         let example = r#"{
     496            1 :             "version":1,
     497            1 :             "timeline_layers":["000000000000000000000000000000000000-FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF__0000000001696070-00000000016960E9"],
     498            1 :             "missing_layers":["This shouldn't fail deserialization"],
     499            1 :             "layer_metadata":{
     500            1 :                 "000000000000000000000000000000000000-FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF__0000000001696070-00000000016960E9": { "file_size": 25600000 },
     501            1 :                 "000000000000000000000000000000000000-FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF__00000000016B59D8-00000000016B5A51": { "file_size": 9007199254741001 }
     502            1 :             },
     503            1 :             "disk_consistent_lsn":"0/16960E8",
     504            1 :             "metadata_bytes":[113,11,159,210,0,54,0,4,0,0,0,0,1,105,96,232,1,0,0,0,0,1,105,96,112,0,0,0,0,0,0,0,0,0,0,0,0,0,1,105,96,112,0,0,0,0,1,105,96,112,0,0,0,14,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]
     505            1 :         }"#;
     506              : 
     507            1 :         let expected = IndexPart {
     508            1 :             // note this is not verified, could be anything, but exists for humans debugging.. could be the git version instead?
     509            1 :             version: 1,
     510            1 :             layer_metadata: HashMap::from([
     511            1 :                 ("000000000000000000000000000000000000-FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF__0000000001696070-00000000016960E9".parse().unwrap(), LayerFileMetadata {
     512            1 :                     file_size: 25600000,
     513            1 :                     generation: Generation::none(),
     514            1 :                     shard: ShardIndex::unsharded()
     515            1 :                 }),
     516            1 :                 ("000000000000000000000000000000000000-FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF__00000000016B59D8-00000000016B5A51".parse().unwrap(), LayerFileMetadata {
     517            1 :                     // serde_json should always parse this but this might be a double with jq for
     518            1 :                     // example.
     519            1 :                     file_size: 9007199254741001,
     520            1 :                     generation: Generation::none(),
     521            1 :                     shard: ShardIndex::unsharded()
     522            1 :                 })
     523            1 :             ]),
     524            1 :             disk_consistent_lsn: "0/16960E8".parse::<Lsn>().unwrap(),
     525            1 :             metadata: TimelineMetadata::from_bytes(&[113,11,159,210,0,54,0,4,0,0,0,0,1,105,96,232,1,0,0,0,0,1,105,96,112,0,0,0,0,0,0,0,0,0,0,0,0,0,1,105,96,112,0,0,0,0,1,105,96,112,0,0,0,14,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]).unwrap(),
     526            1 :             deleted_at: None,
     527            1 :             archived_at: None,
     528            1 :             lineage: Lineage::default(),
     529            1 :             gc_blocking: None,
     530            1 :             last_aux_file_policy: None,
     531            1 :             import_pgdata: None,
     532            1 :             rel_size_migration: None,
     533            1 :             l2_lsn: None,
     534            1 :             gc_compaction: None,
     535            1 :             marked_invisible_at: None,
     536            1 :             rel_size_migrated_at: None,
     537            1 :         };
     538              : 
     539            1 :         let part = IndexPart::from_json_bytes(example.as_bytes()).unwrap();
     540            1 :         assert_eq!(part, expected);
     541            1 :     }
     542              : 
     543              :     #[test]
     544            1 :     fn v2_indexpart_is_parsed_with_deleted_at() {
     545            1 :         let example = r#"{
     546            1 :             "version":2,
     547            1 :             "timeline_layers":["000000000000000000000000000000000000-FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF__0000000001696070-00000000016960E9"],
     548            1 :             "missing_layers":["This shouldn't fail deserialization"],
     549            1 :             "layer_metadata":{
     550            1 :                 "000000000000000000000000000000000000-FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF__0000000001696070-00000000016960E9": { "file_size": 25600000 },
     551            1 :                 "000000000000000000000000000000000000-FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF__00000000016B59D8-00000000016B5A51": { "file_size": 9007199254741001 }
     552            1 :             },
     553            1 :             "disk_consistent_lsn":"0/16960E8",
     554            1 :             "metadata_bytes":[113,11,159,210,0,54,0,4,0,0,0,0,1,105,96,232,1,0,0,0,0,1,105,96,112,0,0,0,0,0,0,0,0,0,0,0,0,0,1,105,96,112,0,0,0,0,1,105,96,112,0,0,0,14,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
     555            1 :             "deleted_at": "2023-07-31T09:00:00.123"
     556            1 :         }"#;
     557              : 
     558            1 :         let expected = IndexPart {
     559            1 :             // note this is not verified, could be anything, but exists for humans debugging.. could be the git version instead?
     560            1 :             version: 2,
     561            1 :             layer_metadata: HashMap::from([
     562            1 :                 ("000000000000000000000000000000000000-FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF__0000000001696070-00000000016960E9".parse().unwrap(), LayerFileMetadata {
     563            1 :                     file_size: 25600000,
     564            1 :                     generation: Generation::none(),
     565            1 :                     shard: ShardIndex::unsharded()
     566            1 :                 }),
     567            1 :                 ("000000000000000000000000000000000000-FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF__00000000016B59D8-00000000016B5A51".parse().unwrap(), LayerFileMetadata {
     568            1 :                     // serde_json should always parse this but this might be a double with jq for
     569            1 :                     // example.
     570            1 :                     file_size: 9007199254741001,
     571            1 :                     generation: Generation::none(),
     572            1 :                     shard: ShardIndex::unsharded()
     573            1 :                 })
     574            1 :             ]),
     575            1 :             disk_consistent_lsn: "0/16960E8".parse::<Lsn>().unwrap(),
     576            1 :             metadata: TimelineMetadata::from_bytes(&[113,11,159,210,0,54,0,4,0,0,0,0,1,105,96,232,1,0,0,0,0,1,105,96,112,0,0,0,0,0,0,0,0,0,0,0,0,0,1,105,96,112,0,0,0,0,1,105,96,112,0,0,0,14,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]).unwrap(),
     577            1 :             deleted_at: Some(parse_naive_datetime("2023-07-31T09:00:00.123000000")),
     578            1 :             archived_at: None,
     579            1 :             lineage: Lineage::default(),
     580            1 :             gc_blocking: None,
     581            1 :             last_aux_file_policy: None,
     582            1 :             import_pgdata: None,
     583            1 :             rel_size_migration: None,
     584            1 :             l2_lsn: None,
     585            1 :             gc_compaction: None,
     586            1 :             marked_invisible_at: None,
     587            1 :             rel_size_migrated_at: None,
     588            1 :         };
     589              : 
     590            1 :         let part = IndexPart::from_json_bytes(example.as_bytes()).unwrap();
     591            1 :         assert_eq!(part, expected);
     592            1 :     }
     593              : 
     594              :     #[test]
     595            1 :     fn empty_layers_are_parsed() {
     596            1 :         let empty_layers_json = r#"{
     597            1 :             "version":1,
     598            1 :             "timeline_layers":[],
     599            1 :             "layer_metadata":{},
     600            1 :             "disk_consistent_lsn":"0/2532648",
     601            1 :             "metadata_bytes":[136,151,49,208,0,70,0,4,0,0,0,0,2,83,38,72,1,0,0,0,0,2,83,38,32,1,87,198,240,135,97,119,45,125,38,29,155,161,140,141,255,210,0,0,0,0,2,83,38,72,0,0,0,0,1,73,240,192,0,0,0,0,1,73,240,192,0,0,0,15,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]
     602            1 :         }"#;
     603              : 
     604            1 :         let expected = IndexPart {
     605            1 :             version: 1,
     606            1 :             layer_metadata: HashMap::new(),
     607            1 :             disk_consistent_lsn: "0/2532648".parse::<Lsn>().unwrap(),
     608            1 :             metadata: TimelineMetadata::from_bytes(&[
     609            1 :                 136, 151, 49, 208, 0, 70, 0, 4, 0, 0, 0, 0, 2, 83, 38, 72, 1, 0, 0, 0, 0, 2, 83,
     610            1 :                 38, 32, 1, 87, 198, 240, 135, 97, 119, 45, 125, 38, 29, 155, 161, 140, 141, 255,
     611            1 :                 210, 0, 0, 0, 0, 2, 83, 38, 72, 0, 0, 0, 0, 1, 73, 240, 192, 0, 0, 0, 0, 1, 73,
     612            1 :                 240, 192, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
     613            1 :                 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
     614            1 :                 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
     615            1 :                 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
     616            1 :                 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
     617            1 :                 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
     618            1 :                 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
     619            1 :                 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
     620            1 :                 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
     621            1 :                 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
     622            1 :                 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
     623            1 :                 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
     624            1 :                 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
     625            1 :                 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
     626            1 :                 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
     627            1 :                 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
     628            1 :                 0, 0,
     629            1 :             ])
     630            1 :             .unwrap(),
     631            1 :             deleted_at: None,
     632            1 :             archived_at: None,
     633            1 :             lineage: Lineage::default(),
     634            1 :             gc_blocking: None,
     635            1 :             last_aux_file_policy: None,
     636            1 :             import_pgdata: None,
     637            1 :             rel_size_migration: None,
     638            1 :             l2_lsn: None,
     639            1 :             gc_compaction: None,
     640            1 :             marked_invisible_at: None,
     641            1 :             rel_size_migrated_at: None,
     642            1 :         };
     643              : 
     644            1 :         let empty_layers_parsed = IndexPart::from_json_bytes(empty_layers_json.as_bytes()).unwrap();
     645              : 
     646            1 :         assert_eq!(empty_layers_parsed, expected);
     647            1 :     }
     648              : 
     649              :     #[test]
     650            1 :     fn v4_indexpart_is_parsed() {
     651            1 :         let example = r#"{
     652            1 :             "version":4,
     653            1 :             "layer_metadata":{
     654            1 :                 "000000000000000000000000000000000000-FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF__0000000001696070-00000000016960E9": { "file_size": 25600000 },
     655            1 :                 "000000000000000000000000000000000000-FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF__00000000016B59D8-00000000016B5A51": { "file_size": 9007199254741001 }
     656            1 :             },
     657            1 :             "disk_consistent_lsn":"0/16960E8",
     658            1 :             "metadata_bytes":[113,11,159,210,0,54,0,4,0,0,0,0,1,105,96,232,1,0,0,0,0,1,105,96,112,0,0,0,0,0,0,0,0,0,0,0,0,0,1,105,96,112,0,0,0,0,1,105,96,112,0,0,0,14,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
     659            1 :             "deleted_at": "2023-07-31T09:00:00.123"
     660            1 :         }"#;
     661              : 
     662            1 :         let expected = IndexPart {
     663            1 :             version: 4,
     664            1 :             layer_metadata: HashMap::from([
     665            1 :                 ("000000000000000000000000000000000000-FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF__0000000001696070-00000000016960E9".parse().unwrap(), LayerFileMetadata {
     666            1 :                     file_size: 25600000,
     667            1 :                     generation: Generation::none(),
     668            1 :                     shard: ShardIndex::unsharded()
     669            1 :                 }),
     670            1 :                 ("000000000000000000000000000000000000-FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF__00000000016B59D8-00000000016B5A51".parse().unwrap(), LayerFileMetadata {
     671            1 :                     // serde_json should always parse this but this might be a double with jq for
     672            1 :                     // example.
     673            1 :                     file_size: 9007199254741001,
     674            1 :                     generation: Generation::none(),
     675            1 :                     shard: ShardIndex::unsharded()
     676            1 :                 })
     677            1 :             ]),
     678            1 :             disk_consistent_lsn: "0/16960E8".parse::<Lsn>().unwrap(),
     679            1 :             metadata: TimelineMetadata::from_bytes(&[113,11,159,210,0,54,0,4,0,0,0,0,1,105,96,232,1,0,0,0,0,1,105,96,112,0,0,0,0,0,0,0,0,0,0,0,0,0,1,105,96,112,0,0,0,0,1,105,96,112,0,0,0,14,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]).unwrap(),
     680            1 :             deleted_at: Some(parse_naive_datetime("2023-07-31T09:00:00.123000000")),
     681            1 :             archived_at: None,
     682            1 :             lineage: Lineage::default(),
     683            1 :             gc_blocking: None,
     684            1 :             last_aux_file_policy: None,
     685            1 :             import_pgdata: None,
     686            1 :             rel_size_migration: None,
     687            1 :             l2_lsn: None,
     688            1 :             gc_compaction: None,
     689            1 :             marked_invisible_at: None,
     690            1 :             rel_size_migrated_at: None,
     691            1 :         };
     692              : 
     693            1 :         let part = IndexPart::from_json_bytes(example.as_bytes()).unwrap();
     694            1 :         assert_eq!(part, expected);
     695            1 :     }
     696              : 
     697              :     #[test]
     698            1 :     fn v5_indexpart_is_parsed() {
     699            1 :         let example = r#"{
     700            1 :             "version":5,
     701            1 :             "layer_metadata":{
     702            1 :                 "000000000000000000000000000000000000-FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF__00000000014EF420-00000000014EF499":{"file_size":23289856,"generation":1},
     703            1 :                 "000000000000000000000000000000000000-FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF__00000000014EF499-00000000015A7619":{"file_size":1015808,"generation":1}},
     704            1 :                 "disk_consistent_lsn":"0/15A7618",
     705            1 :                 "metadata_bytes":[226,88,25,241,0,46,0,4,0,0,0,0,1,90,118,24,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,78,244,32,0,0,0,0,1,78,244,32,0,0,0,16,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
     706            1 :                 "lineage":{
     707            1 :                     "original_ancestor":["e2bfd8c633d713d279e6fcd2bcc15b6d","0/15A7618","2024-05-07T18:52:36.322426563"],
     708            1 :                     "reparenting_history":["e1bfd8c633d713d279e6fcd2bcc15b6d"]
     709            1 :                 }
     710            1 :         }"#;
     711              : 
     712            1 :         let expected = IndexPart {
     713            1 :             version: 5,
     714            1 :             layer_metadata: HashMap::from([
     715            1 :                 ("000000000000000000000000000000000000-FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF__00000000014EF420-00000000014EF499".parse().unwrap(), LayerFileMetadata {
     716            1 :                     file_size: 23289856,
     717            1 :                     generation: Generation::new(1),
     718            1 :                     shard: ShardIndex::unsharded(),
     719            1 :                 }),
     720            1 :                 ("000000000000000000000000000000000000-FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF__00000000014EF499-00000000015A7619".parse().unwrap(), LayerFileMetadata {
     721            1 :                     file_size: 1015808,
     722            1 :                     generation: Generation::new(1),
     723            1 :                     shard: ShardIndex::unsharded(),
     724            1 :                 })
     725            1 :             ]),
     726            1 :             disk_consistent_lsn: Lsn::from_str("0/15A7618").unwrap(),
     727            1 :             metadata: TimelineMetadata::from_bytes(&[226,88,25,241,0,46,0,4,0,0,0,0,1,90,118,24,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,78,244,32,0,0,0,0,1,78,244,32,0,0,0,16,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]).unwrap(),
     728            1 :             deleted_at: None,
     729            1 :             archived_at: None,
     730            1 :             lineage: Lineage {
     731            1 :                 reparenting_history_truncated: false,
     732            1 :                 reparenting_history: vec![TimelineId::from_str("e1bfd8c633d713d279e6fcd2bcc15b6d").unwrap()],
     733            1 :                 original_ancestor: Some((TimelineId::from_str("e2bfd8c633d713d279e6fcd2bcc15b6d").unwrap(), Lsn::from_str("0/15A7618").unwrap(), parse_naive_datetime("2024-05-07T18:52:36.322426563"))),
     734            1 :             },
     735            1 :             gc_blocking: None,
     736            1 :             last_aux_file_policy: None,
     737            1 :             import_pgdata: None,
     738            1 :             rel_size_migration: None,
     739            1 :             l2_lsn: None,
     740            1 :             gc_compaction: None,
     741            1 :             marked_invisible_at: None,
     742            1 :             rel_size_migrated_at: None,
     743            1 :         };
     744              : 
     745            1 :         let part = IndexPart::from_json_bytes(example.as_bytes()).unwrap();
     746            1 :         assert_eq!(part, expected);
     747            1 :     }
     748              : 
     749              :     #[test]
     750            1 :     fn v6_indexpart_is_parsed() {
     751            1 :         let example = r#"{
     752            1 :             "version":6,
     753            1 :             "layer_metadata":{
     754            1 :                 "000000000000000000000000000000000000-FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF__0000000001696070-00000000016960E9": { "file_size": 25600000 },
     755            1 :                 "000000000000000000000000000000000000-FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF__00000000016B59D8-00000000016B5A51": { "file_size": 9007199254741001 }
     756            1 :             },
     757            1 :             "disk_consistent_lsn":"0/16960E8",
     758            1 :             "metadata_bytes":[113,11,159,210,0,54,0,4,0,0,0,0,1,105,96,232,1,0,0,0,0,1,105,96,112,0,0,0,0,0,0,0,0,0,0,0,0,0,1,105,96,112,0,0,0,0,1,105,96,112,0,0,0,14,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
     759            1 :             "deleted_at": "2023-07-31T09:00:00.123",
     760            1 :             "lineage":{
     761            1 :                 "original_ancestor":["e2bfd8c633d713d279e6fcd2bcc15b6d","0/15A7618","2024-05-07T18:52:36.322426563"],
     762            1 :                 "reparenting_history":["e1bfd8c633d713d279e6fcd2bcc15b6d"]
     763            1 :             },
     764            1 :             "last_aux_file_policy": "V2"
     765            1 :         }"#;
     766              : 
     767            1 :         let expected = IndexPart {
     768            1 :             version: 6,
     769            1 :             layer_metadata: HashMap::from([
     770            1 :                 ("000000000000000000000000000000000000-FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF__0000000001696070-00000000016960E9".parse().unwrap(), LayerFileMetadata {
     771            1 :                     file_size: 25600000,
     772            1 :                     generation: Generation::none(),
     773            1 :                     shard: ShardIndex::unsharded()
     774            1 :                 }),
     775            1 :                 ("000000000000000000000000000000000000-FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF__00000000016B59D8-00000000016B5A51".parse().unwrap(), LayerFileMetadata {
     776            1 :                     // serde_json should always parse this but this might be a double with jq for
     777            1 :                     // example.
     778            1 :                     file_size: 9007199254741001,
     779            1 :                     generation: Generation::none(),
     780            1 :                     shard: ShardIndex::unsharded()
     781            1 :                 })
     782            1 :             ]),
     783            1 :             disk_consistent_lsn: "0/16960E8".parse::<Lsn>().unwrap(),
     784            1 :             metadata: TimelineMetadata::from_bytes(&[113,11,159,210,0,54,0,4,0,0,0,0,1,105,96,232,1,0,0,0,0,1,105,96,112,0,0,0,0,0,0,0,0,0,0,0,0,0,1,105,96,112,0,0,0,0,1,105,96,112,0,0,0,14,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]).unwrap(),
     785            1 :             deleted_at: Some(parse_naive_datetime("2023-07-31T09:00:00.123000000")),
     786            1 :             archived_at: None,
     787            1 :             lineage: Lineage {
     788            1 :                 reparenting_history_truncated: false,
     789            1 :                 reparenting_history: vec![TimelineId::from_str("e1bfd8c633d713d279e6fcd2bcc15b6d").unwrap()],
     790            1 :                 original_ancestor: Some((TimelineId::from_str("e2bfd8c633d713d279e6fcd2bcc15b6d").unwrap(), Lsn::from_str("0/15A7618").unwrap(), parse_naive_datetime("2024-05-07T18:52:36.322426563"))),
     791            1 :             },
     792            1 :             gc_blocking: None,
     793            1 :             last_aux_file_policy: Some(AuxFilePolicy::V2),
     794            1 :             import_pgdata: None,
     795            1 :             rel_size_migration: None,
     796            1 :             l2_lsn: None,
     797            1 :             gc_compaction: None,
     798            1 :             marked_invisible_at: None,
     799            1 :             rel_size_migrated_at: None,
     800            1 :         };
     801              : 
     802            1 :         let part = IndexPart::from_json_bytes(example.as_bytes()).unwrap();
     803            1 :         assert_eq!(part, expected);
     804            1 :     }
     805              : 
     806              :     #[test]
     807            1 :     fn v7_indexpart_is_parsed() {
     808            1 :         let example = r#"{
     809            1 :             "version": 7,
     810            1 :             "layer_metadata":{
     811            1 :                 "000000000000000000000000000000000000-FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF__0000000001696070-00000000016960E9": { "file_size": 25600000 },
     812            1 :                 "000000000000000000000000000000000000-FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF__00000000016B59D8-00000000016B5A51": { "file_size": 9007199254741001 }
     813            1 :             },
     814            1 :             "disk_consistent_lsn":"0/16960E8",
     815            1 :             "metadata": {
     816            1 :                 "disk_consistent_lsn": "0/16960E8",
     817            1 :                 "prev_record_lsn": "0/1696070",
     818            1 :                 "ancestor_timeline": "e45a7f37d3ee2ff17dc14bf4f4e3f52e",
     819            1 :                 "ancestor_lsn": "0/0",
     820            1 :                 "latest_gc_cutoff_lsn": "0/1696070",
     821            1 :                 "initdb_lsn": "0/1696070",
     822            1 :                 "pg_version": 14
     823            1 :             },
     824            1 :             "deleted_at": "2023-07-31T09:00:00.123"
     825            1 :         }"#;
     826              : 
     827            1 :         let expected = IndexPart {
     828            1 :             version: 7,
     829            1 :             layer_metadata: HashMap::from([
     830            1 :                 ("000000000000000000000000000000000000-FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF__0000000001696070-00000000016960E9".parse().unwrap(), LayerFileMetadata {
     831            1 :                     file_size: 25600000,
     832            1 :                     generation: Generation::none(),
     833            1 :                     shard: ShardIndex::unsharded()
     834            1 :                 }),
     835            1 :                 ("000000000000000000000000000000000000-FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF__00000000016B59D8-00000000016B5A51".parse().unwrap(), LayerFileMetadata {
     836            1 :                     file_size: 9007199254741001,
     837            1 :                     generation: Generation::none(),
     838            1 :                     shard: ShardIndex::unsharded()
     839            1 :                 })
     840            1 :             ]),
     841            1 :             disk_consistent_lsn: "0/16960E8".parse::<Lsn>().unwrap(),
     842            1 :             metadata: TimelineMetadata::new(
     843            1 :                 Lsn::from_str("0/16960E8").unwrap(),
     844            1 :                 Some(Lsn::from_str("0/1696070").unwrap()),
     845            1 :                 Some(TimelineId::from_str("e45a7f37d3ee2ff17dc14bf4f4e3f52e").unwrap()),
     846            1 :                 Lsn::INVALID,
     847            1 :                 Lsn::from_str("0/1696070").unwrap(),
     848            1 :                 Lsn::from_str("0/1696070").unwrap(),
     849            1 :                 PgMajorVersion::PG14,
     850            1 :             ).with_recalculated_checksum().unwrap(),
     851            1 :             deleted_at: Some(parse_naive_datetime("2023-07-31T09:00:00.123000000")),
     852            1 :             archived_at: None,
     853            1 :             lineage: Default::default(),
     854            1 :             gc_blocking: None,
     855            1 :             last_aux_file_policy: Default::default(),
     856            1 :             import_pgdata: None,
     857            1 :             rel_size_migration: None,
     858            1 :             l2_lsn: None,
     859            1 :             gc_compaction: None,
     860            1 :             marked_invisible_at: None,
     861            1 :             rel_size_migrated_at: None,
     862            1 :         };
     863              : 
     864            1 :         let part = IndexPart::from_json_bytes(example.as_bytes()).unwrap();
     865            1 :         assert_eq!(part, expected);
     866            1 :     }
     867              : 
     868              :     #[test]
     869            1 :     fn v8_indexpart_is_parsed() {
     870            1 :         let example = r#"{
     871            1 :             "version": 8,
     872            1 :             "layer_metadata":{
     873            1 :                 "000000000000000000000000000000000000-FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF__0000000001696070-00000000016960E9": { "file_size": 25600000 },
     874            1 :                 "000000000000000000000000000000000000-FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF__00000000016B59D8-00000000016B5A51": { "file_size": 9007199254741001 }
     875            1 :             },
     876            1 :             "disk_consistent_lsn":"0/16960E8",
     877            1 :             "metadata": {
     878            1 :                 "disk_consistent_lsn": "0/16960E8",
     879            1 :                 "prev_record_lsn": "0/1696070",
     880            1 :                 "ancestor_timeline": "e45a7f37d3ee2ff17dc14bf4f4e3f52e",
     881            1 :                 "ancestor_lsn": "0/0",
     882            1 :                 "latest_gc_cutoff_lsn": "0/1696070",
     883            1 :                 "initdb_lsn": "0/1696070",
     884            1 :                 "pg_version": 14
     885            1 :             },
     886            1 :             "deleted_at": "2023-07-31T09:00:00.123",
     887            1 :             "archived_at": "2023-04-29T09:00:00.123"
     888            1 :         }"#;
     889              : 
     890            1 :         let expected = IndexPart {
     891            1 :             version: 8,
     892            1 :             layer_metadata: HashMap::from([
     893            1 :                 ("000000000000000000000000000000000000-FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF__0000000001696070-00000000016960E9".parse().unwrap(), LayerFileMetadata {
     894            1 :                     file_size: 25600000,
     895            1 :                     generation: Generation::none(),
     896            1 :                     shard: ShardIndex::unsharded()
     897            1 :                 }),
     898            1 :                 ("000000000000000000000000000000000000-FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF__00000000016B59D8-00000000016B5A51".parse().unwrap(), LayerFileMetadata {
     899            1 :                     file_size: 9007199254741001,
     900            1 :                     generation: Generation::none(),
     901            1 :                     shard: ShardIndex::unsharded()
     902            1 :                 })
     903            1 :             ]),
     904            1 :             disk_consistent_lsn: "0/16960E8".parse::<Lsn>().unwrap(),
     905            1 :             metadata: TimelineMetadata::new(
     906            1 :                 Lsn::from_str("0/16960E8").unwrap(),
     907            1 :                 Some(Lsn::from_str("0/1696070").unwrap()),
     908            1 :                 Some(TimelineId::from_str("e45a7f37d3ee2ff17dc14bf4f4e3f52e").unwrap()),
     909            1 :                 Lsn::INVALID,
     910            1 :                 Lsn::from_str("0/1696070").unwrap(),
     911            1 :                 Lsn::from_str("0/1696070").unwrap(),
     912            1 :                 PgMajorVersion::PG14,
     913            1 :             ).with_recalculated_checksum().unwrap(),
     914            1 :             deleted_at: Some(parse_naive_datetime("2023-07-31T09:00:00.123000000")),
     915            1 :             archived_at: Some(parse_naive_datetime("2023-04-29T09:00:00.123000000")),
     916            1 :             lineage: Default::default(),
     917            1 :             gc_blocking: None,
     918            1 :             last_aux_file_policy: Default::default(),
     919            1 :             import_pgdata: None,
     920            1 :             rel_size_migration: None,
     921            1 :             l2_lsn: None,
     922            1 :             gc_compaction: None,
     923            1 :             marked_invisible_at: None,
     924            1 :             rel_size_migrated_at: None,
     925            1 :         };
     926              : 
     927            1 :         let part = IndexPart::from_json_bytes(example.as_bytes()).unwrap();
     928            1 :         assert_eq!(part, expected);
     929            1 :     }
     930              : 
     931              :     #[test]
     932            1 :     fn v9_indexpart_is_parsed() {
     933            1 :         let example = r#"{
     934            1 :             "version": 9,
     935            1 :             "layer_metadata":{
     936            1 :                 "000000000000000000000000000000000000-FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF__0000000001696070-00000000016960E9": { "file_size": 25600000 },
     937            1 :                 "000000000000000000000000000000000000-FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF__00000000016B59D8-00000000016B5A51": { "file_size": 9007199254741001 }
     938            1 :             },
     939            1 :             "disk_consistent_lsn":"0/16960E8",
     940            1 :             "metadata": {
     941            1 :                 "disk_consistent_lsn": "0/16960E8",
     942            1 :                 "prev_record_lsn": "0/1696070",
     943            1 :                 "ancestor_timeline": "e45a7f37d3ee2ff17dc14bf4f4e3f52e",
     944            1 :                 "ancestor_lsn": "0/0",
     945            1 :                 "latest_gc_cutoff_lsn": "0/1696070",
     946            1 :                 "initdb_lsn": "0/1696070",
     947            1 :                 "pg_version": 14
     948            1 :             },
     949            1 :             "gc_blocking": {
     950            1 :                 "started_at": "2024-07-19T09:00:00.123",
     951            1 :                 "reasons": ["DetachAncestor"]
     952            1 :             }
     953            1 :         }"#;
     954              : 
     955            1 :         let expected = IndexPart {
     956            1 :             version: 9,
     957            1 :             layer_metadata: HashMap::from([
     958            1 :                 ("000000000000000000000000000000000000-FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF__0000000001696070-00000000016960E9".parse().unwrap(), LayerFileMetadata {
     959            1 :                     file_size: 25600000,
     960            1 :                     generation: Generation::none(),
     961            1 :                     shard: ShardIndex::unsharded()
     962            1 :                 }),
     963            1 :                 ("000000000000000000000000000000000000-FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF__00000000016B59D8-00000000016B5A51".parse().unwrap(), LayerFileMetadata {
     964            1 :                     file_size: 9007199254741001,
     965            1 :                     generation: Generation::none(),
     966            1 :                     shard: ShardIndex::unsharded()
     967            1 :                 })
     968            1 :             ]),
     969            1 :             disk_consistent_lsn: "0/16960E8".parse::<Lsn>().unwrap(),
     970            1 :             metadata: TimelineMetadata::new(
     971            1 :                 Lsn::from_str("0/16960E8").unwrap(),
     972            1 :                 Some(Lsn::from_str("0/1696070").unwrap()),
     973            1 :                 Some(TimelineId::from_str("e45a7f37d3ee2ff17dc14bf4f4e3f52e").unwrap()),
     974            1 :                 Lsn::INVALID,
     975            1 :                 Lsn::from_str("0/1696070").unwrap(),
     976            1 :                 Lsn::from_str("0/1696070").unwrap(),
     977            1 :                 PgMajorVersion::PG14,
     978            1 :             ).with_recalculated_checksum().unwrap(),
     979            1 :             deleted_at: None,
     980            1 :             lineage: Default::default(),
     981            1 :             gc_blocking: Some(GcBlocking {
     982            1 :                 started_at: parse_naive_datetime("2024-07-19T09:00:00.123000000"),
     983            1 :                 reasons: enumset::EnumSet::from_iter([GcBlockingReason::DetachAncestor]),
     984            1 :             }),
     985            1 :             last_aux_file_policy: Default::default(),
     986            1 :             archived_at: None,
     987            1 :             import_pgdata: None,
     988            1 :             rel_size_migration: None,
     989            1 :             l2_lsn: None,
     990            1 :             gc_compaction: None,
     991            1 :             marked_invisible_at: None,
     992            1 :             rel_size_migrated_at: None,
     993            1 :         };
     994              : 
     995            1 :         let part = IndexPart::from_json_bytes(example.as_bytes()).unwrap();
     996            1 :         assert_eq!(part, expected);
     997            1 :     }
     998              : 
     999              :     #[test]
    1000            1 :     fn v10_importpgdata_is_parsed() {
    1001            1 :         let example = r#"{
    1002            1 :             "version": 10,
    1003            1 :             "layer_metadata":{
    1004            1 :                 "000000000000000000000000000000000000-FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF__0000000001696070-00000000016960E9": { "file_size": 25600000 },
    1005            1 :                 "000000000000000000000000000000000000-FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF__00000000016B59D8-00000000016B5A51": { "file_size": 9007199254741001 }
    1006            1 :             },
    1007            1 :             "disk_consistent_lsn":"0/16960E8",
    1008            1 :             "metadata": {
    1009            1 :                 "disk_consistent_lsn": "0/16960E8",
    1010            1 :                 "prev_record_lsn": "0/1696070",
    1011            1 :                 "ancestor_timeline": "e45a7f37d3ee2ff17dc14bf4f4e3f52e",
    1012            1 :                 "ancestor_lsn": "0/0",
    1013            1 :                 "latest_gc_cutoff_lsn": "0/1696070",
    1014            1 :                 "initdb_lsn": "0/1696070",
    1015            1 :                 "pg_version": 14
    1016            1 :             },
    1017            1 :             "gc_blocking": {
    1018            1 :                 "started_at": "2024-07-19T09:00:00.123",
    1019            1 :                 "reasons": ["DetachAncestor"]
    1020            1 :             },
    1021            1 :             "import_pgdata": {
    1022            1 :                 "V1": {
    1023            1 :                     "Done": {
    1024            1 :                         "idempotency_key": "specified-by-client-218a5213-5044-4562-a28d-d024c5f057f5",
    1025            1 :                         "started_at": "2024-11-13T09:23:42.123",
    1026            1 :                         "finished_at": "2024-11-13T09:42:23.123"
    1027            1 :                     }
    1028            1 :                 }
    1029            1 :             }
    1030            1 :         }"#;
    1031              : 
    1032            1 :         let expected = IndexPart {
    1033            1 :             version: 10,
    1034            1 :             layer_metadata: HashMap::from([
    1035            1 :                 ("000000000000000000000000000000000000-FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF__0000000001696070-00000000016960E9".parse().unwrap(), LayerFileMetadata {
    1036            1 :                     file_size: 25600000,
    1037            1 :                     generation: Generation::none(),
    1038            1 :                     shard: ShardIndex::unsharded()
    1039            1 :                 }),
    1040            1 :                 ("000000000000000000000000000000000000-FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF__00000000016B59D8-00000000016B5A51".parse().unwrap(), LayerFileMetadata {
    1041            1 :                     file_size: 9007199254741001,
    1042            1 :                     generation: Generation::none(),
    1043            1 :                     shard: ShardIndex::unsharded()
    1044            1 :                 })
    1045            1 :             ]),
    1046            1 :             disk_consistent_lsn: "0/16960E8".parse::<Lsn>().unwrap(),
    1047            1 :             metadata: TimelineMetadata::new(
    1048            1 :                 Lsn::from_str("0/16960E8").unwrap(),
    1049            1 :                 Some(Lsn::from_str("0/1696070").unwrap()),
    1050            1 :                 Some(TimelineId::from_str("e45a7f37d3ee2ff17dc14bf4f4e3f52e").unwrap()),
    1051            1 :                 Lsn::INVALID,
    1052            1 :                 Lsn::from_str("0/1696070").unwrap(),
    1053            1 :                 Lsn::from_str("0/1696070").unwrap(),
    1054            1 :                 PgMajorVersion::PG14,
    1055            1 :             ).with_recalculated_checksum().unwrap(),
    1056            1 :             deleted_at: None,
    1057            1 :             lineage: Default::default(),
    1058            1 :             gc_blocking: Some(GcBlocking {
    1059            1 :                 started_at: parse_naive_datetime("2024-07-19T09:00:00.123000000"),
    1060            1 :                 reasons: enumset::EnumSet::from_iter([GcBlockingReason::DetachAncestor]),
    1061            1 :             }),
    1062            1 :             last_aux_file_policy: Default::default(),
    1063            1 :             archived_at: None,
    1064            1 :             import_pgdata: Some(import_pgdata::index_part_format::Root::V1(import_pgdata::index_part_format::V1::Done(import_pgdata::index_part_format::Done{
    1065            1 :                 started_at: parse_naive_datetime("2024-11-13T09:23:42.123000000"),
    1066            1 :                 finished_at: parse_naive_datetime("2024-11-13T09:42:23.123000000"),
    1067            1 :                 idempotency_key: import_pgdata::index_part_format::IdempotencyKey::new("specified-by-client-218a5213-5044-4562-a28d-d024c5f057f5".to_string()),
    1068            1 :             }))),
    1069            1 :             rel_size_migration: None,
    1070            1 :             l2_lsn: None,
    1071            1 :             gc_compaction: None,
    1072            1 :             marked_invisible_at: None,
    1073            1 :             rel_size_migrated_at: None,
    1074            1 :         };
    1075              : 
    1076            1 :         let part = IndexPart::from_json_bytes(example.as_bytes()).unwrap();
    1077            1 :         assert_eq!(part, expected);
    1078            1 :     }
    1079              : 
    1080              :     #[test]
    1081            1 :     fn v11_rel_size_migration_is_parsed() {
    1082            1 :         let example = r#"{
    1083            1 :             "version": 11,
    1084            1 :             "layer_metadata":{
    1085            1 :                 "000000000000000000000000000000000000-FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF__0000000001696070-00000000016960E9": { "file_size": 25600000 },
    1086            1 :                 "000000000000000000000000000000000000-FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF__00000000016B59D8-00000000016B5A51": { "file_size": 9007199254741001 }
    1087            1 :             },
    1088            1 :             "disk_consistent_lsn":"0/16960E8",
    1089            1 :             "metadata": {
    1090            1 :                 "disk_consistent_lsn": "0/16960E8",
    1091            1 :                 "prev_record_lsn": "0/1696070",
    1092            1 :                 "ancestor_timeline": "e45a7f37d3ee2ff17dc14bf4f4e3f52e",
    1093            1 :                 "ancestor_lsn": "0/0",
    1094            1 :                 "latest_gc_cutoff_lsn": "0/1696070",
    1095            1 :                 "initdb_lsn": "0/1696070",
    1096            1 :                 "pg_version": 14
    1097            1 :             },
    1098            1 :             "gc_blocking": {
    1099            1 :                 "started_at": "2024-07-19T09:00:00.123",
    1100            1 :                 "reasons": ["DetachAncestor"]
    1101            1 :             },
    1102            1 :             "import_pgdata": {
    1103            1 :                 "V1": {
    1104            1 :                     "Done": {
    1105            1 :                         "idempotency_key": "specified-by-client-218a5213-5044-4562-a28d-d024c5f057f5",
    1106            1 :                         "started_at": "2024-11-13T09:23:42.123",
    1107            1 :                         "finished_at": "2024-11-13T09:42:23.123"
    1108            1 :                     }
    1109            1 :                 }
    1110            1 :             },
    1111            1 :             "rel_size_migration": "legacy"
    1112            1 :         }"#;
    1113              : 
    1114            1 :         let expected = IndexPart {
    1115            1 :             version: 11,
    1116            1 :             layer_metadata: HashMap::from([
    1117            1 :                 ("000000000000000000000000000000000000-FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF__0000000001696070-00000000016960E9".parse().unwrap(), LayerFileMetadata {
    1118            1 :                     file_size: 25600000,
    1119            1 :                     generation: Generation::none(),
    1120            1 :                     shard: ShardIndex::unsharded()
    1121            1 :                 }),
    1122            1 :                 ("000000000000000000000000000000000000-FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF__00000000016B59D8-00000000016B5A51".parse().unwrap(), LayerFileMetadata {
    1123            1 :                     file_size: 9007199254741001,
    1124            1 :                     generation: Generation::none(),
    1125            1 :                     shard: ShardIndex::unsharded()
    1126            1 :                 })
    1127            1 :             ]),
    1128            1 :             disk_consistent_lsn: "0/16960E8".parse::<Lsn>().unwrap(),
    1129            1 :             metadata: TimelineMetadata::new(
    1130            1 :                 Lsn::from_str("0/16960E8").unwrap(),
    1131            1 :                 Some(Lsn::from_str("0/1696070").unwrap()),
    1132            1 :                 Some(TimelineId::from_str("e45a7f37d3ee2ff17dc14bf4f4e3f52e").unwrap()),
    1133            1 :                 Lsn::INVALID,
    1134            1 :                 Lsn::from_str("0/1696070").unwrap(),
    1135            1 :                 Lsn::from_str("0/1696070").unwrap(),
    1136            1 :                 PgMajorVersion::PG14,
    1137            1 :             ).with_recalculated_checksum().unwrap(),
    1138            1 :             deleted_at: None,
    1139            1 :             lineage: Default::default(),
    1140            1 :             gc_blocking: Some(GcBlocking {
    1141            1 :                 started_at: parse_naive_datetime("2024-07-19T09:00:00.123000000"),
    1142            1 :                 reasons: enumset::EnumSet::from_iter([GcBlockingReason::DetachAncestor]),
    1143            1 :             }),
    1144            1 :             last_aux_file_policy: Default::default(),
    1145            1 :             archived_at: None,
    1146            1 :             import_pgdata: Some(import_pgdata::index_part_format::Root::V1(import_pgdata::index_part_format::V1::Done(import_pgdata::index_part_format::Done{
    1147            1 :                 started_at: parse_naive_datetime("2024-11-13T09:23:42.123000000"),
    1148            1 :                 finished_at: parse_naive_datetime("2024-11-13T09:42:23.123000000"),
    1149            1 :                 idempotency_key: import_pgdata::index_part_format::IdempotencyKey::new("specified-by-client-218a5213-5044-4562-a28d-d024c5f057f5".to_string()),
    1150            1 :             }))),
    1151            1 :             rel_size_migration: Some(RelSizeMigration::Legacy),
    1152            1 :             l2_lsn: None,
    1153            1 :             gc_compaction: None,
    1154            1 :             marked_invisible_at: None,
    1155            1 :             rel_size_migrated_at: None,
    1156            1 :         };
    1157              : 
    1158            1 :         let part = IndexPart::from_json_bytes(example.as_bytes()).unwrap();
    1159            1 :         assert_eq!(part, expected);
    1160            1 :     }
    1161              : 
    1162              :     #[test]
    1163            1 :     fn v12_v13_l2_gc_ompaction_is_parsed() {
    1164            1 :         let example = r#"{
    1165            1 :             "version": 13,
    1166            1 :             "layer_metadata":{
    1167            1 :                 "000000000000000000000000000000000000-FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF__0000000001696070-00000000016960E9": { "file_size": 25600000 },
    1168            1 :                 "000000000000000000000000000000000000-FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF__00000000016B59D8-00000000016B5A51": { "file_size": 9007199254741001 }
    1169            1 :             },
    1170            1 :             "disk_consistent_lsn":"0/16960E8",
    1171            1 :             "metadata": {
    1172            1 :                 "disk_consistent_lsn": "0/16960E8",
    1173            1 :                 "prev_record_lsn": "0/1696070",
    1174            1 :                 "ancestor_timeline": "e45a7f37d3ee2ff17dc14bf4f4e3f52e",
    1175            1 :                 "ancestor_lsn": "0/0",
    1176            1 :                 "latest_gc_cutoff_lsn": "0/1696070",
    1177            1 :                 "initdb_lsn": "0/1696070",
    1178            1 :                 "pg_version": 14
    1179            1 :             },
    1180            1 :             "gc_blocking": {
    1181            1 :                 "started_at": "2024-07-19T09:00:00.123",
    1182            1 :                 "reasons": ["DetachAncestor"]
    1183            1 :             },
    1184            1 :             "import_pgdata": {
    1185            1 :                 "V1": {
    1186            1 :                     "Done": {
    1187            1 :                         "idempotency_key": "specified-by-client-218a5213-5044-4562-a28d-d024c5f057f5",
    1188            1 :                         "started_at": "2024-11-13T09:23:42.123",
    1189            1 :                         "finished_at": "2024-11-13T09:42:23.123"
    1190            1 :                     }
    1191            1 :                 }
    1192            1 :             },
    1193            1 :             "rel_size_migration": "legacy",
    1194            1 :             "l2_lsn": "0/16960E8",
    1195            1 :             "gc_compaction": {
    1196            1 :                 "last_completed_lsn": "0/16960E8"
    1197            1 :             }
    1198            1 :         }"#;
    1199              : 
    1200            1 :         let expected = IndexPart {
    1201            1 :             version: 13,
    1202            1 :             layer_metadata: HashMap::from([
    1203            1 :                 ("000000000000000000000000000000000000-FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF__0000000001696070-00000000016960E9".parse().unwrap(), LayerFileMetadata {
    1204            1 :                     file_size: 25600000,
    1205            1 :                     generation: Generation::none(),
    1206            1 :                     shard: ShardIndex::unsharded()
    1207            1 :                 }),
    1208            1 :                 ("000000000000000000000000000000000000-FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF__00000000016B59D8-00000000016B5A51".parse().unwrap(), LayerFileMetadata {
    1209            1 :                     file_size: 9007199254741001,
    1210            1 :                     generation: Generation::none(),
    1211            1 :                     shard: ShardIndex::unsharded()
    1212            1 :                 })
    1213            1 :             ]),
    1214            1 :             disk_consistent_lsn: "0/16960E8".parse::<Lsn>().unwrap(),
    1215            1 :             metadata: TimelineMetadata::new(
    1216            1 :                 Lsn::from_str("0/16960E8").unwrap(),
    1217            1 :                 Some(Lsn::from_str("0/1696070").unwrap()),
    1218            1 :                 Some(TimelineId::from_str("e45a7f37d3ee2ff17dc14bf4f4e3f52e").unwrap()),
    1219            1 :                 Lsn::INVALID,
    1220            1 :                 Lsn::from_str("0/1696070").unwrap(),
    1221            1 :                 Lsn::from_str("0/1696070").unwrap(),
    1222            1 :                 PgMajorVersion::PG14,
    1223            1 :             ).with_recalculated_checksum().unwrap(),
    1224            1 :             deleted_at: None,
    1225            1 :             lineage: Default::default(),
    1226            1 :             gc_blocking: Some(GcBlocking {
    1227            1 :                 started_at: parse_naive_datetime("2024-07-19T09:00:00.123000000"),
    1228            1 :                 reasons: enumset::EnumSet::from_iter([GcBlockingReason::DetachAncestor]),
    1229            1 :             }),
    1230            1 :             last_aux_file_policy: Default::default(),
    1231            1 :             archived_at: None,
    1232            1 :             import_pgdata: Some(import_pgdata::index_part_format::Root::V1(import_pgdata::index_part_format::V1::Done(import_pgdata::index_part_format::Done{
    1233            1 :                 started_at: parse_naive_datetime("2024-11-13T09:23:42.123000000"),
    1234            1 :                 finished_at: parse_naive_datetime("2024-11-13T09:42:23.123000000"),
    1235            1 :                 idempotency_key: import_pgdata::index_part_format::IdempotencyKey::new("specified-by-client-218a5213-5044-4562-a28d-d024c5f057f5".to_string()),
    1236            1 :             }))),
    1237            1 :             rel_size_migration: Some(RelSizeMigration::Legacy),
    1238            1 :             l2_lsn: Some("0/16960E8".parse::<Lsn>().unwrap()),
    1239            1 :             gc_compaction: Some(GcCompactionState {
    1240            1 :                 last_completed_lsn: "0/16960E8".parse::<Lsn>().unwrap(),
    1241            1 :             }),
    1242            1 :             marked_invisible_at: None,
    1243            1 :             rel_size_migrated_at: None,
    1244            1 :         };
    1245              : 
    1246            1 :         let part = IndexPart::from_json_bytes(example.as_bytes()).unwrap();
    1247            1 :         assert_eq!(part, expected);
    1248            1 :     }
    1249              : 
    1250              :     #[test]
    1251            1 :     fn v14_marked_invisible_at_is_parsed() {
    1252            1 :         let example = r#"{
    1253            1 :             "version": 14,
    1254            1 :             "layer_metadata":{
    1255            1 :                 "000000000000000000000000000000000000-FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF__0000000001696070-00000000016960E9": { "file_size": 25600000 },
    1256            1 :                 "000000000000000000000000000000000000-FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF__00000000016B59D8-00000000016B5A51": { "file_size": 9007199254741001 }
    1257            1 :             },
    1258            1 :             "disk_consistent_lsn":"0/16960E8",
    1259            1 :             "metadata": {
    1260            1 :                 "disk_consistent_lsn": "0/16960E8",
    1261            1 :                 "prev_record_lsn": "0/1696070",
    1262            1 :                 "ancestor_timeline": "e45a7f37d3ee2ff17dc14bf4f4e3f52e",
    1263            1 :                 "ancestor_lsn": "0/0",
    1264            1 :                 "latest_gc_cutoff_lsn": "0/1696070",
    1265            1 :                 "initdb_lsn": "0/1696070",
    1266            1 :                 "pg_version": 14
    1267            1 :             },
    1268            1 :             "gc_blocking": {
    1269            1 :                 "started_at": "2024-07-19T09:00:00.123",
    1270            1 :                 "reasons": ["DetachAncestor"]
    1271            1 :             },
    1272            1 :             "import_pgdata": {
    1273            1 :                 "V1": {
    1274            1 :                     "Done": {
    1275            1 :                         "idempotency_key": "specified-by-client-218a5213-5044-4562-a28d-d024c5f057f5",
    1276            1 :                         "started_at": "2024-11-13T09:23:42.123",
    1277            1 :                         "finished_at": "2024-11-13T09:42:23.123"
    1278            1 :                     }
    1279            1 :                 }
    1280            1 :             },
    1281            1 :             "rel_size_migration": "legacy",
    1282            1 :             "l2_lsn": "0/16960E8",
    1283            1 :             "gc_compaction": {
    1284            1 :                 "last_completed_lsn": "0/16960E8"
    1285            1 :             },
    1286            1 :             "marked_invisible_at": "2023-07-31T09:00:00.123"
    1287            1 :         }"#;
    1288              : 
    1289            1 :         let expected = IndexPart {
    1290            1 :             version: 14,
    1291            1 :             layer_metadata: HashMap::from([
    1292            1 :                 ("000000000000000000000000000000000000-FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF__0000000001696070-00000000016960E9".parse().unwrap(), LayerFileMetadata {
    1293            1 :                     file_size: 25600000,
    1294            1 :                     generation: Generation::none(),
    1295            1 :                     shard: ShardIndex::unsharded()
    1296            1 :                 }),
    1297            1 :                 ("000000000000000000000000000000000000-FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF__00000000016B59D8-00000000016B5A51".parse().unwrap(), LayerFileMetadata {
    1298            1 :                     file_size: 9007199254741001,
    1299            1 :                     generation: Generation::none(),
    1300            1 :                     shard: ShardIndex::unsharded()
    1301            1 :                 })
    1302            1 :             ]),
    1303            1 :             disk_consistent_lsn: "0/16960E8".parse::<Lsn>().unwrap(),
    1304            1 :             metadata: TimelineMetadata::new(
    1305            1 :                 Lsn::from_str("0/16960E8").unwrap(),
    1306            1 :                 Some(Lsn::from_str("0/1696070").unwrap()),
    1307            1 :                 Some(TimelineId::from_str("e45a7f37d3ee2ff17dc14bf4f4e3f52e").unwrap()),
    1308            1 :                 Lsn::INVALID,
    1309            1 :                 Lsn::from_str("0/1696070").unwrap(),
    1310            1 :                 Lsn::from_str("0/1696070").unwrap(),
    1311            1 :                 PgMajorVersion::PG14,
    1312            1 :             ).with_recalculated_checksum().unwrap(),
    1313            1 :             deleted_at: None,
    1314            1 :             lineage: Default::default(),
    1315            1 :             gc_blocking: Some(GcBlocking {
    1316            1 :                 started_at: parse_naive_datetime("2024-07-19T09:00:00.123000000"),
    1317            1 :                 reasons: enumset::EnumSet::from_iter([GcBlockingReason::DetachAncestor]),
    1318            1 :             }),
    1319            1 :             last_aux_file_policy: Default::default(),
    1320            1 :             archived_at: None,
    1321            1 :             import_pgdata: Some(import_pgdata::index_part_format::Root::V1(import_pgdata::index_part_format::V1::Done(import_pgdata::index_part_format::Done{
    1322            1 :                 started_at: parse_naive_datetime("2024-11-13T09:23:42.123000000"),
    1323            1 :                 finished_at: parse_naive_datetime("2024-11-13T09:42:23.123000000"),
    1324            1 :                 idempotency_key: import_pgdata::index_part_format::IdempotencyKey::new("specified-by-client-218a5213-5044-4562-a28d-d024c5f057f5".to_string()),
    1325            1 :             }))),
    1326            1 :             rel_size_migration: Some(RelSizeMigration::Legacy),
    1327            1 :             l2_lsn: Some("0/16960E8".parse::<Lsn>().unwrap()),
    1328            1 :             gc_compaction: Some(GcCompactionState {
    1329            1 :                 last_completed_lsn: "0/16960E8".parse::<Lsn>().unwrap(),
    1330            1 :             }),
    1331            1 :             marked_invisible_at: Some(parse_naive_datetime("2023-07-31T09:00:00.123000000")),
    1332            1 :             rel_size_migrated_at: None,
    1333            1 :         };
    1334              : 
    1335            1 :         let part = IndexPart::from_json_bytes(example.as_bytes()).unwrap();
    1336            1 :         assert_eq!(part, expected);
    1337            1 :     }
    1338              : 
    1339              :     #[test]
    1340            1 :     fn v15_rel_size_migrated_at_is_parsed() {
    1341            1 :         let example = r#"{
    1342            1 :             "version": 15,
    1343            1 :             "layer_metadata":{
    1344            1 :                 "000000000000000000000000000000000000-FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF__0000000001696070-00000000016960E9": { "file_size": 25600000 },
    1345            1 :                 "000000000000000000000000000000000000-FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF__00000000016B59D8-00000000016B5A51": { "file_size": 9007199254741001 }
    1346            1 :             },
    1347            1 :             "disk_consistent_lsn":"0/16960E8",
    1348            1 :             "metadata": {
    1349            1 :                 "disk_consistent_lsn": "0/16960E8",
    1350            1 :                 "prev_record_lsn": "0/1696070",
    1351            1 :                 "ancestor_timeline": "e45a7f37d3ee2ff17dc14bf4f4e3f52e",
    1352            1 :                 "ancestor_lsn": "0/0",
    1353            1 :                 "latest_gc_cutoff_lsn": "0/1696070",
    1354            1 :                 "initdb_lsn": "0/1696070",
    1355            1 :                 "pg_version": 14
    1356            1 :             },
    1357            1 :             "gc_blocking": {
    1358            1 :                 "started_at": "2024-07-19T09:00:00.123",
    1359            1 :                 "reasons": ["DetachAncestor"]
    1360            1 :             },
    1361            1 :             "import_pgdata": {
    1362            1 :                 "V1": {
    1363            1 :                     "Done": {
    1364            1 :                         "idempotency_key": "specified-by-client-218a5213-5044-4562-a28d-d024c5f057f5",
    1365            1 :                         "started_at": "2024-11-13T09:23:42.123",
    1366            1 :                         "finished_at": "2024-11-13T09:42:23.123"
    1367            1 :                     }
    1368            1 :                 }
    1369            1 :             },
    1370            1 :             "rel_size_migration": "legacy",
    1371            1 :             "l2_lsn": "0/16960E8",
    1372            1 :             "gc_compaction": {
    1373            1 :                 "last_completed_lsn": "0/16960E8"
    1374            1 :             },
    1375            1 :             "marked_invisible_at": "2023-07-31T09:00:00.123",
    1376            1 :             "rel_size_migrated_at": "0/16960E8"
    1377            1 :         }"#;
    1378              : 
    1379            1 :         let expected = IndexPart {
    1380            1 :             version: 15,
    1381            1 :             layer_metadata: HashMap::from([
    1382            1 :                 ("000000000000000000000000000000000000-FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF__0000000001696070-00000000016960E9".parse().unwrap(), LayerFileMetadata {
    1383            1 :                     file_size: 25600000,
    1384            1 :                     generation: Generation::none(),
    1385            1 :                     shard: ShardIndex::unsharded()
    1386            1 :                 }),
    1387            1 :                 ("000000000000000000000000000000000000-FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF__00000000016B59D8-00000000016B5A51".parse().unwrap(), LayerFileMetadata {
    1388            1 :                     file_size: 9007199254741001,
    1389            1 :                     generation: Generation::none(),
    1390            1 :                     shard: ShardIndex::unsharded()
    1391            1 :                 })
    1392            1 :             ]),
    1393            1 :             disk_consistent_lsn: "0/16960E8".parse::<Lsn>().unwrap(),
    1394            1 :             metadata: TimelineMetadata::new(
    1395            1 :                 Lsn::from_str("0/16960E8").unwrap(),
    1396            1 :                 Some(Lsn::from_str("0/1696070").unwrap()),
    1397            1 :                 Some(TimelineId::from_str("e45a7f37d3ee2ff17dc14bf4f4e3f52e").unwrap()),
    1398            1 :                 Lsn::INVALID,
    1399            1 :                 Lsn::from_str("0/1696070").unwrap(),
    1400            1 :                 Lsn::from_str("0/1696070").unwrap(),
    1401            1 :                 PgMajorVersion::PG14,
    1402            1 :             ).with_recalculated_checksum().unwrap(),
    1403            1 :             deleted_at: None,
    1404            1 :             lineage: Default::default(),
    1405            1 :             gc_blocking: Some(GcBlocking {
    1406            1 :                 started_at: parse_naive_datetime("2024-07-19T09:00:00.123000000"),
    1407            1 :                 reasons: enumset::EnumSet::from_iter([GcBlockingReason::DetachAncestor]),
    1408            1 :             }),
    1409            1 :             last_aux_file_policy: Default::default(),
    1410            1 :             archived_at: None,
    1411            1 :             import_pgdata: Some(import_pgdata::index_part_format::Root::V1(import_pgdata::index_part_format::V1::Done(import_pgdata::index_part_format::Done{
    1412            1 :                 started_at: parse_naive_datetime("2024-11-13T09:23:42.123000000"),
    1413            1 :                 finished_at: parse_naive_datetime("2024-11-13T09:42:23.123000000"),
    1414            1 :                 idempotency_key: import_pgdata::index_part_format::IdempotencyKey::new("specified-by-client-218a5213-5044-4562-a28d-d024c5f057f5".to_string()),
    1415            1 :             }))),
    1416            1 :             rel_size_migration: Some(RelSizeMigration::Legacy),
    1417            1 :             l2_lsn: Some("0/16960E8".parse::<Lsn>().unwrap()),
    1418            1 :             gc_compaction: Some(GcCompactionState {
    1419            1 :                 last_completed_lsn: "0/16960E8".parse::<Lsn>().unwrap(),
    1420            1 :             }),
    1421            1 :             marked_invisible_at: Some(parse_naive_datetime("2023-07-31T09:00:00.123000000")),
    1422            1 :             rel_size_migrated_at: Some("0/16960E8".parse::<Lsn>().unwrap()),
    1423            1 :         };
    1424              : 
    1425            1 :         let part = IndexPart::from_json_bytes(example.as_bytes()).unwrap();
    1426            1 :         assert_eq!(part, expected);
    1427            1 :     }
    1428              : 
    1429           26 :     fn parse_naive_datetime(s: &str) -> NaiveDateTime {
    1430           26 :         chrono::NaiveDateTime::parse_from_str(s, "%Y-%m-%dT%H:%M:%S.%f").unwrap()
    1431           26 :     }
    1432              : }
        

Generated by: LCOV version 2.1-beta