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 6097 : #[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 :
115 4 : #[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
116 : pub struct GcCompactionState {
117 : /// The upper bound of the last completed garbage-collecting compaction, aka. L2 LSN.
118 : pub(crate) last_completed_lsn: Lsn,
119 : }
120 :
121 : impl IndexPart {
122 : /// When adding or modifying any parts of `IndexPart`, increment the version so that it can be
123 : /// used to understand later versions.
124 : ///
125 : /// Version is currently informative only.
126 : /// Version history
127 : /// - 2: added `deleted_at`
128 : /// - 3: no longer deserialize `timeline_layers` (serialized format is the same, but timeline_layers
129 : /// is always generated from the keys of `layer_metadata`)
130 : /// - 4: timeline_layers is fully removed.
131 : /// - 5: lineage was added
132 : /// - 6: last_aux_file_policy is added.
133 : /// - 7: metadata_bytes is no longer written, but still read
134 : /// - 8: added `archived_at`
135 : /// - 9: +gc_blocking
136 : /// - 10: +import_pgdata
137 : /// - 11: +rel_size_migration
138 : /// - 12: +l2_lsn
139 : /// - 13: +gc_compaction
140 : const LATEST_VERSION: usize = 13;
141 :
142 : // Versions we may see when reading from a bucket.
143 : pub const KNOWN_VERSIONS: &'static [usize] = &[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13];
144 :
145 : pub const FILE_NAME: &'static str = "index_part.json";
146 :
147 1060 : pub fn empty(metadata: TimelineMetadata) -> Self {
148 1060 : IndexPart {
149 1060 : version: Self::LATEST_VERSION,
150 1060 : layer_metadata: Default::default(),
151 1060 : disk_consistent_lsn: metadata.disk_consistent_lsn(),
152 1060 : metadata,
153 1060 : deleted_at: None,
154 1060 : archived_at: None,
155 1060 : lineage: Default::default(),
156 1060 : gc_blocking: None,
157 1060 : last_aux_file_policy: None,
158 1060 : import_pgdata: None,
159 1060 : rel_size_migration: None,
160 1060 : l2_lsn: None,
161 1060 : gc_compaction: None,
162 1060 : }
163 1060 : }
164 :
165 0 : pub fn version(&self) -> usize {
166 0 : self.version
167 0 : }
168 :
169 : /// If you want this under normal operations, read it from self.metadata:
170 : /// this method is just for the scrubber to use when validating an index.
171 0 : pub fn duplicated_disk_consistent_lsn(&self) -> Lsn {
172 0 : self.disk_consistent_lsn
173 0 : }
174 :
175 52 : pub fn from_json_bytes(bytes: &[u8]) -> Result<Self, serde_json::Error> {
176 52 : serde_json::from_slice::<IndexPart>(bytes)
177 52 : }
178 :
179 2981 : pub fn to_json_bytes(&self) -> serde_json::Result<Vec<u8>> {
180 2981 : serde_json::to_vec(self)
181 2981 : }
182 :
183 : #[cfg(test)]
184 56 : pub(crate) fn example() -> Self {
185 56 : Self::empty(TimelineMetadata::example())
186 56 : }
187 :
188 : /// Returns true if the index contains a reference to the given layer (i.e. file path).
189 : ///
190 : /// TODO: there should be a variant of LayerName for the physical remote path that contains
191 : /// information about the shard and generation, to avoid passing in metadata.
192 70954 : pub fn references(&self, name: &LayerName, metadata: &LayerFileMetadata) -> bool {
193 70954 : let Some(index_metadata) = self.layer_metadata.get(name) else {
194 34352 : return false;
195 : };
196 36602 : is_same_remote_layer_path(name, metadata, name, index_metadata)
197 70954 : }
198 :
199 : /// Check for invariants in the index: this is useful when uploading an index to ensure that if
200 : /// we encounter a bug, we do not persist buggy metadata.
201 5437 : pub(crate) fn validate(&self) -> Result<(), String> {
202 5437 : if self.import_pgdata.is_none()
203 5437 : && self.metadata.ancestor_timeline().is_none()
204 4030 : && self.layer_metadata.is_empty()
205 : {
206 : // Unless we're in the middle of a raw pgdata import, or this is a child timeline,the index must
207 : // always have at least one layer.
208 0 : return Err("Index has no ancestor and no layers".to_string());
209 5437 : }
210 5437 :
211 5437 : Ok(())
212 5437 : }
213 : }
214 :
215 : /// Metadata gathered for each of the layer files.
216 : ///
217 : /// Fields have to be `Option`s because remote [`IndexPart`]'s can be from different version, which
218 : /// might have less or more metadata depending if upgrading or rolling back an upgrade.
219 18968 : #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
220 : pub struct LayerFileMetadata {
221 : pub file_size: u64,
222 :
223 : #[serde(default = "Generation::none")]
224 : #[serde(skip_serializing_if = "Generation::is_none")]
225 : pub generation: Generation,
226 :
227 : #[serde(default = "ShardIndex::unsharded")]
228 : #[serde(skip_serializing_if = "ShardIndex::is_unsharded")]
229 : pub shard: ShardIndex,
230 : }
231 :
232 : impl LayerFileMetadata {
233 6332 : pub fn new(file_size: u64, generation: Generation, shard: ShardIndex) -> Self {
234 6332 : LayerFileMetadata {
235 6332 : file_size,
236 6332 : generation,
237 6332 : shard,
238 6332 : }
239 6332 : }
240 : /// Helper to get both generation and file size in a tuple
241 0 : pub fn generation_file_size(&self) -> (Generation, u64) {
242 0 : (self.generation, self.file_size)
243 0 : }
244 : }
245 :
246 : /// Limited history of earlier ancestors.
247 : ///
248 : /// A timeline can have more than 1 earlier ancestor, in the rare case that it was repeatedly
249 : /// reparented by having an later timeline be detached from it's ancestor.
250 16 : #[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize, Default)]
251 : pub(crate) struct Lineage {
252 : /// Has the `reparenting_history` been truncated to [`Lineage::REMEMBER_AT_MOST`].
253 : #[serde(skip_serializing_if = "is_false", default)]
254 : reparenting_history_truncated: bool,
255 :
256 : /// Earlier ancestors, truncated when [`Self::reparenting_history_truncated`]
257 : ///
258 : /// These are stored in case we want to support WAL based DR on the timeline. There can be many
259 : /// of these and at most one [`Self::original_ancestor`]. There cannot be more reparentings
260 : /// after [`Self::original_ancestor`] has been set.
261 : #[serde(skip_serializing_if = "Vec::is_empty", default)]
262 : reparenting_history: Vec<TimelineId>,
263 :
264 : /// The ancestor from which this timeline has been detached from and when.
265 : ///
266 : /// If you are adding support for detaching from a hierarchy, consider changing the ancestry
267 : /// into a `Vec<(TimelineId, Lsn)>` to be a path instead.
268 : // FIXME: this is insufficient even for path of two timelines for future wal recovery
269 : // purposes:
270 : //
271 : // assuming a "old main" which has received most of the WAL, and has a branch "new main",
272 : // starting a bit before "old main" last_record_lsn. the current version works fine,
273 : // because we will know to replay wal and branch at the recorded Lsn to do wal recovery.
274 : //
275 : // then assuming "new main" would similarly receive a branch right before its last_record_lsn,
276 : // "new new main". the current implementation would just store ("new main", ancestor_lsn, _)
277 : // here. however, we cannot recover from WAL using only that information, we would need the
278 : // whole ancestry here:
279 : //
280 : // ```json
281 : // [
282 : // ["old main", ancestor_lsn("new main"), _],
283 : // ["new main", ancestor_lsn("new new main"), _]
284 : // ]
285 : // ```
286 : #[serde(skip_serializing_if = "Option::is_none", default)]
287 : original_ancestor: Option<(TimelineId, Lsn, NaiveDateTime)>,
288 : }
289 :
290 12194 : fn is_false(b: &bool) -> bool {
291 12194 : !b
292 12194 : }
293 :
294 : impl Lineage {
295 : const REMEMBER_AT_MOST: usize = 100;
296 :
297 0 : pub(crate) fn record_previous_ancestor(&mut self, old_ancestor: &TimelineId) -> bool {
298 0 : if self.reparenting_history.last() == Some(old_ancestor) {
299 : // do not re-record it
300 0 : false
301 : } else {
302 : #[cfg(feature = "testing")]
303 : {
304 0 : let existing = self
305 0 : .reparenting_history
306 0 : .iter()
307 0 : .position(|x| x == old_ancestor);
308 0 : assert_eq!(
309 : existing, None,
310 0 : "we cannot reparent onto and off and onto the same timeline twice"
311 : );
312 : }
313 0 : let drop_oldest = self.reparenting_history.len() + 1 >= Self::REMEMBER_AT_MOST;
314 0 :
315 0 : self.reparenting_history_truncated |= drop_oldest;
316 0 : if drop_oldest {
317 0 : self.reparenting_history.remove(0);
318 0 : }
319 0 : self.reparenting_history.push(*old_ancestor);
320 0 : true
321 : }
322 0 : }
323 :
324 : /// Returns true if anything changed.
325 0 : pub(crate) fn record_detaching(&mut self, branchpoint: &(TimelineId, Lsn)) -> bool {
326 0 : if let Some((id, lsn, _)) = self.original_ancestor {
327 0 : assert_eq!(
328 0 : &(id, lsn),
329 : branchpoint,
330 0 : "detaching attempt has to be for the same ancestor we are already detached from"
331 : );
332 0 : false
333 : } else {
334 0 : self.original_ancestor =
335 0 : Some((branchpoint.0, branchpoint.1, chrono::Utc::now().naive_utc()));
336 0 : true
337 : }
338 0 : }
339 :
340 : /// The queried lsn is most likely the basebackup lsn, and this answers question "is it allowed
341 : /// to start a read/write primary at this lsn".
342 : ///
343 : /// Returns true if the Lsn was previously our branch point.
344 0 : pub(crate) fn is_previous_ancestor_lsn(&self, lsn: Lsn) -> bool {
345 0 : self.original_ancestor
346 0 : .is_some_and(|(_, ancestor_lsn, _)| ancestor_lsn == lsn)
347 0 : }
348 :
349 : /// Returns true if the timeline originally had an ancestor, and no longer has one.
350 0 : pub(crate) fn is_detached_from_ancestor(&self) -> bool {
351 0 : self.original_ancestor.is_some()
352 0 : }
353 :
354 : /// Returns original ancestor timeline id and lsn that this timeline has been detached from.
355 0 : pub(crate) fn detached_previous_ancestor(&self) -> Option<(TimelineId, Lsn)> {
356 0 : self.original_ancestor.map(|(id, lsn, _)| (id, lsn))
357 0 : }
358 :
359 0 : pub(crate) fn is_reparented(&self) -> bool {
360 0 : !self.reparenting_history.is_empty()
361 0 : }
362 : }
363 :
364 32 : #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
365 : pub(crate) struct GcBlocking {
366 : pub(crate) started_at: NaiveDateTime,
367 : pub(crate) reasons: enumset::EnumSet<GcBlockingReason>,
368 : }
369 :
370 16 : #[derive(Debug, enumset::EnumSetType, serde::Serialize, serde::Deserialize)]
371 : #[enumset(serialize_repr = "list")]
372 : pub(crate) enum GcBlockingReason {
373 : Manual,
374 : DetachAncestor,
375 : }
376 :
377 : impl GcBlocking {
378 0 : pub(super) fn started_now_for(reason: GcBlockingReason) -> Self {
379 0 : GcBlocking {
380 0 : started_at: chrono::Utc::now().naive_utc(),
381 0 : reasons: enumset::EnumSet::only(reason),
382 0 : }
383 0 : }
384 :
385 : /// Returns true if the given reason is one of the reasons why the gc is blocked.
386 0 : pub(crate) fn blocked_by(&self, reason: GcBlockingReason) -> bool {
387 0 : self.reasons.contains(reason)
388 0 : }
389 :
390 : /// Returns a version of self with the given reason.
391 0 : pub(super) fn with_reason(&self, reason: GcBlockingReason) -> Self {
392 0 : assert!(!self.blocked_by(reason));
393 0 : let mut reasons = self.reasons;
394 0 : reasons.insert(reason);
395 0 :
396 0 : Self {
397 0 : started_at: self.started_at,
398 0 : reasons,
399 0 : }
400 0 : }
401 :
402 : /// Returns a version of self without the given reason. Assumption is that if
403 : /// there are no more reasons, we can unblock the gc by returning `None`.
404 0 : pub(super) fn without_reason(&self, reason: GcBlockingReason) -> Option<Self> {
405 0 : assert!(self.blocked_by(reason));
406 :
407 0 : if self.reasons.len() == 1 {
408 0 : None
409 : } else {
410 0 : let mut reasons = self.reasons;
411 0 : assert!(reasons.remove(reason));
412 0 : assert!(!reasons.is_empty());
413 :
414 0 : Some(Self {
415 0 : started_at: self.started_at,
416 0 : reasons,
417 0 : })
418 : }
419 0 : }
420 : }
421 :
422 : #[cfg(test)]
423 : mod tests {
424 : use std::str::FromStr;
425 :
426 : use utils::id::TimelineId;
427 :
428 : use super::*;
429 :
430 : #[test]
431 4 : fn v1_indexpart_is_parsed() {
432 4 : let example = r#"{
433 4 : "version":1,
434 4 : "timeline_layers":["000000000000000000000000000000000000-FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF__0000000001696070-00000000016960E9"],
435 4 : "layer_metadata":{
436 4 : "000000000000000000000000000000000000-FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF__0000000001696070-00000000016960E9": { "file_size": 25600000 },
437 4 : "000000000000000000000000000000000000-FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF__00000000016B59D8-00000000016B5A51": { "file_size": 9007199254741001 }
438 4 : },
439 4 : "disk_consistent_lsn":"0/16960E8",
440 4 : "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]
441 4 : }"#;
442 4 :
443 4 : let expected = IndexPart {
444 4 : // note this is not verified, could be anything, but exists for humans debugging.. could be the git version instead?
445 4 : version: 1,
446 4 : layer_metadata: HashMap::from([
447 4 : ("000000000000000000000000000000000000-FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF__0000000001696070-00000000016960E9".parse().unwrap(), LayerFileMetadata {
448 4 : file_size: 25600000,
449 4 : generation: Generation::none(),
450 4 : shard: ShardIndex::unsharded()
451 4 : }),
452 4 : ("000000000000000000000000000000000000-FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF__00000000016B59D8-00000000016B5A51".parse().unwrap(), LayerFileMetadata {
453 4 : // serde_json should always parse this but this might be a double with jq for
454 4 : // example.
455 4 : file_size: 9007199254741001,
456 4 : generation: Generation::none(),
457 4 : shard: ShardIndex::unsharded()
458 4 : })
459 4 : ]),
460 4 : disk_consistent_lsn: "0/16960E8".parse::<Lsn>().unwrap(),
461 4 : 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(),
462 4 : deleted_at: None,
463 4 : archived_at: None,
464 4 : lineage: Lineage::default(),
465 4 : gc_blocking: None,
466 4 : last_aux_file_policy: None,
467 4 : import_pgdata: None,
468 4 : rel_size_migration: None,
469 4 : l2_lsn: None,
470 4 : gc_compaction: None,
471 4 : };
472 4 :
473 4 : let part = IndexPart::from_json_bytes(example.as_bytes()).unwrap();
474 4 : assert_eq!(part, expected);
475 4 : }
476 :
477 : #[test]
478 4 : fn v1_indexpart_is_parsed_with_optional_missing_layers() {
479 4 : let example = r#"{
480 4 : "version":1,
481 4 : "timeline_layers":["000000000000000000000000000000000000-FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF__0000000001696070-00000000016960E9"],
482 4 : "missing_layers":["This shouldn't fail deserialization"],
483 4 : "layer_metadata":{
484 4 : "000000000000000000000000000000000000-FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF__0000000001696070-00000000016960E9": { "file_size": 25600000 },
485 4 : "000000000000000000000000000000000000-FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF__00000000016B59D8-00000000016B5A51": { "file_size": 9007199254741001 }
486 4 : },
487 4 : "disk_consistent_lsn":"0/16960E8",
488 4 : "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]
489 4 : }"#;
490 4 :
491 4 : let expected = IndexPart {
492 4 : // note this is not verified, could be anything, but exists for humans debugging.. could be the git version instead?
493 4 : version: 1,
494 4 : layer_metadata: HashMap::from([
495 4 : ("000000000000000000000000000000000000-FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF__0000000001696070-00000000016960E9".parse().unwrap(), LayerFileMetadata {
496 4 : file_size: 25600000,
497 4 : generation: Generation::none(),
498 4 : shard: ShardIndex::unsharded()
499 4 : }),
500 4 : ("000000000000000000000000000000000000-FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF__00000000016B59D8-00000000016B5A51".parse().unwrap(), LayerFileMetadata {
501 4 : // serde_json should always parse this but this might be a double with jq for
502 4 : // example.
503 4 : file_size: 9007199254741001,
504 4 : generation: Generation::none(),
505 4 : shard: ShardIndex::unsharded()
506 4 : })
507 4 : ]),
508 4 : disk_consistent_lsn: "0/16960E8".parse::<Lsn>().unwrap(),
509 4 : 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(),
510 4 : deleted_at: None,
511 4 : archived_at: None,
512 4 : lineage: Lineage::default(),
513 4 : gc_blocking: None,
514 4 : last_aux_file_policy: None,
515 4 : import_pgdata: None,
516 4 : rel_size_migration: None,
517 4 : l2_lsn: None,
518 4 : gc_compaction: None,
519 4 : };
520 4 :
521 4 : let part = IndexPart::from_json_bytes(example.as_bytes()).unwrap();
522 4 : assert_eq!(part, expected);
523 4 : }
524 :
525 : #[test]
526 4 : fn v2_indexpart_is_parsed_with_deleted_at() {
527 4 : let example = r#"{
528 4 : "version":2,
529 4 : "timeline_layers":["000000000000000000000000000000000000-FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF__0000000001696070-00000000016960E9"],
530 4 : "missing_layers":["This shouldn't fail deserialization"],
531 4 : "layer_metadata":{
532 4 : "000000000000000000000000000000000000-FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF__0000000001696070-00000000016960E9": { "file_size": 25600000 },
533 4 : "000000000000000000000000000000000000-FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF__00000000016B59D8-00000000016B5A51": { "file_size": 9007199254741001 }
534 4 : },
535 4 : "disk_consistent_lsn":"0/16960E8",
536 4 : "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],
537 4 : "deleted_at": "2023-07-31T09:00:00.123"
538 4 : }"#;
539 4 :
540 4 : let expected = IndexPart {
541 4 : // note this is not verified, could be anything, but exists for humans debugging.. could be the git version instead?
542 4 : version: 2,
543 4 : layer_metadata: HashMap::from([
544 4 : ("000000000000000000000000000000000000-FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF__0000000001696070-00000000016960E9".parse().unwrap(), LayerFileMetadata {
545 4 : file_size: 25600000,
546 4 : generation: Generation::none(),
547 4 : shard: ShardIndex::unsharded()
548 4 : }),
549 4 : ("000000000000000000000000000000000000-FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF__00000000016B59D8-00000000016B5A51".parse().unwrap(), LayerFileMetadata {
550 4 : // serde_json should always parse this but this might be a double with jq for
551 4 : // example.
552 4 : file_size: 9007199254741001,
553 4 : generation: Generation::none(),
554 4 : shard: ShardIndex::unsharded()
555 4 : })
556 4 : ]),
557 4 : disk_consistent_lsn: "0/16960E8".parse::<Lsn>().unwrap(),
558 4 : 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(),
559 4 : deleted_at: Some(parse_naive_datetime("2023-07-31T09:00:00.123000000")),
560 4 : archived_at: None,
561 4 : lineage: Lineage::default(),
562 4 : gc_blocking: None,
563 4 : last_aux_file_policy: None,
564 4 : import_pgdata: None,
565 4 : rel_size_migration: None,
566 4 : l2_lsn: None,
567 4 : gc_compaction: None,
568 4 : };
569 4 :
570 4 : let part = IndexPart::from_json_bytes(example.as_bytes()).unwrap();
571 4 : assert_eq!(part, expected);
572 4 : }
573 :
574 : #[test]
575 4 : fn empty_layers_are_parsed() {
576 4 : let empty_layers_json = r#"{
577 4 : "version":1,
578 4 : "timeline_layers":[],
579 4 : "layer_metadata":{},
580 4 : "disk_consistent_lsn":"0/2532648",
581 4 : "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]
582 4 : }"#;
583 4 :
584 4 : let expected = IndexPart {
585 4 : version: 1,
586 4 : layer_metadata: HashMap::new(),
587 4 : disk_consistent_lsn: "0/2532648".parse::<Lsn>().unwrap(),
588 4 : metadata: TimelineMetadata::from_bytes(&[
589 4 : 136, 151, 49, 208, 0, 70, 0, 4, 0, 0, 0, 0, 2, 83, 38, 72, 1, 0, 0, 0, 0, 2, 83,
590 4 : 38, 32, 1, 87, 198, 240, 135, 97, 119, 45, 125, 38, 29, 155, 161, 140, 141, 255,
591 4 : 210, 0, 0, 0, 0, 2, 83, 38, 72, 0, 0, 0, 0, 1, 73, 240, 192, 0, 0, 0, 0, 1, 73,
592 4 : 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,
593 4 : 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
594 4 : 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
595 4 : 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
596 4 : 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
597 4 : 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
598 4 : 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
599 4 : 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
600 4 : 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
601 4 : 0, 0, 0, 0, 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 4 : 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
603 4 : 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
604 4 : 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
605 4 : 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
606 4 : 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
607 4 : 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
608 4 : 0, 0,
609 4 : ])
610 4 : .unwrap(),
611 4 : deleted_at: None,
612 4 : archived_at: None,
613 4 : lineage: Lineage::default(),
614 4 : gc_blocking: None,
615 4 : last_aux_file_policy: None,
616 4 : import_pgdata: None,
617 4 : rel_size_migration: None,
618 4 : l2_lsn: None,
619 4 : gc_compaction: None,
620 4 : };
621 4 :
622 4 : let empty_layers_parsed = IndexPart::from_json_bytes(empty_layers_json.as_bytes()).unwrap();
623 4 :
624 4 : assert_eq!(empty_layers_parsed, expected);
625 4 : }
626 :
627 : #[test]
628 4 : fn v4_indexpart_is_parsed() {
629 4 : let example = r#"{
630 4 : "version":4,
631 4 : "layer_metadata":{
632 4 : "000000000000000000000000000000000000-FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF__0000000001696070-00000000016960E9": { "file_size": 25600000 },
633 4 : "000000000000000000000000000000000000-FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF__00000000016B59D8-00000000016B5A51": { "file_size": 9007199254741001 }
634 4 : },
635 4 : "disk_consistent_lsn":"0/16960E8",
636 4 : "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],
637 4 : "deleted_at": "2023-07-31T09:00:00.123"
638 4 : }"#;
639 4 :
640 4 : let expected = IndexPart {
641 4 : version: 4,
642 4 : layer_metadata: HashMap::from([
643 4 : ("000000000000000000000000000000000000-FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF__0000000001696070-00000000016960E9".parse().unwrap(), LayerFileMetadata {
644 4 : file_size: 25600000,
645 4 : generation: Generation::none(),
646 4 : shard: ShardIndex::unsharded()
647 4 : }),
648 4 : ("000000000000000000000000000000000000-FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF__00000000016B59D8-00000000016B5A51".parse().unwrap(), LayerFileMetadata {
649 4 : // serde_json should always parse this but this might be a double with jq for
650 4 : // example.
651 4 : file_size: 9007199254741001,
652 4 : generation: Generation::none(),
653 4 : shard: ShardIndex::unsharded()
654 4 : })
655 4 : ]),
656 4 : disk_consistent_lsn: "0/16960E8".parse::<Lsn>().unwrap(),
657 4 : 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(),
658 4 : deleted_at: Some(parse_naive_datetime("2023-07-31T09:00:00.123000000")),
659 4 : archived_at: None,
660 4 : lineage: Lineage::default(),
661 4 : gc_blocking: None,
662 4 : last_aux_file_policy: None,
663 4 : import_pgdata: None,
664 4 : rel_size_migration: None,
665 4 : l2_lsn: None,
666 4 : gc_compaction: None,
667 4 : };
668 4 :
669 4 : let part = IndexPart::from_json_bytes(example.as_bytes()).unwrap();
670 4 : assert_eq!(part, expected);
671 4 : }
672 :
673 : #[test]
674 4 : fn v5_indexpart_is_parsed() {
675 4 : let example = r#"{
676 4 : "version":5,
677 4 : "layer_metadata":{
678 4 : "000000000000000000000000000000000000-FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF__00000000014EF420-00000000014EF499":{"file_size":23289856,"generation":1},
679 4 : "000000000000000000000000000000000000-FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF__00000000014EF499-00000000015A7619":{"file_size":1015808,"generation":1}},
680 4 : "disk_consistent_lsn":"0/15A7618",
681 4 : "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],
682 4 : "lineage":{
683 4 : "original_ancestor":["e2bfd8c633d713d279e6fcd2bcc15b6d","0/15A7618","2024-05-07T18:52:36.322426563"],
684 4 : "reparenting_history":["e1bfd8c633d713d279e6fcd2bcc15b6d"]
685 4 : }
686 4 : }"#;
687 4 :
688 4 : let expected = IndexPart {
689 4 : version: 5,
690 4 : layer_metadata: HashMap::from([
691 4 : ("000000000000000000000000000000000000-FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF__00000000014EF420-00000000014EF499".parse().unwrap(), LayerFileMetadata {
692 4 : file_size: 23289856,
693 4 : generation: Generation::new(1),
694 4 : shard: ShardIndex::unsharded(),
695 4 : }),
696 4 : ("000000000000000000000000000000000000-FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF__00000000014EF499-00000000015A7619".parse().unwrap(), LayerFileMetadata {
697 4 : file_size: 1015808,
698 4 : generation: Generation::new(1),
699 4 : shard: ShardIndex::unsharded(),
700 4 : })
701 4 : ]),
702 4 : disk_consistent_lsn: Lsn::from_str("0/15A7618").unwrap(),
703 4 : 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(),
704 4 : deleted_at: None,
705 4 : archived_at: None,
706 4 : lineage: Lineage {
707 4 : reparenting_history_truncated: false,
708 4 : reparenting_history: vec![TimelineId::from_str("e1bfd8c633d713d279e6fcd2bcc15b6d").unwrap()],
709 4 : original_ancestor: Some((TimelineId::from_str("e2bfd8c633d713d279e6fcd2bcc15b6d").unwrap(), Lsn::from_str("0/15A7618").unwrap(), parse_naive_datetime("2024-05-07T18:52:36.322426563"))),
710 4 : },
711 4 : gc_blocking: None,
712 4 : last_aux_file_policy: None,
713 4 : import_pgdata: None,
714 4 : rel_size_migration: None,
715 4 : l2_lsn: None,
716 4 : gc_compaction: None,
717 4 : };
718 4 :
719 4 : let part = IndexPart::from_json_bytes(example.as_bytes()).unwrap();
720 4 : assert_eq!(part, expected);
721 4 : }
722 :
723 : #[test]
724 4 : fn v6_indexpart_is_parsed() {
725 4 : let example = r#"{
726 4 : "version":6,
727 4 : "layer_metadata":{
728 4 : "000000000000000000000000000000000000-FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF__0000000001696070-00000000016960E9": { "file_size": 25600000 },
729 4 : "000000000000000000000000000000000000-FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF__00000000016B59D8-00000000016B5A51": { "file_size": 9007199254741001 }
730 4 : },
731 4 : "disk_consistent_lsn":"0/16960E8",
732 4 : "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],
733 4 : "deleted_at": "2023-07-31T09:00:00.123",
734 4 : "lineage":{
735 4 : "original_ancestor":["e2bfd8c633d713d279e6fcd2bcc15b6d","0/15A7618","2024-05-07T18:52:36.322426563"],
736 4 : "reparenting_history":["e1bfd8c633d713d279e6fcd2bcc15b6d"]
737 4 : },
738 4 : "last_aux_file_policy": "V2"
739 4 : }"#;
740 4 :
741 4 : let expected = IndexPart {
742 4 : version: 6,
743 4 : layer_metadata: HashMap::from([
744 4 : ("000000000000000000000000000000000000-FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF__0000000001696070-00000000016960E9".parse().unwrap(), LayerFileMetadata {
745 4 : file_size: 25600000,
746 4 : generation: Generation::none(),
747 4 : shard: ShardIndex::unsharded()
748 4 : }),
749 4 : ("000000000000000000000000000000000000-FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF__00000000016B59D8-00000000016B5A51".parse().unwrap(), LayerFileMetadata {
750 4 : // serde_json should always parse this but this might be a double with jq for
751 4 : // example.
752 4 : file_size: 9007199254741001,
753 4 : generation: Generation::none(),
754 4 : shard: ShardIndex::unsharded()
755 4 : })
756 4 : ]),
757 4 : disk_consistent_lsn: "0/16960E8".parse::<Lsn>().unwrap(),
758 4 : 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(),
759 4 : deleted_at: Some(parse_naive_datetime("2023-07-31T09:00:00.123000000")),
760 4 : archived_at: None,
761 4 : lineage: Lineage {
762 4 : reparenting_history_truncated: false,
763 4 : reparenting_history: vec![TimelineId::from_str("e1bfd8c633d713d279e6fcd2bcc15b6d").unwrap()],
764 4 : original_ancestor: Some((TimelineId::from_str("e2bfd8c633d713d279e6fcd2bcc15b6d").unwrap(), Lsn::from_str("0/15A7618").unwrap(), parse_naive_datetime("2024-05-07T18:52:36.322426563"))),
765 4 : },
766 4 : gc_blocking: None,
767 4 : last_aux_file_policy: Some(AuxFilePolicy::V2),
768 4 : import_pgdata: None,
769 4 : rel_size_migration: None,
770 4 : l2_lsn: None,
771 4 : gc_compaction: None,
772 4 : };
773 4 :
774 4 : let part = IndexPart::from_json_bytes(example.as_bytes()).unwrap();
775 4 : assert_eq!(part, expected);
776 4 : }
777 :
778 : #[test]
779 4 : fn v7_indexpart_is_parsed() {
780 4 : let example = r#"{
781 4 : "version": 7,
782 4 : "layer_metadata":{
783 4 : "000000000000000000000000000000000000-FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF__0000000001696070-00000000016960E9": { "file_size": 25600000 },
784 4 : "000000000000000000000000000000000000-FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF__00000000016B59D8-00000000016B5A51": { "file_size": 9007199254741001 }
785 4 : },
786 4 : "disk_consistent_lsn":"0/16960E8",
787 4 : "metadata": {
788 4 : "disk_consistent_lsn": "0/16960E8",
789 4 : "prev_record_lsn": "0/1696070",
790 4 : "ancestor_timeline": "e45a7f37d3ee2ff17dc14bf4f4e3f52e",
791 4 : "ancestor_lsn": "0/0",
792 4 : "latest_gc_cutoff_lsn": "0/1696070",
793 4 : "initdb_lsn": "0/1696070",
794 4 : "pg_version": 14
795 4 : },
796 4 : "deleted_at": "2023-07-31T09:00:00.123"
797 4 : }"#;
798 4 :
799 4 : let expected = IndexPart {
800 4 : version: 7,
801 4 : layer_metadata: HashMap::from([
802 4 : ("000000000000000000000000000000000000-FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF__0000000001696070-00000000016960E9".parse().unwrap(), LayerFileMetadata {
803 4 : file_size: 25600000,
804 4 : generation: Generation::none(),
805 4 : shard: ShardIndex::unsharded()
806 4 : }),
807 4 : ("000000000000000000000000000000000000-FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF__00000000016B59D8-00000000016B5A51".parse().unwrap(), LayerFileMetadata {
808 4 : file_size: 9007199254741001,
809 4 : generation: Generation::none(),
810 4 : shard: ShardIndex::unsharded()
811 4 : })
812 4 : ]),
813 4 : disk_consistent_lsn: "0/16960E8".parse::<Lsn>().unwrap(),
814 4 : metadata: TimelineMetadata::new(
815 4 : Lsn::from_str("0/16960E8").unwrap(),
816 4 : Some(Lsn::from_str("0/1696070").unwrap()),
817 4 : Some(TimelineId::from_str("e45a7f37d3ee2ff17dc14bf4f4e3f52e").unwrap()),
818 4 : Lsn::INVALID,
819 4 : Lsn::from_str("0/1696070").unwrap(),
820 4 : Lsn::from_str("0/1696070").unwrap(),
821 4 : 14,
822 4 : ).with_recalculated_checksum().unwrap(),
823 4 : deleted_at: Some(parse_naive_datetime("2023-07-31T09:00:00.123000000")),
824 4 : archived_at: None,
825 4 : lineage: Default::default(),
826 4 : gc_blocking: None,
827 4 : last_aux_file_policy: Default::default(),
828 4 : import_pgdata: None,
829 4 : rel_size_migration: None,
830 4 : l2_lsn: None,
831 4 : gc_compaction: None,
832 4 : };
833 4 :
834 4 : let part = IndexPart::from_json_bytes(example.as_bytes()).unwrap();
835 4 : assert_eq!(part, expected);
836 4 : }
837 :
838 : #[test]
839 4 : fn v8_indexpart_is_parsed() {
840 4 : let example = r#"{
841 4 : "version": 8,
842 4 : "layer_metadata":{
843 4 : "000000000000000000000000000000000000-FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF__0000000001696070-00000000016960E9": { "file_size": 25600000 },
844 4 : "000000000000000000000000000000000000-FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF__00000000016B59D8-00000000016B5A51": { "file_size": 9007199254741001 }
845 4 : },
846 4 : "disk_consistent_lsn":"0/16960E8",
847 4 : "metadata": {
848 4 : "disk_consistent_lsn": "0/16960E8",
849 4 : "prev_record_lsn": "0/1696070",
850 4 : "ancestor_timeline": "e45a7f37d3ee2ff17dc14bf4f4e3f52e",
851 4 : "ancestor_lsn": "0/0",
852 4 : "latest_gc_cutoff_lsn": "0/1696070",
853 4 : "initdb_lsn": "0/1696070",
854 4 : "pg_version": 14
855 4 : },
856 4 : "deleted_at": "2023-07-31T09:00:00.123",
857 4 : "archived_at": "2023-04-29T09:00:00.123"
858 4 : }"#;
859 4 :
860 4 : let expected = IndexPart {
861 4 : version: 8,
862 4 : layer_metadata: HashMap::from([
863 4 : ("000000000000000000000000000000000000-FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF__0000000001696070-00000000016960E9".parse().unwrap(), LayerFileMetadata {
864 4 : file_size: 25600000,
865 4 : generation: Generation::none(),
866 4 : shard: ShardIndex::unsharded()
867 4 : }),
868 4 : ("000000000000000000000000000000000000-FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF__00000000016B59D8-00000000016B5A51".parse().unwrap(), LayerFileMetadata {
869 4 : file_size: 9007199254741001,
870 4 : generation: Generation::none(),
871 4 : shard: ShardIndex::unsharded()
872 4 : })
873 4 : ]),
874 4 : disk_consistent_lsn: "0/16960E8".parse::<Lsn>().unwrap(),
875 4 : metadata: TimelineMetadata::new(
876 4 : Lsn::from_str("0/16960E8").unwrap(),
877 4 : Some(Lsn::from_str("0/1696070").unwrap()),
878 4 : Some(TimelineId::from_str("e45a7f37d3ee2ff17dc14bf4f4e3f52e").unwrap()),
879 4 : Lsn::INVALID,
880 4 : Lsn::from_str("0/1696070").unwrap(),
881 4 : Lsn::from_str("0/1696070").unwrap(),
882 4 : 14,
883 4 : ).with_recalculated_checksum().unwrap(),
884 4 : deleted_at: Some(parse_naive_datetime("2023-07-31T09:00:00.123000000")),
885 4 : archived_at: Some(parse_naive_datetime("2023-04-29T09:00:00.123000000")),
886 4 : lineage: Default::default(),
887 4 : gc_blocking: None,
888 4 : last_aux_file_policy: Default::default(),
889 4 : import_pgdata: None,
890 4 : rel_size_migration: None,
891 4 : l2_lsn: None,
892 4 : gc_compaction: None,
893 4 : };
894 4 :
895 4 : let part = IndexPart::from_json_bytes(example.as_bytes()).unwrap();
896 4 : assert_eq!(part, expected);
897 4 : }
898 :
899 : #[test]
900 4 : fn v9_indexpart_is_parsed() {
901 4 : let example = r#"{
902 4 : "version": 9,
903 4 : "layer_metadata":{
904 4 : "000000000000000000000000000000000000-FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF__0000000001696070-00000000016960E9": { "file_size": 25600000 },
905 4 : "000000000000000000000000000000000000-FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF__00000000016B59D8-00000000016B5A51": { "file_size": 9007199254741001 }
906 4 : },
907 4 : "disk_consistent_lsn":"0/16960E8",
908 4 : "metadata": {
909 4 : "disk_consistent_lsn": "0/16960E8",
910 4 : "prev_record_lsn": "0/1696070",
911 4 : "ancestor_timeline": "e45a7f37d3ee2ff17dc14bf4f4e3f52e",
912 4 : "ancestor_lsn": "0/0",
913 4 : "latest_gc_cutoff_lsn": "0/1696070",
914 4 : "initdb_lsn": "0/1696070",
915 4 : "pg_version": 14
916 4 : },
917 4 : "gc_blocking": {
918 4 : "started_at": "2024-07-19T09:00:00.123",
919 4 : "reasons": ["DetachAncestor"]
920 4 : }
921 4 : }"#;
922 4 :
923 4 : let expected = IndexPart {
924 4 : version: 9,
925 4 : layer_metadata: HashMap::from([
926 4 : ("000000000000000000000000000000000000-FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF__0000000001696070-00000000016960E9".parse().unwrap(), LayerFileMetadata {
927 4 : file_size: 25600000,
928 4 : generation: Generation::none(),
929 4 : shard: ShardIndex::unsharded()
930 4 : }),
931 4 : ("000000000000000000000000000000000000-FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF__00000000016B59D8-00000000016B5A51".parse().unwrap(), LayerFileMetadata {
932 4 : file_size: 9007199254741001,
933 4 : generation: Generation::none(),
934 4 : shard: ShardIndex::unsharded()
935 4 : })
936 4 : ]),
937 4 : disk_consistent_lsn: "0/16960E8".parse::<Lsn>().unwrap(),
938 4 : metadata: TimelineMetadata::new(
939 4 : Lsn::from_str("0/16960E8").unwrap(),
940 4 : Some(Lsn::from_str("0/1696070").unwrap()),
941 4 : Some(TimelineId::from_str("e45a7f37d3ee2ff17dc14bf4f4e3f52e").unwrap()),
942 4 : Lsn::INVALID,
943 4 : Lsn::from_str("0/1696070").unwrap(),
944 4 : Lsn::from_str("0/1696070").unwrap(),
945 4 : 14,
946 4 : ).with_recalculated_checksum().unwrap(),
947 4 : deleted_at: None,
948 4 : lineage: Default::default(),
949 4 : gc_blocking: Some(GcBlocking {
950 4 : started_at: parse_naive_datetime("2024-07-19T09:00:00.123000000"),
951 4 : reasons: enumset::EnumSet::from_iter([GcBlockingReason::DetachAncestor]),
952 4 : }),
953 4 : last_aux_file_policy: Default::default(),
954 4 : archived_at: None,
955 4 : import_pgdata: None,
956 4 : rel_size_migration: None,
957 4 : l2_lsn: None,
958 4 : gc_compaction: None,
959 4 : };
960 4 :
961 4 : let part = IndexPart::from_json_bytes(example.as_bytes()).unwrap();
962 4 : assert_eq!(part, expected);
963 4 : }
964 :
965 : #[test]
966 4 : fn v10_importpgdata_is_parsed() {
967 4 : let example = r#"{
968 4 : "version": 10,
969 4 : "layer_metadata":{
970 4 : "000000000000000000000000000000000000-FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF__0000000001696070-00000000016960E9": { "file_size": 25600000 },
971 4 : "000000000000000000000000000000000000-FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF__00000000016B59D8-00000000016B5A51": { "file_size": 9007199254741001 }
972 4 : },
973 4 : "disk_consistent_lsn":"0/16960E8",
974 4 : "metadata": {
975 4 : "disk_consistent_lsn": "0/16960E8",
976 4 : "prev_record_lsn": "0/1696070",
977 4 : "ancestor_timeline": "e45a7f37d3ee2ff17dc14bf4f4e3f52e",
978 4 : "ancestor_lsn": "0/0",
979 4 : "latest_gc_cutoff_lsn": "0/1696070",
980 4 : "initdb_lsn": "0/1696070",
981 4 : "pg_version": 14
982 4 : },
983 4 : "gc_blocking": {
984 4 : "started_at": "2024-07-19T09:00:00.123",
985 4 : "reasons": ["DetachAncestor"]
986 4 : },
987 4 : "import_pgdata": {
988 4 : "V1": {
989 4 : "Done": {
990 4 : "idempotency_key": "specified-by-client-218a5213-5044-4562-a28d-d024c5f057f5",
991 4 : "started_at": "2024-11-13T09:23:42.123",
992 4 : "finished_at": "2024-11-13T09:42:23.123"
993 4 : }
994 4 : }
995 4 : }
996 4 : }"#;
997 4 :
998 4 : let expected = IndexPart {
999 4 : version: 10,
1000 4 : layer_metadata: HashMap::from([
1001 4 : ("000000000000000000000000000000000000-FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF__0000000001696070-00000000016960E9".parse().unwrap(), LayerFileMetadata {
1002 4 : file_size: 25600000,
1003 4 : generation: Generation::none(),
1004 4 : shard: ShardIndex::unsharded()
1005 4 : }),
1006 4 : ("000000000000000000000000000000000000-FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF__00000000016B59D8-00000000016B5A51".parse().unwrap(), LayerFileMetadata {
1007 4 : file_size: 9007199254741001,
1008 4 : generation: Generation::none(),
1009 4 : shard: ShardIndex::unsharded()
1010 4 : })
1011 4 : ]),
1012 4 : disk_consistent_lsn: "0/16960E8".parse::<Lsn>().unwrap(),
1013 4 : metadata: TimelineMetadata::new(
1014 4 : Lsn::from_str("0/16960E8").unwrap(),
1015 4 : Some(Lsn::from_str("0/1696070").unwrap()),
1016 4 : Some(TimelineId::from_str("e45a7f37d3ee2ff17dc14bf4f4e3f52e").unwrap()),
1017 4 : Lsn::INVALID,
1018 4 : Lsn::from_str("0/1696070").unwrap(),
1019 4 : Lsn::from_str("0/1696070").unwrap(),
1020 4 : 14,
1021 4 : ).with_recalculated_checksum().unwrap(),
1022 4 : deleted_at: None,
1023 4 : lineage: Default::default(),
1024 4 : gc_blocking: Some(GcBlocking {
1025 4 : started_at: parse_naive_datetime("2024-07-19T09:00:00.123000000"),
1026 4 : reasons: enumset::EnumSet::from_iter([GcBlockingReason::DetachAncestor]),
1027 4 : }),
1028 4 : last_aux_file_policy: Default::default(),
1029 4 : archived_at: None,
1030 4 : import_pgdata: Some(import_pgdata::index_part_format::Root::V1(import_pgdata::index_part_format::V1::Done(import_pgdata::index_part_format::Done{
1031 4 : started_at: parse_naive_datetime("2024-11-13T09:23:42.123000000"),
1032 4 : finished_at: parse_naive_datetime("2024-11-13T09:42:23.123000000"),
1033 4 : idempotency_key: import_pgdata::index_part_format::IdempotencyKey::new("specified-by-client-218a5213-5044-4562-a28d-d024c5f057f5".to_string()),
1034 4 : }))),
1035 4 : rel_size_migration: None,
1036 4 : l2_lsn: None,
1037 4 : gc_compaction: None,
1038 4 : };
1039 4 :
1040 4 : let part = IndexPart::from_json_bytes(example.as_bytes()).unwrap();
1041 4 : assert_eq!(part, expected);
1042 4 : }
1043 :
1044 : #[test]
1045 4 : fn v11_rel_size_migration_is_parsed() {
1046 4 : let example = r#"{
1047 4 : "version": 11,
1048 4 : "layer_metadata":{
1049 4 : "000000000000000000000000000000000000-FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF__0000000001696070-00000000016960E9": { "file_size": 25600000 },
1050 4 : "000000000000000000000000000000000000-FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF__00000000016B59D8-00000000016B5A51": { "file_size": 9007199254741001 }
1051 4 : },
1052 4 : "disk_consistent_lsn":"0/16960E8",
1053 4 : "metadata": {
1054 4 : "disk_consistent_lsn": "0/16960E8",
1055 4 : "prev_record_lsn": "0/1696070",
1056 4 : "ancestor_timeline": "e45a7f37d3ee2ff17dc14bf4f4e3f52e",
1057 4 : "ancestor_lsn": "0/0",
1058 4 : "latest_gc_cutoff_lsn": "0/1696070",
1059 4 : "initdb_lsn": "0/1696070",
1060 4 : "pg_version": 14
1061 4 : },
1062 4 : "gc_blocking": {
1063 4 : "started_at": "2024-07-19T09:00:00.123",
1064 4 : "reasons": ["DetachAncestor"]
1065 4 : },
1066 4 : "import_pgdata": {
1067 4 : "V1": {
1068 4 : "Done": {
1069 4 : "idempotency_key": "specified-by-client-218a5213-5044-4562-a28d-d024c5f057f5",
1070 4 : "started_at": "2024-11-13T09:23:42.123",
1071 4 : "finished_at": "2024-11-13T09:42:23.123"
1072 4 : }
1073 4 : }
1074 4 : },
1075 4 : "rel_size_migration": "legacy"
1076 4 : }"#;
1077 4 :
1078 4 : let expected = IndexPart {
1079 4 : version: 11,
1080 4 : layer_metadata: HashMap::from([
1081 4 : ("000000000000000000000000000000000000-FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF__0000000001696070-00000000016960E9".parse().unwrap(), LayerFileMetadata {
1082 4 : file_size: 25600000,
1083 4 : generation: Generation::none(),
1084 4 : shard: ShardIndex::unsharded()
1085 4 : }),
1086 4 : ("000000000000000000000000000000000000-FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF__00000000016B59D8-00000000016B5A51".parse().unwrap(), LayerFileMetadata {
1087 4 : file_size: 9007199254741001,
1088 4 : generation: Generation::none(),
1089 4 : shard: ShardIndex::unsharded()
1090 4 : })
1091 4 : ]),
1092 4 : disk_consistent_lsn: "0/16960E8".parse::<Lsn>().unwrap(),
1093 4 : metadata: TimelineMetadata::new(
1094 4 : Lsn::from_str("0/16960E8").unwrap(),
1095 4 : Some(Lsn::from_str("0/1696070").unwrap()),
1096 4 : Some(TimelineId::from_str("e45a7f37d3ee2ff17dc14bf4f4e3f52e").unwrap()),
1097 4 : Lsn::INVALID,
1098 4 : Lsn::from_str("0/1696070").unwrap(),
1099 4 : Lsn::from_str("0/1696070").unwrap(),
1100 4 : 14,
1101 4 : ).with_recalculated_checksum().unwrap(),
1102 4 : deleted_at: None,
1103 4 : lineage: Default::default(),
1104 4 : gc_blocking: Some(GcBlocking {
1105 4 : started_at: parse_naive_datetime("2024-07-19T09:00:00.123000000"),
1106 4 : reasons: enumset::EnumSet::from_iter([GcBlockingReason::DetachAncestor]),
1107 4 : }),
1108 4 : last_aux_file_policy: Default::default(),
1109 4 : archived_at: None,
1110 4 : import_pgdata: Some(import_pgdata::index_part_format::Root::V1(import_pgdata::index_part_format::V1::Done(import_pgdata::index_part_format::Done{
1111 4 : started_at: parse_naive_datetime("2024-11-13T09:23:42.123000000"),
1112 4 : finished_at: parse_naive_datetime("2024-11-13T09:42:23.123000000"),
1113 4 : idempotency_key: import_pgdata::index_part_format::IdempotencyKey::new("specified-by-client-218a5213-5044-4562-a28d-d024c5f057f5".to_string()),
1114 4 : }))),
1115 4 : rel_size_migration: Some(RelSizeMigration::Legacy),
1116 4 : l2_lsn: None,
1117 4 : gc_compaction: None,
1118 4 : };
1119 4 :
1120 4 : let part = IndexPart::from_json_bytes(example.as_bytes()).unwrap();
1121 4 : assert_eq!(part, expected);
1122 4 : }
1123 :
1124 : #[test]
1125 4 : fn v12_v13_l2_gc_ompaction_is_parsed() {
1126 4 : let example = r#"{
1127 4 : "version": 12,
1128 4 : "layer_metadata":{
1129 4 : "000000000000000000000000000000000000-FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF__0000000001696070-00000000016960E9": { "file_size": 25600000 },
1130 4 : "000000000000000000000000000000000000-FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF__00000000016B59D8-00000000016B5A51": { "file_size": 9007199254741001 }
1131 4 : },
1132 4 : "disk_consistent_lsn":"0/16960E8",
1133 4 : "metadata": {
1134 4 : "disk_consistent_lsn": "0/16960E8",
1135 4 : "prev_record_lsn": "0/1696070",
1136 4 : "ancestor_timeline": "e45a7f37d3ee2ff17dc14bf4f4e3f52e",
1137 4 : "ancestor_lsn": "0/0",
1138 4 : "latest_gc_cutoff_lsn": "0/1696070",
1139 4 : "initdb_lsn": "0/1696070",
1140 4 : "pg_version": 14
1141 4 : },
1142 4 : "gc_blocking": {
1143 4 : "started_at": "2024-07-19T09:00:00.123",
1144 4 : "reasons": ["DetachAncestor"]
1145 4 : },
1146 4 : "import_pgdata": {
1147 4 : "V1": {
1148 4 : "Done": {
1149 4 : "idempotency_key": "specified-by-client-218a5213-5044-4562-a28d-d024c5f057f5",
1150 4 : "started_at": "2024-11-13T09:23:42.123",
1151 4 : "finished_at": "2024-11-13T09:42:23.123"
1152 4 : }
1153 4 : }
1154 4 : },
1155 4 : "rel_size_migration": "legacy",
1156 4 : "l2_lsn": "0/16960E8",
1157 4 : "gc_compaction": {
1158 4 : "last_completed_lsn": "0/16960E8"
1159 4 : }
1160 4 : }"#;
1161 4 :
1162 4 : let expected = IndexPart {
1163 4 : version: 12,
1164 4 : layer_metadata: HashMap::from([
1165 4 : ("000000000000000000000000000000000000-FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF__0000000001696070-00000000016960E9".parse().unwrap(), LayerFileMetadata {
1166 4 : file_size: 25600000,
1167 4 : generation: Generation::none(),
1168 4 : shard: ShardIndex::unsharded()
1169 4 : }),
1170 4 : ("000000000000000000000000000000000000-FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF__00000000016B59D8-00000000016B5A51".parse().unwrap(), LayerFileMetadata {
1171 4 : file_size: 9007199254741001,
1172 4 : generation: Generation::none(),
1173 4 : shard: ShardIndex::unsharded()
1174 4 : })
1175 4 : ]),
1176 4 : disk_consistent_lsn: "0/16960E8".parse::<Lsn>().unwrap(),
1177 4 : metadata: TimelineMetadata::new(
1178 4 : Lsn::from_str("0/16960E8").unwrap(),
1179 4 : Some(Lsn::from_str("0/1696070").unwrap()),
1180 4 : Some(TimelineId::from_str("e45a7f37d3ee2ff17dc14bf4f4e3f52e").unwrap()),
1181 4 : Lsn::INVALID,
1182 4 : Lsn::from_str("0/1696070").unwrap(),
1183 4 : Lsn::from_str("0/1696070").unwrap(),
1184 4 : 14,
1185 4 : ).with_recalculated_checksum().unwrap(),
1186 4 : deleted_at: None,
1187 4 : lineage: Default::default(),
1188 4 : gc_blocking: Some(GcBlocking {
1189 4 : started_at: parse_naive_datetime("2024-07-19T09:00:00.123000000"),
1190 4 : reasons: enumset::EnumSet::from_iter([GcBlockingReason::DetachAncestor]),
1191 4 : }),
1192 4 : last_aux_file_policy: Default::default(),
1193 4 : archived_at: None,
1194 4 : import_pgdata: Some(import_pgdata::index_part_format::Root::V1(import_pgdata::index_part_format::V1::Done(import_pgdata::index_part_format::Done{
1195 4 : started_at: parse_naive_datetime("2024-11-13T09:23:42.123000000"),
1196 4 : finished_at: parse_naive_datetime("2024-11-13T09:42:23.123000000"),
1197 4 : idempotency_key: import_pgdata::index_part_format::IdempotencyKey::new("specified-by-client-218a5213-5044-4562-a28d-d024c5f057f5".to_string()),
1198 4 : }))),
1199 4 : rel_size_migration: Some(RelSizeMigration::Legacy),
1200 4 : l2_lsn: Some("0/16960E8".parse::<Lsn>().unwrap()),
1201 4 : gc_compaction: Some(GcCompactionState {
1202 4 : last_completed_lsn: "0/16960E8".parse::<Lsn>().unwrap(),
1203 4 : }),
1204 4 : };
1205 4 :
1206 4 : let part = IndexPart::from_json_bytes(example.as_bytes()).unwrap();
1207 4 : assert_eq!(part, expected);
1208 4 : }
1209 :
1210 72 : fn parse_naive_datetime(s: &str) -> NaiveDateTime {
1211 72 : chrono::NaiveDateTime::parse_from_str(s, "%Y-%m-%dT%H:%M:%S.%f").unwrap()
1212 72 : }
1213 : }
|