Skip to main content

iceberg/spec/
table_metadata.rs

1// Licensed to the Apache Software Foundation (ASF) under one
2// or more contributor license agreements.  See the NOTICE file
3// distributed with this work for additional information
4// regarding copyright ownership.  The ASF licenses this file
5// to you under the Apache License, Version 2.0 (the
6// "License"); you may not use this file except in compliance
7// with the License.  You may obtain a copy of the License at
8//
9//   http://www.apache.org/licenses/LICENSE-2.0
10//
11// Unless required by applicable law or agreed to in writing,
12// software distributed under the License is distributed on an
13// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14// KIND, either express or implied.  See the License for the
15// specific language governing permissions and limitations
16// under the License.
17
18//! Defines the [table metadata](https://iceberg.apache.org/spec/#table-metadata).
19//! The main struct here is [TableMetadataV2] which defines the data for a table.
20
21use std::cmp::Ordering;
22use std::collections::HashMap;
23use std::fmt::{Display, Formatter};
24use std::hash::Hash;
25use std::sync::Arc;
26
27use _serde::TableMetadataEnum;
28use chrono::{DateTime, Utc};
29use serde::{Deserialize, Serialize};
30use serde_repr::{Deserialize_repr, Serialize_repr};
31use uuid::Uuid;
32
33use super::snapshot::SnapshotReference;
34pub use super::table_metadata_builder::{TableMetadataBuildResult, TableMetadataBuilder};
35use super::{
36    DEFAULT_PARTITION_SPEC_ID, PartitionSpecRef, PartitionStatisticsFile, Schema, SchemaId,
37    SchemaRef, SnapshotRef, SnapshotRetention, SortOrder, SortOrderRef, StatisticsFile, StructType,
38    TableProperties, Transform,
39};
40use crate::catalog::{METADATA_FOLDER_NAME, MetadataLocation};
41use crate::compression::CompressionCodec;
42use crate::error::{Result, invalid_data, timestamp_ms_to_utc};
43use crate::io::FileIO;
44use crate::partitioning::compute_unified_partition_type;
45use crate::spec::EncryptedKey;
46use crate::{Error, ErrorKind};
47
48static MAIN_BRANCH: &str = "main";
49pub(crate) static ONE_MINUTE_MS: i64 = 60_000;
50
51/// Sentinel value used by the Java implementation and older metadata files
52/// to represent a missing/empty current snapshot ID. During deserialization,
53/// this value is normalized to `None`.
54pub(crate) static EMPTY_SNAPSHOT_ID: i64 = -1;
55pub(crate) static INITIAL_SEQUENCE_NUMBER: i64 = 0;
56
57/// Initial row id for row lineage for new v3 tables and older tables upgrading to v3.
58pub const INITIAL_ROW_ID: u64 = 0;
59/// Minimum format version that supports row lineage (v3).
60pub const MIN_FORMAT_VERSION_ROW_LINEAGE: FormatVersion = FormatVersion::V3;
61/// Reference to [`TableMetadata`].
62pub type TableMetadataRef = Arc<TableMetadata>;
63
64#[derive(Debug, PartialEq, Deserialize, Eq, Clone)]
65#[serde(try_from = "TableMetadataEnum")]
66/// Fields for the version 2 of the table metadata.
67///
68/// We assume that this data structure is always valid, so we will panic when invalid error happens.
69/// We check the validity of this data structure when constructing.
70pub struct TableMetadata {
71    /// Integer Version for the format.
72    pub(crate) format_version: FormatVersion,
73    /// A UUID that identifies the table
74    pub(crate) table_uuid: Uuid,
75    /// Location tables base location
76    pub(crate) location: String,
77    /// The tables highest sequence number
78    pub(crate) last_sequence_number: i64,
79    /// Timestamp in milliseconds from the unix epoch when the table was last updated.
80    pub(crate) last_updated_ms: i64,
81    /// An integer; the highest assigned column ID for the table.
82    pub(crate) last_column_id: i32,
83    /// A list of schemas, stored as objects with schema-id.
84    pub(crate) schemas: HashMap<i32, SchemaRef>,
85    /// ID of the table’s current schema.
86    pub(crate) current_schema_id: i32,
87    /// A list of partition specs, stored as full partition spec objects.
88    pub(crate) partition_specs: HashMap<i32, PartitionSpecRef>,
89    /// ID of the “current” spec that writers should use by default.
90    pub(crate) default_spec: PartitionSpecRef,
91    /// Partition type of the default partition spec.
92    pub(crate) default_partition_type: StructType,
93    /// An integer; the highest assigned partition field ID across all partition specs for the table.
94    pub(crate) last_partition_id: i32,
95    ///A string to string map of table properties. This is used to control settings that
96    /// affect reading and writing and is not intended to be used for arbitrary metadata.
97    /// For example, commit.retry.num-retries is used to control the number of commit retries.
98    pub(crate) properties: HashMap<String, String>,
99    /// long ID of the current table snapshot; must be the same as the current
100    /// ID of the main branch in refs.
101    pub(crate) current_snapshot_id: Option<i64>,
102    ///A list of valid snapshots. Valid snapshots are snapshots for which all
103    /// data files exist in the file system. A data file must not be deleted
104    /// from the file system until the last snapshot in which it was listed is
105    /// garbage collected.
106    pub(crate) snapshots: HashMap<i64, SnapshotRef>,
107    /// A list (optional) of timestamp and snapshot ID pairs that encodes changes
108    /// to the current snapshot for the table. Each time the current-snapshot-id
109    /// is changed, a new entry should be added with the last-updated-ms
110    /// and the new current-snapshot-id. When snapshots are expired from
111    /// the list of valid snapshots, all entries before a snapshot that has
112    /// expired should be removed.
113    pub(crate) snapshot_log: Vec<SnapshotLog>,
114
115    /// A list (optional) of timestamp and metadata file location pairs
116    /// that encodes changes to the previous metadata files for the table.
117    /// Each time a new metadata file is created, a new entry of the
118    /// previous metadata file location should be added to the list.
119    /// Tables can be configured to remove the oldest metadata log entries and
120    /// keep a fixed-size log of the most recent entries after a commit.
121    pub(crate) metadata_log: Vec<MetadataLog>,
122
123    /// A list of sort orders, stored as full sort order objects.
124    pub(crate) sort_orders: HashMap<i64, SortOrderRef>,
125    /// Default sort order id of the table. Note that this could be used by
126    /// writers, but is not used when reading because reads use the specs
127    /// stored in manifest files.
128    pub(crate) default_sort_order_id: i64,
129    /// A map of snapshot references. The map keys are the unique snapshot reference
130    /// names in the table, and the map values are snapshot reference objects.
131    /// There is always a main branch reference pointing to the current-snapshot-id
132    /// even if the refs map is null.
133    pub(crate) refs: HashMap<String, SnapshotReference>,
134    /// Mapping of snapshot ids to statistics files.
135    pub(crate) statistics: HashMap<i64, StatisticsFile>,
136    /// Mapping of snapshot ids to partition statistics files.
137    pub(crate) partition_statistics: HashMap<i64, PartitionStatisticsFile>,
138    /// Encryption Keys - map of key id to the actual key
139    pub(crate) encryption_keys: HashMap<String, EncryptedKey>,
140    /// Next row id to be assigned for Row Lineage (v3)
141    pub(crate) next_row_id: u64,
142}
143
144impl TableMetadata {
145    /// Convert this Table Metadata into a builder for modification.
146    ///
147    /// `current_file_location` is the location where the current version
148    /// of the metadata file is stored. This is used to update the metadata log.
149    /// If `current_file_location` is `None`, the metadata log will not be updated.
150    /// This should only be used to stage-create tables.
151    #[must_use]
152    pub fn into_builder(self, current_file_location: Option<String>) -> TableMetadataBuilder {
153        TableMetadataBuilder::new_from_metadata(self, current_file_location)
154    }
155
156    /// Check if a field name exists in any schema.
157    #[inline]
158    pub(crate) fn name_exists_in_any_schema(&self, name: &str) -> bool {
159        self.schemas
160            .values()
161            .any(|schema| schema.field_by_name(name).is_some())
162    }
163
164    /// Returns format version of this metadata.
165    #[inline]
166    pub fn format_version(&self) -> FormatVersion {
167        self.format_version
168    }
169
170    /// Returns uuid of current table.
171    #[inline]
172    pub fn uuid(&self) -> Uuid {
173        self.table_uuid
174    }
175
176    /// Returns table location.
177    #[inline]
178    pub fn location(&self) -> &str {
179        self.location.as_str()
180    }
181
182    /// Returns last sequence number.
183    #[inline]
184    pub fn last_sequence_number(&self) -> i64 {
185        self.last_sequence_number
186    }
187
188    /// Returns the next sequence number for the table.
189    ///
190    /// For format version 1, it always returns the initial sequence number.
191    /// For other versions, it returns the last sequence number incremented by 1.
192    #[inline]
193    pub fn next_sequence_number(&self) -> i64 {
194        match self.format_version {
195            FormatVersion::V1 => INITIAL_SEQUENCE_NUMBER,
196            _ => self.last_sequence_number + 1,
197        }
198    }
199
200    /// Returns the last column id.
201    #[inline]
202    pub fn last_column_id(&self) -> i32 {
203        self.last_column_id
204    }
205
206    /// Returns the last partition_id
207    #[inline]
208    pub fn last_partition_id(&self) -> i32 {
209        self.last_partition_id
210    }
211
212    /// Returns last updated time.
213    #[inline]
214    pub fn last_updated_timestamp(&self) -> Result<DateTime<Utc>> {
215        timestamp_ms_to_utc(self.last_updated_ms)
216    }
217
218    /// Returns last updated time in milliseconds.
219    #[inline]
220    pub fn last_updated_ms(&self) -> i64 {
221        self.last_updated_ms
222    }
223
224    /// Returns schemas
225    #[inline]
226    pub fn schemas_iter(&self) -> impl ExactSizeIterator<Item = &SchemaRef> {
227        self.schemas.values()
228    }
229
230    /// Lookup schema by id.
231    #[inline]
232    pub fn schema_by_id(&self, schema_id: SchemaId) -> Option<&SchemaRef> {
233        self.schemas.get(&schema_id)
234    }
235
236    /// Get current schema
237    #[inline]
238    pub fn current_schema(&self) -> &SchemaRef {
239        self.schema_by_id(self.current_schema_id)
240            .expect("Current schema id set, but not found in table metadata")
241    }
242
243    /// Get the id of the current schema
244    #[inline]
245    pub fn current_schema_id(&self) -> SchemaId {
246        self.current_schema_id
247    }
248
249    /// Returns all partition specs.
250    #[inline]
251    pub fn partition_specs_iter(&self) -> impl ExactSizeIterator<Item = &PartitionSpecRef> {
252        self.partition_specs.values()
253    }
254
255    /// Lookup partition spec by id.
256    #[inline]
257    pub fn partition_spec_by_id(&self, spec_id: i32) -> Option<&PartitionSpecRef> {
258        self.partition_specs.get(&spec_id)
259    }
260
261    /// Get default partition spec
262    #[inline]
263    pub fn default_partition_spec(&self) -> &PartitionSpecRef {
264        &self.default_spec
265    }
266
267    /// Return the partition type of the default partition spec.
268    #[inline]
269    pub fn default_partition_type(&self) -> &StructType {
270        &self.default_partition_type
271    }
272
273    /// Returns the unified partition type across every partition spec in the table, resolved
274    /// against `schema`.
275    ///
276    /// Unlike [`Self::default_partition_type`], the result contains all partition fields ever
277    /// used by the table, so partition values stay readable across partition spec evolution.
278    /// See [`compute_unified_partition_type`] for the exact merge rules.
279    pub fn unified_partition_type(&self, schema: &Schema) -> Result<StructType> {
280        compute_unified_partition_type(
281            self.partition_specs_iter().map(|spec| spec.as_ref()),
282            schema,
283        )
284    }
285
286    #[inline]
287    /// Returns spec id of the "current" partition spec.
288    pub fn default_partition_spec_id(&self) -> i32 {
289        self.default_spec.spec_id()
290    }
291
292    /// Returns all snapshots
293    #[inline]
294    pub fn snapshots(&self) -> impl ExactSizeIterator<Item = &SnapshotRef> {
295        self.snapshots.values()
296    }
297
298    /// Lookup snapshot by id.
299    #[inline]
300    pub fn snapshot_by_id(&self, snapshot_id: i64) -> Option<&SnapshotRef> {
301        self.snapshots.get(&snapshot_id)
302    }
303
304    /// Returns snapshot history.
305    #[inline]
306    pub fn history(&self) -> &[SnapshotLog] {
307        &self.snapshot_log
308    }
309
310    /// Returns the metadata log.
311    #[inline]
312    pub fn metadata_log(&self) -> &[MetadataLog] {
313        &self.metadata_log
314    }
315
316    /// Get current snapshot
317    #[inline]
318    pub fn current_snapshot(&self) -> Option<&SnapshotRef> {
319        self.current_snapshot_id.map(|s| {
320            self.snapshot_by_id(s)
321                .expect("Current snapshot id has been set, but doesn't exist in metadata")
322        })
323    }
324
325    /// Get the current snapshot id
326    #[inline]
327    pub fn current_snapshot_id(&self) -> Option<i64> {
328        self.current_snapshot_id
329    }
330
331    /// Get the snapshot for a reference
332    /// Returns an option if the `ref_name` is not found
333    #[inline]
334    pub fn snapshot_for_ref(&self, ref_name: &str) -> Option<&SnapshotRef> {
335        self.refs.get(ref_name).map(|r| {
336            self.snapshot_by_id(r.snapshot_id)
337                .unwrap_or_else(|| panic!("Snapshot id of ref {ref_name} doesn't exist"))
338        })
339    }
340
341    /// Return all sort orders.
342    #[inline]
343    pub fn sort_orders_iter(&self) -> impl ExactSizeIterator<Item = &SortOrderRef> {
344        self.sort_orders.values()
345    }
346
347    /// Lookup sort order by id.
348    #[inline]
349    pub fn sort_order_by_id(&self, sort_order_id: i64) -> Option<&SortOrderRef> {
350        self.sort_orders.get(&sort_order_id)
351    }
352
353    /// Returns default sort order id.
354    #[inline]
355    pub fn default_sort_order(&self) -> &SortOrderRef {
356        self.sort_orders
357            .get(&self.default_sort_order_id)
358            .expect("Default order id has been set, but not found in table metadata!")
359    }
360
361    /// Returns default sort order id.
362    #[inline]
363    pub fn default_sort_order_id(&self) -> i64 {
364        self.default_sort_order_id
365    }
366
367    /// Returns properties of table.
368    #[inline]
369    pub fn properties(&self) -> &HashMap<String, String> {
370        &self.properties
371    }
372
373    /// Returns the base location for metadata files (manifests, manifest lists).
374    ///
375    /// Honors the `write.metadata.path` table property when set, otherwise defaults
376    /// to the `metadata` subdirectory under the table location.
377    pub fn metadata_location(&self) -> Result<String> {
378        Ok(self
379            .table_properties()
380            .write_metadata_path()?
381            .unwrap_or_else(|| format!("{}/{}", self.location(), METADATA_FOLDER_NAME)))
382    }
383
384    /// Returns the metadata compression codec from table properties.
385    ///
386    /// Returns `CompressionCodec::None` if compression is disabled or not configured.
387    /// Returns `CompressionCodec::Gzip` if gzip compression is enabled.
388    ///
389    /// # Errors
390    ///
391    /// Returns an error if the compression codec property has an invalid value.
392    pub fn metadata_compression_codec(&self) -> Result<CompressionCodec> {
393        self.table_properties().metadata_compression_codec()
394    }
395
396    /// Returns a typed view that parses each table property when its getter is called.
397    #[inline]
398    pub fn table_properties(&self) -> TableProperties<'_> {
399        TableProperties::new(&self.properties)
400    }
401
402    /// Return location of statistics files.
403    #[inline]
404    pub fn statistics_iter(&self) -> impl ExactSizeIterator<Item = &StatisticsFile> {
405        self.statistics.values()
406    }
407
408    /// Return location of partition statistics files.
409    #[inline]
410    pub fn partition_statistics_iter(
411        &self,
412    ) -> impl ExactSizeIterator<Item = &PartitionStatisticsFile> {
413        self.partition_statistics.values()
414    }
415
416    /// Get a statistics file for a snapshot id.
417    #[inline]
418    pub fn statistics_for_snapshot(&self, snapshot_id: i64) -> Option<&StatisticsFile> {
419        self.statistics.get(&snapshot_id)
420    }
421
422    /// Get a partition statistics file for a snapshot id.
423    #[inline]
424    pub fn partition_statistics_for_snapshot(
425        &self,
426        snapshot_id: i64,
427    ) -> Option<&PartitionStatisticsFile> {
428        self.partition_statistics.get(&snapshot_id)
429    }
430
431    fn construct_refs(&mut self) {
432        if let Some(current_snapshot_id) = self.current_snapshot_id
433            && !self.refs.contains_key(MAIN_BRANCH)
434        {
435            self.refs
436                .insert(MAIN_BRANCH.to_string(), SnapshotReference {
437                    snapshot_id: current_snapshot_id,
438                    retention: SnapshotRetention::Branch {
439                        min_snapshots_to_keep: None,
440                        max_snapshot_age_ms: None,
441                        max_ref_age_ms: None,
442                    },
443                });
444        }
445    }
446
447    /// Iterate over all encryption keys
448    #[inline]
449    pub fn encryption_keys_iter(&self) -> impl ExactSizeIterator<Item = &EncryptedKey> {
450        self.encryption_keys.values()
451    }
452
453    /// Get the encryption key for a given key id
454    #[inline]
455    pub fn encryption_key(&self, key_id: &str) -> Option<&EncryptedKey> {
456        self.encryption_keys.get(key_id)
457    }
458
459    /// Get the next row id to be assigned
460    #[inline]
461    pub fn next_row_id(&self) -> u64 {
462        self.next_row_id
463    }
464
465    /// Read table metadata from the given location.
466    pub async fn read_from(
467        file_io: &FileIO,
468        metadata_location: impl AsRef<str>,
469    ) -> Result<TableMetadata> {
470        let metadata_location = metadata_location.as_ref();
471        let input_file = file_io.new_input(metadata_location)?;
472        let metadata_content = input_file.read().await?;
473
474        // Check if the file is compressed by looking for the gzip "magic number".
475        let metadata = if metadata_content.len() > 2
476            && metadata_content[0] == 0x1F
477            && metadata_content[1] == 0x8B
478        {
479            let decompressed_data = CompressionCodec::gzip_default()
480                .decompress(metadata_content.to_vec())
481                .map_err(|e| {
482                    invalid_data!("Trying to read compressed metadata file")
483                        .with_context("file_path", metadata_location)
484                        .with_source(e)
485                })?;
486            serde_json::from_slice(&decompressed_data)?
487        } else {
488            serde_json::from_slice(&metadata_content)?
489        };
490
491        Ok(metadata)
492    }
493
494    /// Write table metadata to the given location.
495    pub async fn write_to(
496        &self,
497        file_io: &FileIO,
498        metadata_location: &MetadataLocation,
499    ) -> Result<()> {
500        let json_data = serde_json::to_vec(self)?;
501
502        // Check if compression codec from properties matches the one in metadata_location
503        let codec = self.table_properties().metadata_compression_codec()?;
504
505        if codec != metadata_location.compression_codec() {
506            return Err(invalid_data!(
507                "Compression codec mismatch: metadata_location has {:?}, but table properties specify {:?}",
508                metadata_location.compression_codec(),
509                codec
510            ));
511        }
512
513        // Apply compression based on codec
514        let data_to_write = match codec {
515            CompressionCodec::Gzip(_) => codec.compress(json_data)?,
516            CompressionCodec::None => json_data,
517            _ => {
518                return Err(invalid_data!(
519                    "Unsupported metadata compression codec: {codec:?}"
520                ));
521            }
522        };
523
524        file_io
525            .new_output(metadata_location.to_string())?
526            .write(data_to_write.into())
527            .await
528    }
529
530    /// Normalize this partition spec.
531    ///
532    /// This is an internal method
533    /// meant to be called after constructing table metadata from untrusted sources.
534    /// We run this method after json deserialization.
535    /// All constructors for `TableMetadata` which are part of `iceberg-rust`
536    /// should return normalized `TableMetadata`.
537    pub(super) fn try_normalize(&mut self) -> Result<&mut Self> {
538        self.validate_current_schema()?;
539        self.normalize_current_snapshot()?;
540        self.construct_refs();
541        self.validate_refs()?;
542        self.validate_chronological_snapshot_logs()?;
543        self.validate_chronological_metadata_logs()?;
544        // Normalize location (remove trailing slash)
545        self.location = self.location.trim_end_matches('/').to_string();
546        self.validate_snapshot_sequence_number()?;
547        self.validate_schema_format_compatibility()?;
548        self.try_normalize_partition_spec()?;
549        self.try_normalize_sort_order()?;
550        Ok(self)
551    }
552
553    /// Validate active default-spec sources and add the spec if it is not present.
554    fn try_normalize_partition_spec(&mut self) -> Result<()> {
555        for field in self.default_spec.fields() {
556            // Historical specs may reference dropped columns, but active default-spec
557            // fields need a source for new writes. Void fields do not read their source.
558            if field.transform != Transform::Void
559                && self.current_schema().field_by_id(field.source_id).is_none()
560            {
561                return Err(invalid_data!(
562                    "Default partition spec {} references missing source field {} in current schema {}",
563                    self.default_spec.spec_id(),
564                    field.source_id,
565                    self.current_schema_id
566                ));
567            }
568        }
569
570        if self
571            .partition_spec_by_id(self.default_spec.spec_id())
572            .is_none()
573        {
574            self.partition_specs.insert(
575                self.default_spec.spec_id(),
576                Arc::new(Arc::unwrap_or_clone(self.default_spec.clone())),
577            );
578        }
579
580        Ok(())
581    }
582
583    /// If the default sort order is unsorted but the sort order is not present, add it
584    fn try_normalize_sort_order(&mut self) -> Result<()> {
585        // Validate that sort order ID 0 (reserved for unsorted) has no fields
586        if let Some(sort_order) = self.sort_order_by_id(SortOrder::UNSORTED_ORDER_ID)
587            && !sort_order.fields.is_empty()
588        {
589            return Err(Error::new(
590                ErrorKind::Unexpected,
591                format!(
592                    "Sort order ID {} is reserved for unsorted order",
593                    SortOrder::UNSORTED_ORDER_ID
594                ),
595            ));
596        }
597
598        if self.sort_order_by_id(self.default_sort_order_id).is_some() {
599            return Ok(());
600        }
601
602        if self.default_sort_order_id != SortOrder::UNSORTED_ORDER_ID {
603            return Err(invalid_data!(
604                "No sort order exists with the default sort order id {}.",
605                self.default_sort_order_id
606            ));
607        }
608
609        let sort_order = SortOrder::unsorted_order();
610        self.sort_orders
611            .insert(SortOrder::UNSORTED_ORDER_ID, Arc::new(sort_order));
612        Ok(())
613    }
614
615    /// Validate the current schema is set and exists.
616    fn validate_current_schema(&self) -> Result<()> {
617        if self.schema_by_id(self.current_schema_id).is_none() {
618            return Err(invalid_data!(
619                "No schema exists with the current schema id {}.",
620                self.current_schema_id
621            ));
622        }
623        Ok(())
624    }
625
626    /// If current snapshot is Some(-1) then set it to None.
627    fn normalize_current_snapshot(&mut self) -> Result<()> {
628        if let Some(current_snapshot_id) = self.current_snapshot_id {
629            if current_snapshot_id == EMPTY_SNAPSHOT_ID {
630                self.current_snapshot_id = None;
631            } else if self.snapshot_by_id(current_snapshot_id).is_none() {
632                return Err(invalid_data!(
633                    "Snapshot for current snapshot id {current_snapshot_id} does not exist in the existing snapshots list"
634                ));
635            }
636        }
637        Ok(())
638    }
639
640    /// Validate that all refs are valid (snapshot exists)
641    fn validate_refs(&self) -> Result<()> {
642        for (name, snapshot_ref) in self.refs.iter() {
643            if self.snapshot_by_id(snapshot_ref.snapshot_id).is_none() {
644                return Err(invalid_data!(
645                    "Snapshot for reference {name} does not exist in the existing snapshots list"
646                ));
647            }
648        }
649
650        let main_ref = self.refs.get(MAIN_BRANCH);
651        if self.current_snapshot_id.is_some() {
652            if let Some(main_ref) = main_ref
653                && main_ref.snapshot_id != self.current_snapshot_id.unwrap_or_default()
654            {
655                return Err(invalid_data!(
656                    "Current snapshot id does not match main branch ({:?} != {:?})",
657                    self.current_snapshot_id.unwrap_or_default(),
658                    main_ref.snapshot_id
659                ));
660            }
661        } else if main_ref.is_some() {
662            return Err(invalid_data!(
663                "Current snapshot is not set, but main branch exists"
664            ));
665        }
666
667        Ok(())
668    }
669
670    /// Validate that for V1 Metadata the last_sequence_number is 0
671    fn validate_snapshot_sequence_number(&self) -> Result<()> {
672        if self.format_version < FormatVersion::V2 && self.last_sequence_number != 0 {
673            return Err(invalid_data!(
674                "Last sequence number must be 0 in v1. Found {}",
675                self.last_sequence_number
676            ));
677        }
678
679        if self.format_version >= FormatVersion::V2
680            && let Some(snapshot) = self
681                .snapshots
682                .values()
683                .find(|snapshot| snapshot.sequence_number() > self.last_sequence_number)
684        {
685            return Err(invalid_data!(
686                "Invalid snapshot with id {} and sequence number {} greater than last sequence number {}",
687                snapshot.snapshot_id(),
688                snapshot.sequence_number(),
689                self.last_sequence_number
690            ));
691        }
692
693        Ok(())
694    }
695
696    /// Validate snapshots logs are chronological and last updated is after the last snapshot log.
697    fn validate_chronological_snapshot_logs(&self) -> Result<()> {
698        for window in self.snapshot_log.windows(2) {
699            let (prev, curr) = (&window[0], &window[1]);
700            // commits can happen concurrently from different machines.
701            // A tolerance helps us avoid failure for small clock skew
702            if curr.timestamp_ms - prev.timestamp_ms < -ONE_MINUTE_MS {
703                return Err(invalid_data!("Expected sorted snapshot log entries"));
704            }
705        }
706
707        if let Some(last) = self.snapshot_log.last() {
708            // commits can happen concurrently from different machines.
709            // A tolerance helps us avoid failure for small clock skew
710            if self.last_updated_ms - last.timestamp_ms < -ONE_MINUTE_MS {
711                return Err(invalid_data!(
712                    "Invalid update timestamp {}: before last snapshot log entry at {}",
713                    self.last_updated_ms,
714                    last.timestamp_ms
715                ));
716            }
717        }
718        Ok(())
719    }
720
721    fn validate_chronological_metadata_logs(&self) -> Result<()> {
722        for window in self.metadata_log.windows(2) {
723            let (prev, curr) = (&window[0], &window[1]);
724            // commits can happen concurrently from different machines.
725            // A tolerance helps us avoid failure for small clock skew
726            if curr.timestamp_ms - prev.timestamp_ms < -ONE_MINUTE_MS {
727                return Err(invalid_data!("Expected sorted metadata log entries"));
728            }
729        }
730
731        if let Some(last) = self.metadata_log.last() {
732            // commits can happen concurrently from different machines.
733            // A tolerance helps us avoid failure for small clock skew
734            if self.last_updated_ms - last.timestamp_ms < -ONE_MINUTE_MS {
735                return Err(invalid_data!(
736                    "Invalid update timestamp {}: before last metadata log entry at {}",
737                    self.last_updated_ms,
738                    last.timestamp_ms
739                ));
740            }
741        }
742
743        Ok(())
744    }
745
746    /// Validates that every type used in the current schema is supported by the
747    /// table's format version.  Delegates to [`Schema::check_format_compatibility`].
748    fn validate_schema_format_compatibility(&self) -> Result<()> {
749        self.current_schema()
750            .check_format_compatibility(self.format_version)
751    }
752}
753
754pub(super) mod _serde {
755    use std::borrow::BorrowMut;
756    /// This is a helper module that defines types to help with serialization/deserialization.
757    /// For deserialization the input first gets read into either the [TableMetadataV1] or [TableMetadataV2] struct
758    /// and then converted into the [TableMetadata] struct. Serialization works the other way around.
759    /// [TableMetadataV1] and [TableMetadataV2] are internal struct that are only used for serialization and deserialization.
760    use std::collections::HashMap;
761    /// This is a helper module that defines types to help with serialization/deserialization.
762    /// For deserialization the input first gets read into either the [TableMetadataV1] or [TableMetadataV2] struct
763    /// and then converted into the [TableMetadata] struct. Serialization works the other way around.
764    /// [TableMetadataV1] and [TableMetadataV2] are internal struct that are only used for serialization and deserialization.
765    use std::sync::Arc;
766
767    use serde::{Deserialize, Serialize};
768    use uuid::Uuid;
769
770    use super::{
771        DEFAULT_PARTITION_SPEC_ID, EMPTY_SNAPSHOT_ID, FormatVersion, MAIN_BRANCH, MetadataLog,
772        SnapshotLog, TableMetadata,
773    };
774    use crate::error::invalid_data;
775    use crate::spec::schema::_serde::{SchemaV1, SchemaV2};
776    use crate::spec::snapshot::_serde::{SnapshotV1, SnapshotV2, SnapshotV3};
777    use crate::spec::{
778        EncryptedKey, INITIAL_ROW_ID, PartitionField, PartitionSpec, PartitionSpecRef,
779        PartitionStatisticsFile, Schema, SchemaRef, Snapshot, SnapshotReference, SnapshotRetention,
780        SortOrder, StatisticsFile, UnboundPartitionSpec,
781    };
782    use crate::{Error, ErrorKind};
783
784    #[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
785    #[serde(untagged)]
786    pub(super) enum TableMetadataEnum {
787        V3(TableMetadataV3),
788        V2(TableMetadataV2),
789        V1(TableMetadataV1),
790    }
791
792    #[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
793    #[serde(rename_all = "kebab-case")]
794    /// Defines the structure of a v2 table metadata for serialization/deserialization
795    pub(super) struct TableMetadataV3 {
796        pub format_version: VersionNumber<3>,
797        #[serde(flatten)]
798        pub shared: TableMetadataV2V3Shared,
799        pub next_row_id: u64,
800        #[serde(skip_serializing_if = "Option::is_none")]
801        pub encryption_keys: Option<Vec<EncryptedKey>>,
802        #[serde(skip_serializing_if = "Option::is_none")]
803        pub snapshots: Option<Vec<SnapshotV3>>,
804    }
805
806    #[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
807    #[serde(rename_all = "kebab-case")]
808    /// Defines the structure of a v2 table metadata for serialization/deserialization
809    pub(super) struct TableMetadataV2V3Shared {
810        pub table_uuid: Uuid,
811        pub location: String,
812        pub last_sequence_number: i64,
813        pub last_updated_ms: i64,
814        pub last_column_id: i32,
815        pub schemas: Vec<SchemaV2>,
816        pub current_schema_id: i32,
817        pub partition_specs: Vec<PartitionSpec>,
818        pub default_spec_id: i32,
819        pub last_partition_id: i32,
820        #[serde(skip_serializing_if = "Option::is_none")]
821        pub properties: Option<HashMap<String, String>>,
822        #[serde(skip_serializing_if = "Option::is_none")]
823        pub current_snapshot_id: Option<i64>,
824        #[serde(skip_serializing_if = "Option::is_none")]
825        pub snapshot_log: Option<Vec<SnapshotLog>>,
826        #[serde(skip_serializing_if = "Option::is_none")]
827        pub metadata_log: Option<Vec<MetadataLog>>,
828        pub sort_orders: Vec<SortOrder>,
829        pub default_sort_order_id: i64,
830        #[serde(skip_serializing_if = "Option::is_none")]
831        pub refs: Option<HashMap<String, SnapshotReference>>,
832        #[serde(default, skip_serializing_if = "Vec::is_empty")]
833        pub statistics: Vec<StatisticsFile>,
834        #[serde(default, skip_serializing_if = "Vec::is_empty")]
835        pub partition_statistics: Vec<PartitionStatisticsFile>,
836    }
837
838    #[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
839    #[serde(rename_all = "kebab-case")]
840    /// Defines the structure of a v2 table metadata for serialization/deserialization
841    pub(super) struct TableMetadataV2 {
842        pub format_version: VersionNumber<2>,
843        #[serde(flatten)]
844        pub shared: TableMetadataV2V3Shared,
845        #[serde(skip_serializing_if = "Option::is_none")]
846        pub snapshots: Option<Vec<SnapshotV2>>,
847    }
848
849    #[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
850    #[serde(rename_all = "kebab-case")]
851    /// Defines the structure of a v1 table metadata for serialization/deserialization
852    pub(super) struct TableMetadataV1 {
853        pub format_version: VersionNumber<1>,
854        #[serde(skip_serializing_if = "Option::is_none")]
855        pub table_uuid: Option<Uuid>,
856        pub location: String,
857        pub last_updated_ms: i64,
858        pub last_column_id: i32,
859        /// `schema` is optional to prioritize `schemas` and `current-schema-id`, allowing liberal reading of V1 metadata.
860        pub schema: Option<SchemaV1>,
861        #[serde(skip_serializing_if = "Option::is_none")]
862        pub schemas: Option<Vec<SchemaV1>>,
863        #[serde(skip_serializing_if = "Option::is_none")]
864        pub current_schema_id: Option<i32>,
865        /// `partition_spec` is optional to prioritize `partition_specs`, aligning with liberal reading of potentially invalid V1 metadata.
866        pub partition_spec: Option<Vec<PartitionField>>,
867        #[serde(skip_serializing_if = "Option::is_none")]
868        pub partition_specs: Option<Vec<PartitionSpec>>,
869        #[serde(skip_serializing_if = "Option::is_none")]
870        pub default_spec_id: Option<i32>,
871        #[serde(skip_serializing_if = "Option::is_none")]
872        pub last_partition_id: Option<i32>,
873        #[serde(skip_serializing_if = "Option::is_none")]
874        pub properties: Option<HashMap<String, String>>,
875        #[serde(skip_serializing_if = "Option::is_none")]
876        pub current_snapshot_id: Option<i64>,
877        #[serde(skip_serializing_if = "Option::is_none")]
878        pub snapshots: Option<Vec<SnapshotV1>>,
879        #[serde(skip_serializing_if = "Option::is_none")]
880        pub snapshot_log: Option<Vec<SnapshotLog>>,
881        #[serde(skip_serializing_if = "Option::is_none")]
882        pub metadata_log: Option<Vec<MetadataLog>>,
883        pub sort_orders: Option<Vec<SortOrder>>,
884        pub default_sort_order_id: Option<i64>,
885        #[serde(default, skip_serializing_if = "Vec::is_empty")]
886        pub statistics: Vec<StatisticsFile>,
887        #[serde(default, skip_serializing_if = "Vec::is_empty")]
888        pub partition_statistics: Vec<PartitionStatisticsFile>,
889    }
890
891    /// Helper to serialize and deserialize the format version.
892    #[derive(Debug, PartialEq, Eq)]
893    pub(crate) struct VersionNumber<const V: u8>;
894
895    impl Serialize for TableMetadata {
896        fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
897        where S: serde::Serializer {
898            // we must do a clone here
899            let table_metadata_enum: TableMetadataEnum =
900                self.clone().try_into().map_err(serde::ser::Error::custom)?;
901
902            table_metadata_enum.serialize(serializer)
903        }
904    }
905
906    impl<const V: u8> Serialize for VersionNumber<V> {
907        fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
908        where S: serde::Serializer {
909            serializer.serialize_u8(V)
910        }
911    }
912
913    impl<'de, const V: u8> Deserialize<'de> for VersionNumber<V> {
914        fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
915        where D: serde::Deserializer<'de> {
916            let value = u8::deserialize(deserializer)?;
917            if value == V {
918                Ok(VersionNumber::<V>)
919            } else {
920                Err(serde::de::Error::custom("Invalid Version"))
921            }
922        }
923    }
924
925    impl TryFrom<TableMetadataEnum> for TableMetadata {
926        type Error = Error;
927        fn try_from(value: TableMetadataEnum) -> Result<Self, Error> {
928            match value {
929                TableMetadataEnum::V3(value) => value.try_into(),
930                TableMetadataEnum::V2(value) => value.try_into(),
931                TableMetadataEnum::V1(value) => value.try_into(),
932            }
933        }
934    }
935
936    impl TryFrom<TableMetadata> for TableMetadataEnum {
937        type Error = Error;
938        fn try_from(value: TableMetadata) -> Result<Self, Error> {
939            Ok(match value.format_version {
940                FormatVersion::V3 => TableMetadataEnum::V3(value.try_into()?),
941                FormatVersion::V2 => TableMetadataEnum::V2(value.into()),
942                FormatVersion::V1 => TableMetadataEnum::V1(value.try_into()?),
943            })
944        }
945    }
946
947    impl TryFrom<TableMetadataV3> for TableMetadata {
948        type Error = Error;
949        fn try_from(value: TableMetadataV3) -> Result<Self, Error> {
950            let TableMetadataV3 {
951                format_version: _,
952                shared: value,
953                next_row_id,
954                encryption_keys,
955                snapshots,
956            } = value;
957            let current_snapshot_id = if value.current_snapshot_id == Some(EMPTY_SNAPSHOT_ID) {
958                None
959            } else {
960                value.current_snapshot_id
961            };
962            let schemas = HashMap::from_iter(
963                value
964                    .schemas
965                    .into_iter()
966                    .map(|schema| Ok((schema.schema_id, Arc::new(schema.try_into()?))))
967                    .collect::<Result<Vec<_>, Error>>()?,
968            );
969
970            let current_schema: &SchemaRef =
971                schemas.get(&value.current_schema_id).ok_or_else(|| {
972                    invalid_data!(
973                        "No schema exists with the current schema id {}.",
974                        value.current_schema_id
975                    )
976                })?;
977            let partition_specs = HashMap::from_iter(
978                value
979                    .partition_specs
980                    .into_iter()
981                    .map(|x| (x.spec_id(), Arc::new(x))),
982            );
983            let default_spec_id = value.default_spec_id;
984            let default_spec: PartitionSpecRef = partition_specs
985                .get(&value.default_spec_id)
986                .map(|spec| (**spec).clone())
987                .or_else(|| {
988                    (DEFAULT_PARTITION_SPEC_ID == default_spec_id)
989                        .then(PartitionSpec::unpartition_spec)
990                })
991                .ok_or_else(|| invalid_data!("Default partition spec {default_spec_id} not found"))?
992                .into();
993            let default_partition_type = default_spec.partition_type(current_schema)?;
994
995            let mut metadata = TableMetadata {
996                format_version: FormatVersion::V3,
997                table_uuid: value.table_uuid,
998                location: value.location,
999                last_sequence_number: value.last_sequence_number,
1000                last_updated_ms: value.last_updated_ms,
1001                last_column_id: value.last_column_id,
1002                current_schema_id: value.current_schema_id,
1003                schemas,
1004                partition_specs,
1005                default_partition_type,
1006                default_spec,
1007                last_partition_id: value.last_partition_id,
1008                properties: value.properties.unwrap_or_default(),
1009                current_snapshot_id,
1010                snapshots: snapshots
1011                    .map(|snapshots| {
1012                        HashMap::from_iter(
1013                            snapshots
1014                                .into_iter()
1015                                .map(|x| (x.snapshot_id, Arc::new(x.into()))),
1016                        )
1017                    })
1018                    .unwrap_or_default(),
1019                snapshot_log: value.snapshot_log.unwrap_or_default(),
1020                metadata_log: value.metadata_log.unwrap_or_default(),
1021                sort_orders: HashMap::from_iter(
1022                    value
1023                        .sort_orders
1024                        .into_iter()
1025                        .map(|x| (x.order_id, Arc::new(x))),
1026                ),
1027                default_sort_order_id: value.default_sort_order_id,
1028                refs: value.refs.unwrap_or_else(|| {
1029                    if let Some(snapshot_id) = current_snapshot_id {
1030                        HashMap::from_iter(vec![(MAIN_BRANCH.to_string(), SnapshotReference {
1031                            snapshot_id,
1032                            retention: SnapshotRetention::Branch {
1033                                min_snapshots_to_keep: None,
1034                                max_snapshot_age_ms: None,
1035                                max_ref_age_ms: None,
1036                            },
1037                        })])
1038                    } else {
1039                        HashMap::new()
1040                    }
1041                }),
1042                statistics: index_statistics(value.statistics),
1043                partition_statistics: index_partition_statistics(value.partition_statistics),
1044                encryption_keys: encryption_keys
1045                    .map(|keys| {
1046                        HashMap::from_iter(keys.into_iter().map(|key| (key.key_id.clone(), key)))
1047                    })
1048                    .unwrap_or_default(),
1049                next_row_id,
1050            };
1051
1052            metadata.borrow_mut().try_normalize()?;
1053            Ok(metadata)
1054        }
1055    }
1056
1057    impl TryFrom<TableMetadataV2> for TableMetadata {
1058        type Error = Error;
1059        fn try_from(value: TableMetadataV2) -> Result<Self, Error> {
1060            let snapshots = value.snapshots;
1061            let value = value.shared;
1062            let current_snapshot_id = if value.current_snapshot_id == Some(EMPTY_SNAPSHOT_ID) {
1063                None
1064            } else {
1065                value.current_snapshot_id
1066            };
1067            let schemas = HashMap::from_iter(
1068                value
1069                    .schemas
1070                    .into_iter()
1071                    .map(|schema| Ok((schema.schema_id, Arc::new(schema.try_into()?))))
1072                    .collect::<Result<Vec<_>, Error>>()?,
1073            );
1074
1075            let current_schema: &SchemaRef =
1076                schemas.get(&value.current_schema_id).ok_or_else(|| {
1077                    invalid_data!(
1078                        "No schema exists with the current schema id {}.",
1079                        value.current_schema_id
1080                    )
1081                })?;
1082            let partition_specs = HashMap::from_iter(
1083                value
1084                    .partition_specs
1085                    .into_iter()
1086                    .map(|x| (x.spec_id(), Arc::new(x))),
1087            );
1088            let default_spec_id = value.default_spec_id;
1089            let default_spec: PartitionSpecRef = partition_specs
1090                .get(&value.default_spec_id)
1091                .map(|spec| (**spec).clone())
1092                .or_else(|| {
1093                    (DEFAULT_PARTITION_SPEC_ID == default_spec_id)
1094                        .then(PartitionSpec::unpartition_spec)
1095                })
1096                .ok_or_else(|| invalid_data!("Default partition spec {default_spec_id} not found"))?
1097                .into();
1098            let default_partition_type = default_spec.partition_type(current_schema)?;
1099
1100            let mut metadata = TableMetadata {
1101                format_version: FormatVersion::V2,
1102                table_uuid: value.table_uuid,
1103                location: value.location,
1104                last_sequence_number: value.last_sequence_number,
1105                last_updated_ms: value.last_updated_ms,
1106                last_column_id: value.last_column_id,
1107                current_schema_id: value.current_schema_id,
1108                schemas,
1109                partition_specs,
1110                default_partition_type,
1111                default_spec,
1112                last_partition_id: value.last_partition_id,
1113                properties: value.properties.unwrap_or_default(),
1114                current_snapshot_id,
1115                snapshots: snapshots
1116                    .map(|snapshots| {
1117                        HashMap::from_iter(
1118                            snapshots
1119                                .into_iter()
1120                                .map(|x| (x.snapshot_id, Arc::new(x.into()))),
1121                        )
1122                    })
1123                    .unwrap_or_default(),
1124                snapshot_log: value.snapshot_log.unwrap_or_default(),
1125                metadata_log: value.metadata_log.unwrap_or_default(),
1126                sort_orders: HashMap::from_iter(
1127                    value
1128                        .sort_orders
1129                        .into_iter()
1130                        .map(|x| (x.order_id, Arc::new(x))),
1131                ),
1132                default_sort_order_id: value.default_sort_order_id,
1133                refs: value.refs.unwrap_or_else(|| {
1134                    if let Some(snapshot_id) = current_snapshot_id {
1135                        HashMap::from_iter(vec![(MAIN_BRANCH.to_string(), SnapshotReference {
1136                            snapshot_id,
1137                            retention: SnapshotRetention::Branch {
1138                                min_snapshots_to_keep: None,
1139                                max_snapshot_age_ms: None,
1140                                max_ref_age_ms: None,
1141                            },
1142                        })])
1143                    } else {
1144                        HashMap::new()
1145                    }
1146                }),
1147                statistics: index_statistics(value.statistics),
1148                partition_statistics: index_partition_statistics(value.partition_statistics),
1149                encryption_keys: HashMap::new(),
1150                next_row_id: INITIAL_ROW_ID,
1151            };
1152
1153            metadata.borrow_mut().try_normalize()?;
1154            Ok(metadata)
1155        }
1156    }
1157
1158    impl TryFrom<TableMetadataV1> for TableMetadata {
1159        type Error = Error;
1160        fn try_from(value: TableMetadataV1) -> Result<Self, Error> {
1161            let current_snapshot_id = if value.current_snapshot_id == Some(EMPTY_SNAPSHOT_ID) {
1162                None
1163            } else {
1164                value.current_snapshot_id
1165            };
1166
1167            let (schemas, current_schema_id, current_schema) =
1168                if let (Some(schemas_vec), Some(schema_id)) =
1169                    (&value.schemas, value.current_schema_id)
1170                {
1171                    // Option 1: Use 'schemas' + 'current_schema_id'
1172                    let schema_map = HashMap::from_iter(
1173                        schemas_vec
1174                            .clone()
1175                            .into_iter()
1176                            .map(|schema| {
1177                                let schema: Schema = schema.try_into()?;
1178                                Ok((schema.schema_id(), Arc::new(schema)))
1179                            })
1180                            .collect::<Result<Vec<_>, Error>>()?,
1181                    );
1182
1183                    let schema = schema_map
1184                        .get(&schema_id)
1185                        .ok_or_else(|| {
1186                            invalid_data!(
1187                                "No schema exists with the current schema id {schema_id}."
1188                            )
1189                        })?
1190                        .clone();
1191                    (schema_map, schema_id, schema)
1192                } else if let Some(schema) = value.schema {
1193                    // Option 2: Fall back to `schema`
1194                    let schema: Schema = schema.try_into()?;
1195                    let schema_id = schema.schema_id();
1196                    let schema_arc = Arc::new(schema);
1197                    let schema_map = HashMap::from_iter(vec![(schema_id, schema_arc.clone())]);
1198                    (schema_map, schema_id, schema_arc)
1199                } else {
1200                    // Option 3: No valid schema configuration found
1201                    return Err(invalid_data!(
1202                        "No valid schema configuration found in table metadata"
1203                    ));
1204                };
1205
1206            // Prioritize 'partition_specs' over 'partition_spec'
1207            let partition_specs = if let Some(specs_vec) = value.partition_specs {
1208                // Option 1: Use 'partition_specs'
1209                specs_vec
1210                    .into_iter()
1211                    .map(|x| (x.spec_id(), Arc::new(x)))
1212                    .collect::<HashMap<_, _>>()
1213            } else if let Some(partition_spec) = value.partition_spec {
1214                // Option 2: Fall back to 'partition_spec'
1215                let spec = UnboundPartitionSpec {
1216                    spec_id: Some(DEFAULT_PARTITION_SPEC_ID),
1217                    fields: partition_spec
1218                        .into_iter()
1219                        .map(|f| f.into_unbound())
1220                        .collect(),
1221                }
1222                .bind(current_schema.clone())?;
1223
1224                HashMap::from_iter(vec![(DEFAULT_PARTITION_SPEC_ID, Arc::new(spec))])
1225            } else {
1226                // Option 3: Create empty partition spec
1227                let spec = PartitionSpec::builder(current_schema.clone())
1228                    .with_spec_id(DEFAULT_PARTITION_SPEC_ID)
1229                    .build()?;
1230
1231                HashMap::from_iter(vec![(DEFAULT_PARTITION_SPEC_ID, Arc::new(spec))])
1232            };
1233
1234            // Get the default_spec_id, prioritizing the explicit value if provided
1235            let default_spec_id = value
1236                .default_spec_id
1237                .unwrap_or_else(|| partition_specs.keys().copied().max().unwrap_or_default());
1238
1239            // Get the default spec
1240            let default_spec: PartitionSpecRef = partition_specs
1241                .get(&default_spec_id)
1242                .map(|x| Arc::unwrap_or_clone(x.clone()))
1243                .ok_or_else(|| invalid_data!("Default partition spec {default_spec_id} not found"))?
1244                .into();
1245            let default_partition_type = default_spec.partition_type(&current_schema)?;
1246
1247            let mut metadata = TableMetadata {
1248                format_version: FormatVersion::V1,
1249                table_uuid: value.table_uuid.unwrap_or_default(),
1250                location: value.location,
1251                last_sequence_number: 0,
1252                last_updated_ms: value.last_updated_ms,
1253                last_column_id: value.last_column_id,
1254                current_schema_id,
1255                default_spec,
1256                default_partition_type,
1257                last_partition_id: value
1258                    .last_partition_id
1259                    .unwrap_or_else(|| partition_specs.keys().copied().max().unwrap_or_default()),
1260                partition_specs,
1261                schemas,
1262                properties: value.properties.unwrap_or_default(),
1263                current_snapshot_id,
1264                snapshots: value
1265                    .snapshots
1266                    .map(|snapshots| {
1267                        Ok::<_, Error>(HashMap::from_iter(
1268                            snapshots
1269                                .into_iter()
1270                                .map(|x| Ok((x.snapshot_id, Arc::new(x.try_into()?))))
1271                                .collect::<Result<Vec<_>, Error>>()?,
1272                        ))
1273                    })
1274                    .transpose()?
1275                    .unwrap_or_default(),
1276                snapshot_log: value.snapshot_log.unwrap_or_default(),
1277                metadata_log: value.metadata_log.unwrap_or_default(),
1278                sort_orders: match value.sort_orders {
1279                    Some(sort_orders) => HashMap::from_iter(
1280                        sort_orders.into_iter().map(|x| (x.order_id, Arc::new(x))),
1281                    ),
1282                    None => HashMap::new(),
1283                },
1284                default_sort_order_id: value
1285                    .default_sort_order_id
1286                    .unwrap_or(SortOrder::UNSORTED_ORDER_ID),
1287                refs: if let Some(snapshot_id) = current_snapshot_id {
1288                    HashMap::from_iter(vec![(MAIN_BRANCH.to_string(), SnapshotReference {
1289                        snapshot_id,
1290                        retention: SnapshotRetention::Branch {
1291                            min_snapshots_to_keep: None,
1292                            max_snapshot_age_ms: None,
1293                            max_ref_age_ms: None,
1294                        },
1295                    })])
1296                } else {
1297                    HashMap::new()
1298                },
1299                statistics: index_statistics(value.statistics),
1300                partition_statistics: index_partition_statistics(value.partition_statistics),
1301                encryption_keys: HashMap::new(),
1302                next_row_id: INITIAL_ROW_ID, // v1 has no row lineage
1303            };
1304
1305            metadata.borrow_mut().try_normalize()?;
1306            Ok(metadata)
1307        }
1308    }
1309
1310    impl TryFrom<TableMetadata> for TableMetadataV3 {
1311        type Error = Error;
1312
1313        fn try_from(mut v: TableMetadata) -> Result<Self, Self::Error> {
1314            let next_row_id = v.next_row_id;
1315            let encryption_keys = std::mem::take(&mut v.encryption_keys);
1316            let snapshots = std::mem::take(&mut v.snapshots);
1317            let shared = v.into();
1318
1319            Ok(TableMetadataV3 {
1320                format_version: VersionNumber::<3>,
1321                shared,
1322                next_row_id,
1323                encryption_keys: if encryption_keys.is_empty() {
1324                    None
1325                } else {
1326                    Some(encryption_keys.into_values().collect())
1327                },
1328                snapshots: if snapshots.is_empty() {
1329                    None
1330                } else {
1331                    Some(
1332                        snapshots
1333                            .into_values()
1334                            .map(|s| SnapshotV3::try_from(Arc::unwrap_or_clone(s)))
1335                            .collect::<Result<_, _>>()?,
1336                    )
1337                },
1338            })
1339        }
1340    }
1341
1342    impl From<TableMetadata> for TableMetadataV2 {
1343        fn from(mut v: TableMetadata) -> Self {
1344            let snapshots = std::mem::take(&mut v.snapshots);
1345            let shared = v.into();
1346
1347            TableMetadataV2 {
1348                format_version: VersionNumber::<2>,
1349                shared,
1350                snapshots: if snapshots.is_empty() {
1351                    None
1352                } else {
1353                    Some(
1354                        snapshots
1355                            .into_values()
1356                            .map(|s| SnapshotV2::from(Arc::unwrap_or_clone(s)))
1357                            .collect(),
1358                    )
1359                },
1360            }
1361        }
1362    }
1363
1364    impl From<TableMetadata> for TableMetadataV2V3Shared {
1365        fn from(v: TableMetadata) -> Self {
1366            TableMetadataV2V3Shared {
1367                table_uuid: v.table_uuid,
1368                location: v.location,
1369                last_sequence_number: v.last_sequence_number,
1370                last_updated_ms: v.last_updated_ms,
1371                last_column_id: v.last_column_id,
1372                schemas: v
1373                    .schemas
1374                    .into_values()
1375                    .map(|x| {
1376                        Arc::try_unwrap(x)
1377                            .unwrap_or_else(|schema| schema.as_ref().clone())
1378                            .into()
1379                    })
1380                    .collect(),
1381                current_schema_id: v.current_schema_id,
1382                partition_specs: v
1383                    .partition_specs
1384                    .into_values()
1385                    .map(|x| Arc::try_unwrap(x).unwrap_or_else(|s| s.as_ref().clone()))
1386                    .collect(),
1387                default_spec_id: v.default_spec.spec_id(),
1388                last_partition_id: v.last_partition_id,
1389                properties: if v.properties.is_empty() {
1390                    None
1391                } else {
1392                    Some(v.properties)
1393                },
1394                current_snapshot_id: v.current_snapshot_id,
1395                snapshot_log: if v.snapshot_log.is_empty() {
1396                    None
1397                } else {
1398                    Some(v.snapshot_log)
1399                },
1400                metadata_log: if v.metadata_log.is_empty() {
1401                    None
1402                } else {
1403                    Some(v.metadata_log)
1404                },
1405                sort_orders: v
1406                    .sort_orders
1407                    .into_values()
1408                    .map(|x| Arc::try_unwrap(x).unwrap_or_else(|s| s.as_ref().clone()))
1409                    .collect(),
1410                default_sort_order_id: v.default_sort_order_id,
1411                refs: Some(v.refs),
1412                statistics: v.statistics.into_values().collect(),
1413                partition_statistics: v.partition_statistics.into_values().collect(),
1414            }
1415        }
1416    }
1417
1418    impl TryFrom<TableMetadata> for TableMetadataV1 {
1419        type Error = Error;
1420        fn try_from(v: TableMetadata) -> Result<Self, Error> {
1421            Ok(TableMetadataV1 {
1422                format_version: VersionNumber::<1>,
1423                table_uuid: Some(v.table_uuid),
1424                location: v.location,
1425                last_updated_ms: v.last_updated_ms,
1426                last_column_id: v.last_column_id,
1427                schema: Some(
1428                    v.schemas
1429                        .get(&v.current_schema_id)
1430                        .ok_or(Error::new(
1431                            ErrorKind::Unexpected,
1432                            "current_schema_id not found in schemas",
1433                        ))?
1434                        .as_ref()
1435                        .clone()
1436                        .into(),
1437                ),
1438                schemas: Some(
1439                    v.schemas
1440                        .into_values()
1441                        .map(|x| {
1442                            Arc::try_unwrap(x)
1443                                .unwrap_or_else(|schema| schema.as_ref().clone())
1444                                .into()
1445                        })
1446                        .collect(),
1447                ),
1448                current_schema_id: Some(v.current_schema_id),
1449                partition_spec: Some(v.default_spec.fields().to_vec()),
1450                partition_specs: Some(
1451                    v.partition_specs
1452                        .into_values()
1453                        .map(|x| Arc::try_unwrap(x).unwrap_or_else(|s| s.as_ref().clone()))
1454                        .collect(),
1455                ),
1456                default_spec_id: Some(v.default_spec.spec_id()),
1457                last_partition_id: Some(v.last_partition_id),
1458                properties: if v.properties.is_empty() {
1459                    None
1460                } else {
1461                    Some(v.properties)
1462                },
1463                current_snapshot_id: v.current_snapshot_id,
1464                snapshots: if v.snapshots.is_empty() {
1465                    None
1466                } else {
1467                    Some(
1468                        v.snapshots
1469                            .into_values()
1470                            .map(|x| Snapshot::clone(&x).into())
1471                            .collect(),
1472                    )
1473                },
1474                snapshot_log: if v.snapshot_log.is_empty() {
1475                    None
1476                } else {
1477                    Some(v.snapshot_log)
1478                },
1479                metadata_log: if v.metadata_log.is_empty() {
1480                    None
1481                } else {
1482                    Some(v.metadata_log)
1483                },
1484                sort_orders: Some(
1485                    v.sort_orders
1486                        .into_values()
1487                        .map(|s| Arc::try_unwrap(s).unwrap_or_else(|s| s.as_ref().clone()))
1488                        .collect(),
1489                ),
1490                default_sort_order_id: Some(v.default_sort_order_id),
1491                statistics: v.statistics.into_values().collect(),
1492                partition_statistics: v.partition_statistics.into_values().collect(),
1493            })
1494        }
1495    }
1496
1497    fn index_statistics(statistics: Vec<StatisticsFile>) -> HashMap<i64, StatisticsFile> {
1498        statistics
1499            .into_iter()
1500            .rev()
1501            .map(|s| (s.snapshot_id, s))
1502            .collect()
1503    }
1504
1505    fn index_partition_statistics(
1506        statistics: Vec<PartitionStatisticsFile>,
1507    ) -> HashMap<i64, PartitionStatisticsFile> {
1508        statistics
1509            .into_iter()
1510            .rev()
1511            .map(|s| (s.snapshot_id, s))
1512            .collect()
1513    }
1514}
1515
1516#[derive(Debug, Serialize_repr, Deserialize_repr, PartialEq, Eq, Clone, Copy, Hash)]
1517#[repr(u8)]
1518/// Iceberg format version
1519pub enum FormatVersion {
1520    /// Iceberg spec version 1
1521    V1 = 1u8,
1522    /// Iceberg spec version 2
1523    V2 = 2u8,
1524    /// Iceberg spec version 3
1525    V3 = 3u8,
1526}
1527
1528impl PartialOrd for FormatVersion {
1529    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
1530        Some(self.cmp(other))
1531    }
1532}
1533
1534impl Ord for FormatVersion {
1535    fn cmp(&self, other: &Self) -> Ordering {
1536        (*self as u8).cmp(&(*other as u8))
1537    }
1538}
1539
1540impl Display for FormatVersion {
1541    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
1542        match self {
1543            FormatVersion::V1 => write!(f, "v1"),
1544            FormatVersion::V2 => write!(f, "v2"),
1545            FormatVersion::V3 => write!(f, "v3"),
1546        }
1547    }
1548}
1549
1550#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)]
1551#[serde(rename_all = "kebab-case")]
1552/// Encodes changes to the previous metadata files for the table
1553pub struct MetadataLog {
1554    /// The file for the log.
1555    pub metadata_file: String,
1556    /// Time new metadata was created
1557    pub timestamp_ms: i64,
1558}
1559
1560#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)]
1561#[serde(rename_all = "kebab-case")]
1562/// A log of when each snapshot was made.
1563pub struct SnapshotLog {
1564    /// Id of the snapshot.
1565    pub snapshot_id: i64,
1566    /// Last updated timestamp
1567    pub timestamp_ms: i64,
1568}
1569
1570impl SnapshotLog {
1571    /// Returns the last updated timestamp as a [`DateTime<Utc>`] with millisecond precision
1572    pub fn timestamp(self) -> Result<DateTime<Utc>> {
1573        timestamp_ms_to_utc(self.timestamp_ms)
1574    }
1575
1576    /// Returns the timestamp in milliseconds
1577    #[inline]
1578    pub fn timestamp_ms(&self) -> i64 {
1579        self.timestamp_ms
1580    }
1581}
1582
1583#[cfg(test)]
1584mod tests {
1585    use std::collections::HashMap;
1586    use std::fs;
1587    use std::sync::Arc;
1588
1589    use anyhow::Result;
1590    use base64::Engine as _;
1591    use pretty_assertions::assert_eq;
1592    use tempfile::TempDir;
1593    use uuid::Uuid;
1594
1595    use super::{FormatVersion, MetadataLog, SnapshotLog, TableMetadataBuilder};
1596    use crate::catalog::MetadataLocation;
1597    use crate::compression::CompressionCodec;
1598    use crate::io::FileIO;
1599    use crate::spec::table_metadata::TableMetadata;
1600    use crate::spec::{
1601        BlobMetadata, EncryptedKey, INITIAL_ROW_ID, Literal, NestedField, NullOrder, Operation,
1602        PartitionSpec, PartitionStatisticsFile, PrimitiveLiteral, PrimitiveType, Schema, Snapshot,
1603        SnapshotReference, SnapshotRetention, SortDirection, SortField, SortOrder, StatisticsFile,
1604        Summary, TableProperties, Transform, Type, UnboundPartitionField, UnboundPartitionSpec,
1605    };
1606    use crate::{ErrorKind, TableCreation};
1607
1608    fn check_table_metadata_serde(json: &str, expected_type: TableMetadata) {
1609        let desered_type: TableMetadata = serde_json::from_str(json).unwrap();
1610        assert_eq!(desered_type, expected_type);
1611
1612        let sered_json = serde_json::to_string(&expected_type).unwrap();
1613        let parsed_json_value = serde_json::from_str::<TableMetadata>(&sered_json).unwrap();
1614
1615        assert_eq!(parsed_json_value, desered_type);
1616    }
1617
1618    fn get_test_table_metadata(file_name: &str) -> TableMetadata {
1619        let path = format!("testdata/table_metadata/{file_name}");
1620        let metadata: String = fs::read_to_string(path).unwrap();
1621
1622        serde_json::from_str(&metadata).unwrap()
1623    }
1624
1625    /// Loads a test table metadata and relocates it to `location`, so that derived
1626    /// metadata paths point at a writable (e.g. temp) directory.
1627    fn get_test_table_metadata_at(file_name: &str, location: &str) -> TableMetadata {
1628        TableMetadataBuilder::new_from_metadata(get_test_table_metadata(file_name), None)
1629            .set_location(location.to_string())
1630            .build()
1631            .unwrap()
1632            .metadata
1633    }
1634
1635    #[test]
1636    fn test_table_data_v2() {
1637        let data = r#"
1638            {
1639                "format-version" : 2,
1640                "table-uuid": "fb072c92-a02b-11e9-ae9c-1bb7bc9eca94",
1641                "location": "s3://b/wh/data.db/table",
1642                "last-sequence-number" : 1,
1643                "last-updated-ms": 1515100955770,
1644                "last-column-id": 1,
1645                "schemas": [
1646                    {
1647                        "schema-id" : 1,
1648                        "type" : "struct",
1649                        "fields" :[
1650                            {
1651                                "id": 1,
1652                                "name": "struct_name",
1653                                "required": true,
1654                                "type": "fixed[1]"
1655                            },
1656                            {
1657                                "id": 4,
1658                                "name": "ts",
1659                                "required": true,
1660                                "type": "timestamp"
1661                            }
1662                        ]
1663                    }
1664                ],
1665                "current-schema-id" : 1,
1666                "partition-specs": [
1667                    {
1668                        "spec-id": 0,
1669                        "fields": [
1670                            {
1671                                "source-id": 4,
1672                                "field-id": 1000,
1673                                "name": "ts_day",
1674                                "transform": "day"
1675                            }
1676                        ]
1677                    }
1678                ],
1679                "default-spec-id": 0,
1680                "last-partition-id": 1000,
1681                "properties": {
1682                    "commit.retry.num-retries": "1"
1683                },
1684                "metadata-log": [
1685                    {
1686                        "metadata-file": "s3://bucket/.../v1.json",
1687                        "timestamp-ms": 1515100
1688                    }
1689                ],
1690                "refs": {},
1691                "sort-orders": [
1692                    {
1693                    "order-id": 0,
1694                    "fields": []
1695                    }
1696                ],
1697                "default-sort-order-id": 0
1698            }
1699        "#;
1700
1701        let schema = Schema::builder()
1702            .with_schema_id(1)
1703            .with_fields(vec![
1704                Arc::new(NestedField::required(
1705                    1,
1706                    "struct_name",
1707                    Type::Primitive(PrimitiveType::Fixed(1)),
1708                )),
1709                Arc::new(NestedField::required(
1710                    4,
1711                    "ts",
1712                    Type::Primitive(PrimitiveType::Timestamp),
1713                )),
1714            ])
1715            .build()
1716            .unwrap();
1717
1718        let partition_spec = PartitionSpec::builder(schema.clone())
1719            .with_spec_id(0)
1720            .add_unbound_field(
1721                UnboundPartitionField::builder()
1722                    .source_ids(vec![4])
1723                    .field_id(1000)
1724                    .name("ts_day".to_string())
1725                    .transform(Transform::Day)
1726                    .build()
1727                    .unwrap(),
1728            )
1729            .unwrap()
1730            .build()
1731            .unwrap();
1732
1733        let default_partition_type = partition_spec.partition_type(&schema).unwrap();
1734        let expected = TableMetadata {
1735            format_version: FormatVersion::V2,
1736            table_uuid: Uuid::parse_str("fb072c92-a02b-11e9-ae9c-1bb7bc9eca94").unwrap(),
1737            location: "s3://b/wh/data.db/table".to_string(),
1738            last_updated_ms: 1515100955770,
1739            last_column_id: 1,
1740            schemas: HashMap::from_iter(vec![(1, Arc::new(schema))]),
1741            current_schema_id: 1,
1742            partition_specs: HashMap::from_iter(vec![(0, partition_spec.clone().into())]),
1743            default_partition_type,
1744            default_spec: partition_spec.into(),
1745            last_partition_id: 1000,
1746            default_sort_order_id: 0,
1747            sort_orders: HashMap::from_iter(vec![(0, SortOrder::unsorted_order().into())]),
1748            snapshots: HashMap::default(),
1749            current_snapshot_id: None,
1750            last_sequence_number: 1,
1751            properties: HashMap::from_iter(vec![(
1752                "commit.retry.num-retries".to_string(),
1753                "1".to_string(),
1754            )]),
1755            snapshot_log: Vec::new(),
1756            metadata_log: vec![MetadataLog {
1757                metadata_file: "s3://bucket/.../v1.json".to_string(),
1758                timestamp_ms: 1515100,
1759            }],
1760            refs: HashMap::new(),
1761            statistics: HashMap::new(),
1762            partition_statistics: HashMap::new(),
1763            encryption_keys: HashMap::new(),
1764            next_row_id: INITIAL_ROW_ID,
1765        };
1766
1767        let expected_json_value = serde_json::to_value(&expected).unwrap();
1768        check_table_metadata_serde(data, expected);
1769
1770        let json_value = serde_json::from_str::<serde_json::Value>(data).unwrap();
1771        assert_eq!(json_value, expected_json_value);
1772    }
1773
1774    #[test]
1775    fn test_table_data_v3() {
1776        let data = r#"
1777            {
1778                "format-version" : 3,
1779                "table-uuid": "fb072c92-a02b-11e9-ae9c-1bb7bc9eca94",
1780                "location": "s3://b/wh/data.db/table",
1781                "last-sequence-number" : 1,
1782                "last-updated-ms": 1515100955770,
1783                "last-column-id": 1,
1784                "next-row-id": 5,
1785                "schemas": [
1786                    {
1787                        "schema-id" : 1,
1788                        "type" : "struct",
1789                        "fields" :[
1790                            {
1791                                "id": 4,
1792                                "name": "ts",
1793                                "required": true,
1794                                "type": "timestamp"
1795                            }
1796                        ]
1797                    }
1798                ],
1799                "current-schema-id" : 1,
1800                "partition-specs": [
1801                    {
1802                        "spec-id": 0,
1803                        "fields": [
1804                            {
1805                                "source-id": 4,
1806                                "field-id": 1000,
1807                                "name": "ts_day",
1808                                "transform": "day"
1809                            }
1810                        ]
1811                    }
1812                ],
1813                "default-spec-id": 0,
1814                "last-partition-id": 1000,
1815                "properties": {
1816                    "commit.retry.num-retries": "1"
1817                },
1818                "metadata-log": [
1819                    {
1820                        "metadata-file": "s3://bucket/.../v1.json",
1821                        "timestamp-ms": 1515100
1822                    }
1823                ],
1824                "refs": {},
1825                "snapshots" : [ {
1826                    "snapshot-id" : 1,
1827                    "timestamp-ms" : 1662532818843,
1828                    "sequence-number" : 0,
1829                    "first-row-id" : 0,
1830                    "added-rows" : 4,
1831                    "key-id" : "key1",
1832                    "summary" : {
1833                        "operation" : "append"
1834                    },
1835                    "manifest-list" : "/home/iceberg/warehouse/nyc/taxis/metadata/snap-638933773299822130-1-7e6760f0-4f6c-4b23-b907-0a5a174e3863.avro",
1836                    "schema-id" : 0
1837                    }
1838                ],
1839                "encryption-keys": [
1840                    {
1841                        "key-id": "key1",
1842                        "encrypted-by-id": "KMS",
1843                        "encrypted-key-metadata": "c29tZS1lbmNyeXB0aW9uLWtleQ==",
1844                        "properties": {
1845                            "p1": "v1"
1846                        }
1847                    }
1848                ],
1849                "sort-orders": [
1850                    {
1851                    "order-id": 0,
1852                    "fields": []
1853                    }
1854                ],
1855                "default-sort-order-id": 0
1856            }
1857        "#;
1858
1859        let schema = Schema::builder()
1860            .with_schema_id(1)
1861            .with_fields(vec![Arc::new(NestedField::required(
1862                4,
1863                "ts",
1864                Type::Primitive(PrimitiveType::Timestamp),
1865            ))])
1866            .build()
1867            .unwrap();
1868
1869        let partition_spec = PartitionSpec::builder(schema.clone())
1870            .with_spec_id(0)
1871            .add_unbound_field(
1872                UnboundPartitionField::builder()
1873                    .source_ids(vec![4])
1874                    .field_id(1000)
1875                    .name("ts_day".to_string())
1876                    .transform(Transform::Day)
1877                    .build()
1878                    .unwrap(),
1879            )
1880            .unwrap()
1881            .build()
1882            .unwrap();
1883
1884        let snapshot = Snapshot::builder()
1885            .with_snapshot_id(1)
1886            .with_timestamp_ms(1662532818843)
1887            .with_sequence_number(0)
1888            .with_row_range(0, 4)
1889            .with_encryption_key_id(Some("key1".to_string()))
1890            .with_summary(Summary {
1891                operation: Operation::Append,
1892                additional_properties: HashMap::new(),
1893            })
1894            .with_manifest_list("/home/iceberg/warehouse/nyc/taxis/metadata/snap-638933773299822130-1-7e6760f0-4f6c-4b23-b907-0a5a174e3863.avro".to_string())
1895            .with_schema_id(0)
1896            .build();
1897
1898        let encryption_key = EncryptedKey::builder()
1899            .key_id("key1".to_string())
1900            .encrypted_by_id("KMS".to_string())
1901            .encrypted_key_metadata(
1902                base64::prelude::BASE64_STANDARD
1903                    .decode("c29tZS1lbmNyeXB0aW9uLWtleQ==")
1904                    .unwrap(),
1905            )
1906            .properties(HashMap::from_iter(vec![(
1907                "p1".to_string(),
1908                "v1".to_string(),
1909            )]))
1910            .build();
1911
1912        let default_partition_type = partition_spec.partition_type(&schema).unwrap();
1913        let expected = TableMetadata {
1914            format_version: FormatVersion::V3,
1915            table_uuid: Uuid::parse_str("fb072c92-a02b-11e9-ae9c-1bb7bc9eca94").unwrap(),
1916            location: "s3://b/wh/data.db/table".to_string(),
1917            last_updated_ms: 1515100955770,
1918            last_column_id: 1,
1919            schemas: HashMap::from_iter(vec![(1, Arc::new(schema))]),
1920            current_schema_id: 1,
1921            partition_specs: HashMap::from_iter(vec![(0, partition_spec.clone().into())]),
1922            default_partition_type,
1923            default_spec: partition_spec.into(),
1924            last_partition_id: 1000,
1925            default_sort_order_id: 0,
1926            sort_orders: HashMap::from_iter(vec![(0, SortOrder::unsorted_order().into())]),
1927            snapshots: HashMap::from_iter(vec![(1, snapshot.into())]),
1928            current_snapshot_id: None,
1929            last_sequence_number: 1,
1930            properties: HashMap::from_iter(vec![(
1931                "commit.retry.num-retries".to_string(),
1932                "1".to_string(),
1933            )]),
1934            snapshot_log: Vec::new(),
1935            metadata_log: vec![MetadataLog {
1936                metadata_file: "s3://bucket/.../v1.json".to_string(),
1937                timestamp_ms: 1515100,
1938            }],
1939            refs: HashMap::new(),
1940            statistics: HashMap::new(),
1941            partition_statistics: HashMap::new(),
1942            encryption_keys: HashMap::from_iter(vec![("key1".to_string(), encryption_key)]),
1943            next_row_id: 5,
1944        };
1945
1946        let expected_json_value = serde_json::to_value(&expected).unwrap();
1947        check_table_metadata_serde(data, expected);
1948
1949        let json_value = serde_json::from_str::<serde_json::Value>(data).unwrap();
1950        assert_eq!(json_value, expected_json_value);
1951    }
1952
1953    #[test]
1954    fn test_table_data_v1() {
1955        let data = r#"
1956        {
1957            "format-version" : 1,
1958            "table-uuid" : "df838b92-0b32-465d-a44e-d39936e538b7",
1959            "location" : "/home/iceberg/warehouse/nyc/taxis",
1960            "last-updated-ms" : 1662532818843,
1961            "last-column-id" : 5,
1962            "schema" : {
1963              "type" : "struct",
1964              "schema-id" : 0,
1965              "fields" : [ {
1966                "id" : 1,
1967                "name" : "vendor_id",
1968                "required" : false,
1969                "type" : "long"
1970              }, {
1971                "id" : 2,
1972                "name" : "trip_id",
1973                "required" : false,
1974                "type" : "long"
1975              }, {
1976                "id" : 3,
1977                "name" : "trip_distance",
1978                "required" : false,
1979                "type" : "float"
1980              }, {
1981                "id" : 4,
1982                "name" : "fare_amount",
1983                "required" : false,
1984                "type" : "double"
1985              }, {
1986                "id" : 5,
1987                "name" : "store_and_fwd_flag",
1988                "required" : false,
1989                "type" : "string"
1990              } ]
1991            },
1992            "partition-spec" : [ {
1993              "name" : "vendor_id",
1994              "transform" : "identity",
1995              "source-id" : 1,
1996              "field-id" : 1000
1997            } ],
1998            "last-partition-id" : 1000,
1999            "default-sort-order-id" : 0,
2000            "sort-orders" : [ {
2001              "order-id" : 0,
2002              "fields" : [ ]
2003            } ],
2004            "properties" : {
2005              "owner" : "root"
2006            },
2007            "current-snapshot-id" : 638933773299822130,
2008            "refs" : {
2009              "main" : {
2010                "snapshot-id" : 638933773299822130,
2011                "type" : "branch"
2012              }
2013            },
2014            "snapshots" : [ {
2015              "snapshot-id" : 638933773299822130,
2016              "timestamp-ms" : 1662532818843,
2017              "sequence-number" : 0,
2018              "summary" : {
2019                "operation" : "append",
2020                "spark.app.id" : "local-1662532784305",
2021                "added-data-files" : "4",
2022                "added-records" : "4",
2023                "added-files-size" : "6001"
2024              },
2025              "manifest-list" : "/home/iceberg/warehouse/nyc/taxis/metadata/snap-638933773299822130-1-7e6760f0-4f6c-4b23-b907-0a5a174e3863.avro",
2026              "schema-id" : 0
2027            } ],
2028            "snapshot-log" : [ {
2029              "timestamp-ms" : 1662532818843,
2030              "snapshot-id" : 638933773299822130
2031            } ],
2032            "metadata-log" : [ {
2033              "timestamp-ms" : 1662532805245,
2034              "metadata-file" : "/home/iceberg/warehouse/nyc/taxis/metadata/00000-8a62c37d-4573-4021-952a-c0baef7d21d0.metadata.json"
2035            } ]
2036          }
2037        "#;
2038
2039        let schema = Schema::builder()
2040            .with_fields(vec![
2041                Arc::new(NestedField::optional(
2042                    1,
2043                    "vendor_id",
2044                    Type::Primitive(PrimitiveType::Long),
2045                )),
2046                Arc::new(NestedField::optional(
2047                    2,
2048                    "trip_id",
2049                    Type::Primitive(PrimitiveType::Long),
2050                )),
2051                Arc::new(NestedField::optional(
2052                    3,
2053                    "trip_distance",
2054                    Type::Primitive(PrimitiveType::Float),
2055                )),
2056                Arc::new(NestedField::optional(
2057                    4,
2058                    "fare_amount",
2059                    Type::Primitive(PrimitiveType::Double),
2060                )),
2061                Arc::new(NestedField::optional(
2062                    5,
2063                    "store_and_fwd_flag",
2064                    Type::Primitive(PrimitiveType::String),
2065                )),
2066            ])
2067            .build()
2068            .unwrap();
2069
2070        let schema = Arc::new(schema);
2071        let partition_spec = PartitionSpec::builder(schema.clone())
2072            .with_spec_id(0)
2073            .add_partition_field("vendor_id", "vendor_id", Transform::Identity)
2074            .unwrap()
2075            .build()
2076            .unwrap();
2077
2078        let sort_order = SortOrder::builder()
2079            .with_order_id(0)
2080            .build_unbound()
2081            .unwrap();
2082
2083        let snapshot = Snapshot::builder()
2084            .with_snapshot_id(638933773299822130)
2085            .with_timestamp_ms(1662532818843)
2086            .with_sequence_number(0)
2087            .with_schema_id(0)
2088            .with_manifest_list("/home/iceberg/warehouse/nyc/taxis/metadata/snap-638933773299822130-1-7e6760f0-4f6c-4b23-b907-0a5a174e3863.avro")
2089            .with_summary(Summary { operation: Operation::Append, additional_properties: HashMap::from_iter(vec![("spark.app.id".to_string(), "local-1662532784305".to_string()), ("added-data-files".to_string(), "4".to_string()), ("added-records".to_string(), "4".to_string()), ("added-files-size".to_string(), "6001".to_string())]) })
2090            .build();
2091
2092        let default_partition_type = partition_spec.partition_type(&schema).unwrap();
2093        let expected = TableMetadata {
2094            format_version: FormatVersion::V1,
2095            table_uuid: Uuid::parse_str("df838b92-0b32-465d-a44e-d39936e538b7").unwrap(),
2096            location: "/home/iceberg/warehouse/nyc/taxis".to_string(),
2097            last_updated_ms: 1662532818843,
2098            last_column_id: 5,
2099            schemas: HashMap::from_iter(vec![(0, schema)]),
2100            current_schema_id: 0,
2101            partition_specs: HashMap::from_iter(vec![(0, partition_spec.clone().into())]),
2102            default_partition_type,
2103            default_spec: Arc::new(partition_spec),
2104            last_partition_id: 1000,
2105            default_sort_order_id: 0,
2106            sort_orders: HashMap::from_iter(vec![(0, sort_order.into())]),
2107            snapshots: HashMap::from_iter(vec![(638933773299822130, Arc::new(snapshot))]),
2108            current_snapshot_id: Some(638933773299822130),
2109            last_sequence_number: 0,
2110            properties: HashMap::from_iter(vec![("owner".to_string(), "root".to_string())]),
2111            snapshot_log: vec![SnapshotLog {
2112                snapshot_id: 638933773299822130,
2113                timestamp_ms: 1662532818843,
2114            }],
2115            metadata_log: vec![MetadataLog { metadata_file: "/home/iceberg/warehouse/nyc/taxis/metadata/00000-8a62c37d-4573-4021-952a-c0baef7d21d0.metadata.json".to_string(), timestamp_ms: 1662532805245 }],
2116            refs: HashMap::from_iter(vec![("main".to_string(), SnapshotReference { snapshot_id: 638933773299822130, retention: SnapshotRetention::Branch { min_snapshots_to_keep: None, max_snapshot_age_ms: None, max_ref_age_ms: None } })]),
2117            statistics: HashMap::new(),
2118            partition_statistics: HashMap::new(),
2119            encryption_keys: HashMap::new(),
2120            next_row_id: INITIAL_ROW_ID,
2121        };
2122
2123        check_table_metadata_serde(data, expected);
2124    }
2125
2126    #[test]
2127    fn test_table_data_v2_no_snapshots() {
2128        let data = r#"
2129        {
2130            "format-version" : 2,
2131            "table-uuid": "fb072c92-a02b-11e9-ae9c-1bb7bc9eca94",
2132            "location": "s3://b/wh/data.db/table",
2133            "last-sequence-number" : 1,
2134            "last-updated-ms": 1515100955770,
2135            "last-column-id": 1,
2136            "schemas": [
2137                {
2138                    "schema-id" : 1,
2139                    "type" : "struct",
2140                    "fields" :[
2141                        {
2142                            "id": 1,
2143                            "name": "struct_name",
2144                            "required": true,
2145                            "type": "fixed[1]"
2146                        }
2147                    ]
2148                }
2149            ],
2150            "current-schema-id" : 1,
2151            "partition-specs": [
2152                {
2153                    "spec-id": 0,
2154                    "fields": []
2155                }
2156            ],
2157            "refs": {},
2158            "default-spec-id": 0,
2159            "last-partition-id": 1000,
2160            "metadata-log": [
2161                {
2162                    "metadata-file": "s3://bucket/.../v1.json",
2163                    "timestamp-ms": 1515100
2164                }
2165            ],
2166            "sort-orders": [
2167                {
2168                "order-id": 0,
2169                "fields": []
2170                }
2171            ],
2172            "default-sort-order-id": 0
2173        }
2174        "#;
2175
2176        let schema = Schema::builder()
2177            .with_schema_id(1)
2178            .with_fields(vec![Arc::new(NestedField::required(
2179                1,
2180                "struct_name",
2181                Type::Primitive(PrimitiveType::Fixed(1)),
2182            ))])
2183            .build()
2184            .unwrap();
2185
2186        let partition_spec = PartitionSpec::builder(schema.clone())
2187            .with_spec_id(0)
2188            .build()
2189            .unwrap();
2190
2191        let default_partition_type = partition_spec.partition_type(&schema).unwrap();
2192        let expected = TableMetadata {
2193            format_version: FormatVersion::V2,
2194            table_uuid: Uuid::parse_str("fb072c92-a02b-11e9-ae9c-1bb7bc9eca94").unwrap(),
2195            location: "s3://b/wh/data.db/table".to_string(),
2196            last_updated_ms: 1515100955770,
2197            last_column_id: 1,
2198            schemas: HashMap::from_iter(vec![(1, Arc::new(schema))]),
2199            current_schema_id: 1,
2200            partition_specs: HashMap::from_iter(vec![(0, partition_spec.clone().into())]),
2201            default_partition_type,
2202            default_spec: partition_spec.into(),
2203            last_partition_id: 1000,
2204            default_sort_order_id: 0,
2205            sort_orders: HashMap::from_iter(vec![(0, SortOrder::unsorted_order().into())]),
2206            snapshots: HashMap::default(),
2207            current_snapshot_id: None,
2208            last_sequence_number: 1,
2209            properties: HashMap::new(),
2210            snapshot_log: Vec::new(),
2211            metadata_log: vec![MetadataLog {
2212                metadata_file: "s3://bucket/.../v1.json".to_string(),
2213                timestamp_ms: 1515100,
2214            }],
2215            refs: HashMap::new(),
2216            statistics: HashMap::new(),
2217            partition_statistics: HashMap::new(),
2218            encryption_keys: HashMap::new(),
2219            next_row_id: INITIAL_ROW_ID,
2220        };
2221
2222        let expected_json_value = serde_json::to_value(&expected).unwrap();
2223        check_table_metadata_serde(data, expected);
2224
2225        let json_value = serde_json::from_str::<serde_json::Value>(data).unwrap();
2226        assert_eq!(json_value, expected_json_value);
2227    }
2228
2229    #[test]
2230    fn test_current_snapshot_id_must_match_main_branch() {
2231        let data = r#"
2232        {
2233            "format-version" : 2,
2234            "table-uuid": "fb072c92-a02b-11e9-ae9c-1bb7bc9eca94",
2235            "location": "s3://b/wh/data.db/table",
2236            "last-sequence-number" : 1,
2237            "last-updated-ms": 1515100955770,
2238            "last-column-id": 1,
2239            "schemas": [
2240                {
2241                    "schema-id" : 1,
2242                    "type" : "struct",
2243                    "fields" :[
2244                        {
2245                            "id": 1,
2246                            "name": "struct_name",
2247                            "required": true,
2248                            "type": "fixed[1]"
2249                        },
2250                        {
2251                            "id": 4,
2252                            "name": "ts",
2253                            "required": true,
2254                            "type": "timestamp"
2255                        }
2256                    ]
2257                }
2258            ],
2259            "current-schema-id" : 1,
2260            "partition-specs": [
2261                {
2262                    "spec-id": 0,
2263                    "fields": [
2264                        {
2265                            "source-id": 4,
2266                            "field-id": 1000,
2267                            "name": "ts_day",
2268                            "transform": "day"
2269                        }
2270                    ]
2271                }
2272            ],
2273            "default-spec-id": 0,
2274            "last-partition-id": 1000,
2275            "properties": {
2276                "commit.retry.num-retries": "1"
2277            },
2278            "metadata-log": [
2279                {
2280                    "metadata-file": "s3://bucket/.../v1.json",
2281                    "timestamp-ms": 1515100
2282                }
2283            ],
2284            "sort-orders": [
2285                {
2286                "order-id": 0,
2287                "fields": []
2288                }
2289            ],
2290            "default-sort-order-id": 0,
2291            "current-snapshot-id" : 1,
2292            "refs" : {
2293              "main" : {
2294                "snapshot-id" : 2,
2295                "type" : "branch"
2296              }
2297            },
2298            "snapshots" : [ {
2299              "snapshot-id" : 1,
2300              "timestamp-ms" : 1662532818843,
2301              "sequence-number" : 0,
2302              "summary" : {
2303                "operation" : "append",
2304                "spark.app.id" : "local-1662532784305",
2305                "added-data-files" : "4",
2306                "added-records" : "4",
2307                "added-files-size" : "6001"
2308              },
2309              "manifest-list" : "/home/iceberg/warehouse/nyc/taxis/metadata/snap-638933773299822130-1-7e6760f0-4f6c-4b23-b907-0a5a174e3863.avro",
2310              "schema-id" : 0
2311            },
2312            {
2313              "snapshot-id" : 2,
2314              "timestamp-ms" : 1662532818844,
2315              "sequence-number" : 0,
2316              "summary" : {
2317                "operation" : "append",
2318                "spark.app.id" : "local-1662532784305",
2319                "added-data-files" : "4",
2320                "added-records" : "4",
2321                "added-files-size" : "6001"
2322              },
2323              "manifest-list" : "/home/iceberg/warehouse/nyc/taxis/metadata/snap-638933773299822130-1-7e6760f0-4f6c-4b23-b907-0a5a174e3863.avro",
2324              "schema-id" : 0
2325            } ]
2326        }
2327    "#;
2328
2329        let err = serde_json::from_str::<TableMetadata>(data).unwrap_err();
2330        assert!(
2331            err.to_string()
2332                .contains("Current snapshot id does not match main branch")
2333        );
2334    }
2335
2336    #[test]
2337    fn test_main_without_current() {
2338        let data = r#"
2339        {
2340            "format-version" : 2,
2341            "table-uuid": "fb072c92-a02b-11e9-ae9c-1bb7bc9eca94",
2342            "location": "s3://b/wh/data.db/table",
2343            "last-sequence-number" : 1,
2344            "last-updated-ms": 1515100955770,
2345            "last-column-id": 1,
2346            "schemas": [
2347                {
2348                    "schema-id" : 1,
2349                    "type" : "struct",
2350                    "fields" :[
2351                        {
2352                            "id": 1,
2353                            "name": "struct_name",
2354                            "required": true,
2355                            "type": "fixed[1]"
2356                        },
2357                        {
2358                            "id": 4,
2359                            "name": "ts",
2360                            "required": true,
2361                            "type": "timestamp"
2362                        }
2363                    ]
2364                }
2365            ],
2366            "current-schema-id" : 1,
2367            "partition-specs": [
2368                {
2369                    "spec-id": 0,
2370                    "fields": [
2371                        {
2372                            "source-id": 4,
2373                            "field-id": 1000,
2374                            "name": "ts_day",
2375                            "transform": "day"
2376                        }
2377                    ]
2378                }
2379            ],
2380            "default-spec-id": 0,
2381            "last-partition-id": 1000,
2382            "properties": {
2383                "commit.retry.num-retries": "1"
2384            },
2385            "metadata-log": [
2386                {
2387                    "metadata-file": "s3://bucket/.../v1.json",
2388                    "timestamp-ms": 1515100
2389                }
2390            ],
2391            "sort-orders": [
2392                {
2393                "order-id": 0,
2394                "fields": []
2395                }
2396            ],
2397            "default-sort-order-id": 0,
2398            "refs" : {
2399              "main" : {
2400                "snapshot-id" : 1,
2401                "type" : "branch"
2402              }
2403            },
2404            "snapshots" : [ {
2405              "snapshot-id" : 1,
2406              "timestamp-ms" : 1662532818843,
2407              "sequence-number" : 0,
2408              "summary" : {
2409                "operation" : "append",
2410                "spark.app.id" : "local-1662532784305",
2411                "added-data-files" : "4",
2412                "added-records" : "4",
2413                "added-files-size" : "6001"
2414              },
2415              "manifest-list" : "/home/iceberg/warehouse/nyc/taxis/metadata/snap-638933773299822130-1-7e6760f0-4f6c-4b23-b907-0a5a174e3863.avro",
2416              "schema-id" : 0
2417            } ]
2418        }
2419    "#;
2420
2421        let err = serde_json::from_str::<TableMetadata>(data).unwrap_err();
2422        assert!(
2423            err.to_string()
2424                .contains("Current snapshot is not set, but main branch exists")
2425        );
2426    }
2427
2428    #[test]
2429    fn test_branch_snapshot_missing() {
2430        let data = r#"
2431        {
2432            "format-version" : 2,
2433            "table-uuid": "fb072c92-a02b-11e9-ae9c-1bb7bc9eca94",
2434            "location": "s3://b/wh/data.db/table",
2435            "last-sequence-number" : 1,
2436            "last-updated-ms": 1515100955770,
2437            "last-column-id": 1,
2438            "schemas": [
2439                {
2440                    "schema-id" : 1,
2441                    "type" : "struct",
2442                    "fields" :[
2443                        {
2444                            "id": 1,
2445                            "name": "struct_name",
2446                            "required": true,
2447                            "type": "fixed[1]"
2448                        },
2449                        {
2450                            "id": 4,
2451                            "name": "ts",
2452                            "required": true,
2453                            "type": "timestamp"
2454                        }
2455                    ]
2456                }
2457            ],
2458            "current-schema-id" : 1,
2459            "partition-specs": [
2460                {
2461                    "spec-id": 0,
2462                    "fields": [
2463                        {
2464                            "source-id": 4,
2465                            "field-id": 1000,
2466                            "name": "ts_day",
2467                            "transform": "day"
2468                        }
2469                    ]
2470                }
2471            ],
2472            "default-spec-id": 0,
2473            "last-partition-id": 1000,
2474            "properties": {
2475                "commit.retry.num-retries": "1"
2476            },
2477            "metadata-log": [
2478                {
2479                    "metadata-file": "s3://bucket/.../v1.json",
2480                    "timestamp-ms": 1515100
2481                }
2482            ],
2483            "sort-orders": [
2484                {
2485                "order-id": 0,
2486                "fields": []
2487                }
2488            ],
2489            "default-sort-order-id": 0,
2490            "refs" : {
2491              "main" : {
2492                "snapshot-id" : 1,
2493                "type" : "branch"
2494              },
2495              "foo" : {
2496                "snapshot-id" : 2,
2497                "type" : "branch"
2498              }
2499            },
2500            "snapshots" : [ {
2501              "snapshot-id" : 1,
2502              "timestamp-ms" : 1662532818843,
2503              "sequence-number" : 0,
2504              "summary" : {
2505                "operation" : "append",
2506                "spark.app.id" : "local-1662532784305",
2507                "added-data-files" : "4",
2508                "added-records" : "4",
2509                "added-files-size" : "6001"
2510              },
2511              "manifest-list" : "/home/iceberg/warehouse/nyc/taxis/metadata/snap-638933773299822130-1-7e6760f0-4f6c-4b23-b907-0a5a174e3863.avro",
2512              "schema-id" : 0
2513            } ]
2514        }
2515    "#;
2516
2517        let err = serde_json::from_str::<TableMetadata>(data).unwrap_err();
2518        assert!(
2519            err.to_string().contains(
2520                "Snapshot for reference foo does not exist in the existing snapshots list"
2521            )
2522        );
2523    }
2524
2525    #[test]
2526    fn test_v2_wrong_max_snapshot_sequence_number() {
2527        let data = r#"
2528        {
2529            "format-version": 2,
2530            "table-uuid": "9c12d441-03fe-4693-9a96-a0705ddf69c1",
2531            "location": "s3://bucket/test/location",
2532            "last-sequence-number": 1,
2533            "last-updated-ms": 1602638573590,
2534            "last-column-id": 3,
2535            "current-schema-id": 0,
2536            "schemas": [
2537                {
2538                    "type": "struct",
2539                    "schema-id": 0,
2540                    "fields": [
2541                        {
2542                            "id": 1,
2543                            "name": "x",
2544                            "required": true,
2545                            "type": "long"
2546                        }
2547                    ]
2548                }
2549            ],
2550            "default-spec-id": 0,
2551            "partition-specs": [
2552                {
2553                    "spec-id": 0,
2554                    "fields": []
2555                }
2556            ],
2557            "last-partition-id": 1000,
2558            "default-sort-order-id": 0,
2559            "sort-orders": [
2560                {
2561                    "order-id": 0,
2562                    "fields": []
2563                }
2564            ],
2565            "properties": {},
2566            "current-snapshot-id": 3055729675574597004,
2567            "snapshots": [
2568                {
2569                    "snapshot-id": 3055729675574597004,
2570                    "timestamp-ms": 1555100955770,
2571                    "sequence-number": 4,
2572                    "summary": {
2573                        "operation": "append"
2574                    },
2575                    "manifest-list": "s3://a/b/2.avro",
2576                    "schema-id": 0
2577                }
2578            ],
2579            "statistics": [],
2580            "snapshot-log": [],
2581            "metadata-log": []
2582        }
2583    "#;
2584
2585        let err = serde_json::from_str::<TableMetadata>(data).unwrap_err();
2586        assert!(err.to_string().contains(
2587            "Invalid snapshot with id 3055729675574597004 and sequence number 4 greater than last sequence number 1"
2588        ));
2589
2590        // Change max sequence number to 4 - should work
2591        let data = data.replace(
2592            r#""last-sequence-number": 1,"#,
2593            r#""last-sequence-number": 4,"#,
2594        );
2595        let metadata = serde_json::from_str::<TableMetadata>(data.as_str()).unwrap();
2596        assert_eq!(metadata.last_sequence_number, 4);
2597
2598        // Change max sequence number to 5 - should work
2599        let data = data.replace(
2600            r#""last-sequence-number": 4,"#,
2601            r#""last-sequence-number": 5,"#,
2602        );
2603        let metadata = serde_json::from_str::<TableMetadata>(data.as_str()).unwrap();
2604        assert_eq!(metadata.last_sequence_number, 5);
2605    }
2606
2607    #[test]
2608    fn test_statistic_files() {
2609        let data = r#"
2610        {
2611            "format-version": 2,
2612            "table-uuid": "9c12d441-03fe-4693-9a96-a0705ddf69c1",
2613            "location": "s3://bucket/test/location",
2614            "last-sequence-number": 34,
2615            "last-updated-ms": 1602638573590,
2616            "last-column-id": 3,
2617            "current-schema-id": 0,
2618            "schemas": [
2619                {
2620                    "type": "struct",
2621                    "schema-id": 0,
2622                    "fields": [
2623                        {
2624                            "id": 1,
2625                            "name": "x",
2626                            "required": true,
2627                            "type": "long"
2628                        }
2629                    ]
2630                }
2631            ],
2632            "default-spec-id": 0,
2633            "partition-specs": [
2634                {
2635                    "spec-id": 0,
2636                    "fields": []
2637                }
2638            ],
2639            "last-partition-id": 1000,
2640            "default-sort-order-id": 0,
2641            "sort-orders": [
2642                {
2643                    "order-id": 0,
2644                    "fields": []
2645                }
2646            ],
2647            "properties": {},
2648            "current-snapshot-id": 3055729675574597004,
2649            "snapshots": [
2650                {
2651                    "snapshot-id": 3055729675574597004,
2652                    "timestamp-ms": 1555100955770,
2653                    "sequence-number": 1,
2654                    "summary": {
2655                        "operation": "append"
2656                    },
2657                    "manifest-list": "s3://a/b/2.avro",
2658                    "schema-id": 0
2659                }
2660            ],
2661            "statistics": [
2662                {
2663                    "snapshot-id": 3055729675574597004,
2664                    "statistics-path": "s3://a/b/stats.puffin",
2665                    "file-size-in-bytes": 413,
2666                    "file-footer-size-in-bytes": 42,
2667                    "blob-metadata": [
2668                        {
2669                            "type": "ndv",
2670                            "snapshot-id": 3055729675574597004,
2671                            "sequence-number": 1,
2672                            "fields": [
2673                                1
2674                            ]
2675                        }
2676                    ]
2677                }
2678            ],
2679            "snapshot-log": [],
2680            "metadata-log": []
2681        }
2682    "#;
2683
2684        let schema = Schema::builder()
2685            .with_schema_id(0)
2686            .with_fields(vec![Arc::new(NestedField::required(
2687                1,
2688                "x",
2689                Type::Primitive(PrimitiveType::Long),
2690            ))])
2691            .build()
2692            .unwrap();
2693        let partition_spec = PartitionSpec::builder(schema.clone())
2694            .with_spec_id(0)
2695            .build()
2696            .unwrap();
2697        let snapshot = Snapshot::builder()
2698            .with_snapshot_id(3055729675574597004)
2699            .with_timestamp_ms(1555100955770)
2700            .with_sequence_number(1)
2701            .with_manifest_list("s3://a/b/2.avro")
2702            .with_schema_id(0)
2703            .with_summary(Summary {
2704                operation: Operation::Append,
2705                additional_properties: HashMap::new(),
2706            })
2707            .build();
2708
2709        let default_partition_type = partition_spec.partition_type(&schema).unwrap();
2710        let expected = TableMetadata {
2711            format_version: FormatVersion::V2,
2712            table_uuid: Uuid::parse_str("9c12d441-03fe-4693-9a96-a0705ddf69c1").unwrap(),
2713            location: "s3://bucket/test/location".to_string(),
2714            last_updated_ms: 1602638573590,
2715            last_column_id: 3,
2716            schemas: HashMap::from_iter(vec![(0, Arc::new(schema))]),
2717            current_schema_id: 0,
2718            partition_specs: HashMap::from_iter(vec![(0, partition_spec.clone().into())]),
2719            default_partition_type,
2720            default_spec: Arc::new(partition_spec),
2721            last_partition_id: 1000,
2722            default_sort_order_id: 0,
2723            sort_orders: HashMap::from_iter(vec![(0, SortOrder::unsorted_order().into())]),
2724            snapshots: HashMap::from_iter(vec![(3055729675574597004, Arc::new(snapshot))]),
2725            current_snapshot_id: Some(3055729675574597004),
2726            last_sequence_number: 34,
2727            properties: HashMap::new(),
2728            snapshot_log: Vec::new(),
2729            metadata_log: Vec::new(),
2730            statistics: HashMap::from_iter(vec![(3055729675574597004, StatisticsFile {
2731                snapshot_id: 3055729675574597004,
2732                statistics_path: "s3://a/b/stats.puffin".to_string(),
2733                file_size_in_bytes: 413,
2734                file_footer_size_in_bytes: 42,
2735                key_metadata: None,
2736                blob_metadata: vec![BlobMetadata {
2737                    snapshot_id: 3055729675574597004,
2738                    sequence_number: 1,
2739                    fields: vec![1],
2740                    r#type: "ndv".to_string(),
2741                    properties: HashMap::new(),
2742                }],
2743            })]),
2744            partition_statistics: HashMap::new(),
2745            refs: HashMap::from_iter(vec![("main".to_string(), SnapshotReference {
2746                snapshot_id: 3055729675574597004,
2747                retention: SnapshotRetention::Branch {
2748                    min_snapshots_to_keep: None,
2749                    max_snapshot_age_ms: None,
2750                    max_ref_age_ms: None,
2751                },
2752            })]),
2753            encryption_keys: HashMap::new(),
2754            next_row_id: INITIAL_ROW_ID,
2755        };
2756
2757        check_table_metadata_serde(data, expected);
2758    }
2759
2760    #[test]
2761    fn test_partition_statistics_file() {
2762        let data = r#"
2763        {
2764            "format-version": 2,
2765            "table-uuid": "9c12d441-03fe-4693-9a96-a0705ddf69c1",
2766            "location": "s3://bucket/test/location",
2767            "last-sequence-number": 34,
2768            "last-updated-ms": 1602638573590,
2769            "last-column-id": 3,
2770            "current-schema-id": 0,
2771            "schemas": [
2772                {
2773                    "type": "struct",
2774                    "schema-id": 0,
2775                    "fields": [
2776                        {
2777                            "id": 1,
2778                            "name": "x",
2779                            "required": true,
2780                            "type": "long"
2781                        }
2782                    ]
2783                }
2784            ],
2785            "default-spec-id": 0,
2786            "partition-specs": [
2787                {
2788                    "spec-id": 0,
2789                    "fields": []
2790                }
2791            ],
2792            "last-partition-id": 1000,
2793            "default-sort-order-id": 0,
2794            "sort-orders": [
2795                {
2796                    "order-id": 0,
2797                    "fields": []
2798                }
2799            ],
2800            "properties": {},
2801            "current-snapshot-id": 3055729675574597004,
2802            "snapshots": [
2803                {
2804                    "snapshot-id": 3055729675574597004,
2805                    "timestamp-ms": 1555100955770,
2806                    "sequence-number": 1,
2807                    "summary": {
2808                        "operation": "append"
2809                    },
2810                    "manifest-list": "s3://a/b/2.avro",
2811                    "schema-id": 0
2812                }
2813            ],
2814            "partition-statistics": [
2815                {
2816                    "snapshot-id": 3055729675574597004,
2817                    "statistics-path": "s3://a/b/partition-stats.parquet",
2818                    "file-size-in-bytes": 43
2819                }
2820            ],
2821            "snapshot-log": [],
2822            "metadata-log": []
2823        }
2824        "#;
2825
2826        let schema = Schema::builder()
2827            .with_schema_id(0)
2828            .with_fields(vec![Arc::new(NestedField::required(
2829                1,
2830                "x",
2831                Type::Primitive(PrimitiveType::Long),
2832            ))])
2833            .build()
2834            .unwrap();
2835        let partition_spec = PartitionSpec::builder(schema.clone())
2836            .with_spec_id(0)
2837            .build()
2838            .unwrap();
2839        let snapshot = Snapshot::builder()
2840            .with_snapshot_id(3055729675574597004)
2841            .with_timestamp_ms(1555100955770)
2842            .with_sequence_number(1)
2843            .with_manifest_list("s3://a/b/2.avro")
2844            .with_schema_id(0)
2845            .with_summary(Summary {
2846                operation: Operation::Append,
2847                additional_properties: HashMap::new(),
2848            })
2849            .build();
2850
2851        let default_partition_type = partition_spec.partition_type(&schema).unwrap();
2852        let expected = TableMetadata {
2853            format_version: FormatVersion::V2,
2854            table_uuid: Uuid::parse_str("9c12d441-03fe-4693-9a96-a0705ddf69c1").unwrap(),
2855            location: "s3://bucket/test/location".to_string(),
2856            last_updated_ms: 1602638573590,
2857            last_column_id: 3,
2858            schemas: HashMap::from_iter(vec![(0, Arc::new(schema))]),
2859            current_schema_id: 0,
2860            partition_specs: HashMap::from_iter(vec![(0, partition_spec.clone().into())]),
2861            default_spec: Arc::new(partition_spec),
2862            default_partition_type,
2863            last_partition_id: 1000,
2864            default_sort_order_id: 0,
2865            sort_orders: HashMap::from_iter(vec![(0, SortOrder::unsorted_order().into())]),
2866            snapshots: HashMap::from_iter(vec![(3055729675574597004, Arc::new(snapshot))]),
2867            current_snapshot_id: Some(3055729675574597004),
2868            last_sequence_number: 34,
2869            properties: HashMap::new(),
2870            snapshot_log: Vec::new(),
2871            metadata_log: Vec::new(),
2872            statistics: HashMap::new(),
2873            partition_statistics: HashMap::from_iter(vec![(
2874                3055729675574597004,
2875                PartitionStatisticsFile {
2876                    snapshot_id: 3055729675574597004,
2877                    statistics_path: "s3://a/b/partition-stats.parquet".to_string(),
2878                    file_size_in_bytes: 43,
2879                },
2880            )]),
2881            refs: HashMap::from_iter(vec![("main".to_string(), SnapshotReference {
2882                snapshot_id: 3055729675574597004,
2883                retention: SnapshotRetention::Branch {
2884                    min_snapshots_to_keep: None,
2885                    max_snapshot_age_ms: None,
2886                    max_ref_age_ms: None,
2887                },
2888            })]),
2889            encryption_keys: HashMap::new(),
2890            next_row_id: INITIAL_ROW_ID,
2891        };
2892
2893        check_table_metadata_serde(data, expected);
2894    }
2895
2896    #[test]
2897    fn test_invalid_table_uuid() -> Result<()> {
2898        let data = r#"
2899            {
2900                "format-version" : 2,
2901                "table-uuid": "xxxx"
2902            }
2903        "#;
2904        assert!(serde_json::from_str::<TableMetadata>(data).is_err());
2905        Ok(())
2906    }
2907
2908    #[test]
2909    fn test_deserialize_table_data_v2_invalid_format_version() -> Result<()> {
2910        let data = r#"
2911            {
2912                "format-version" : 1
2913            }
2914        "#;
2915        assert!(serde_json::from_str::<TableMetadata>(data).is_err());
2916        Ok(())
2917    }
2918
2919    #[test]
2920    fn test_table_metadata_v3_valid_minimal() {
2921        let metadata_str =
2922            fs::read_to_string("testdata/table_metadata/TableMetadataV3ValidMinimal.json").unwrap();
2923
2924        let table_metadata = serde_json::from_str::<TableMetadata>(&metadata_str).unwrap();
2925        assert_eq!(table_metadata.format_version, FormatVersion::V3);
2926
2927        let schema = Schema::builder()
2928            .with_schema_id(0)
2929            .with_fields(vec![
2930                Arc::new(
2931                    NestedField::required(1, "x", Type::Primitive(PrimitiveType::Long))
2932                        .with_initial_default(Literal::Primitive(PrimitiveLiteral::Long(1)))
2933                        .with_write_default(Literal::Primitive(PrimitiveLiteral::Long(1))),
2934                ),
2935                Arc::new(
2936                    NestedField::required(2, "y", Type::Primitive(PrimitiveType::Long))
2937                        .with_doc("comment"),
2938                ),
2939                Arc::new(NestedField::required(
2940                    3,
2941                    "z",
2942                    Type::Primitive(PrimitiveType::Long),
2943                )),
2944            ])
2945            .build()
2946            .unwrap();
2947
2948        let partition_spec = PartitionSpec::builder(schema.clone())
2949            .with_spec_id(0)
2950            .add_unbound_field(
2951                UnboundPartitionField::builder()
2952                    .source_ids(vec![1])
2953                    .field_id(1000)
2954                    .name("x".to_string())
2955                    .transform(Transform::Identity)
2956                    .build()
2957                    .unwrap(),
2958            )
2959            .unwrap()
2960            .build()
2961            .unwrap();
2962
2963        let sort_order = SortOrder::builder()
2964            .with_order_id(3)
2965            .with_sort_field(SortField {
2966                source_id: 2,
2967                transform: Transform::Identity,
2968                direction: SortDirection::Ascending,
2969                null_order: NullOrder::First,
2970            })
2971            .with_sort_field(SortField {
2972                source_id: 3,
2973                transform: Transform::Bucket(4),
2974                direction: SortDirection::Descending,
2975                null_order: NullOrder::Last,
2976            })
2977            .build_unbound()
2978            .unwrap();
2979
2980        let default_partition_type = partition_spec.partition_type(&schema).unwrap();
2981        let expected = TableMetadata {
2982            format_version: FormatVersion::V3,
2983            table_uuid: Uuid::parse_str("9c12d441-03fe-4693-9a96-a0705ddf69c1").unwrap(),
2984            location: "s3://bucket/test/location".to_string(),
2985            last_updated_ms: 1602638573590,
2986            last_column_id: 3,
2987            schemas: HashMap::from_iter(vec![(0, Arc::new(schema))]),
2988            current_schema_id: 0,
2989            partition_specs: HashMap::from_iter(vec![(0, partition_spec.clone().into())]),
2990            default_spec: Arc::new(partition_spec),
2991            default_partition_type,
2992            last_partition_id: 1000,
2993            default_sort_order_id: 3,
2994            sort_orders: HashMap::from_iter(vec![(3, sort_order.into())]),
2995            snapshots: HashMap::default(),
2996            current_snapshot_id: None,
2997            last_sequence_number: 34,
2998            properties: HashMap::new(),
2999            snapshot_log: Vec::new(),
3000            metadata_log: Vec::new(),
3001            refs: HashMap::new(),
3002            statistics: HashMap::new(),
3003            partition_statistics: HashMap::new(),
3004            encryption_keys: HashMap::new(),
3005            next_row_id: 0, // V3 specific field from the JSON
3006        };
3007
3008        check_table_metadata_serde(&metadata_str, expected);
3009    }
3010
3011    #[test]
3012    fn test_table_metadata_v2_file_valid() {
3013        let metadata =
3014            fs::read_to_string("testdata/table_metadata/TableMetadataV2Valid.json").unwrap();
3015
3016        let schema1 = Schema::builder()
3017            .with_schema_id(0)
3018            .with_fields(vec![Arc::new(NestedField::required(
3019                1,
3020                "x",
3021                Type::Primitive(PrimitiveType::Long),
3022            ))])
3023            .build()
3024            .unwrap();
3025
3026        let schema2 = Schema::builder()
3027            .with_schema_id(1)
3028            .with_fields(vec![
3029                Arc::new(NestedField::required(
3030                    1,
3031                    "x",
3032                    Type::Primitive(PrimitiveType::Long),
3033                )),
3034                Arc::new(
3035                    NestedField::required(2, "y", Type::Primitive(PrimitiveType::Long))
3036                        .with_doc("comment"),
3037                ),
3038                Arc::new(NestedField::required(
3039                    3,
3040                    "z",
3041                    Type::Primitive(PrimitiveType::Long),
3042                )),
3043            ])
3044            .with_identifier_field_ids(vec![1, 2])
3045            .build()
3046            .unwrap();
3047
3048        let partition_spec = PartitionSpec::builder(schema2.clone())
3049            .with_spec_id(0)
3050            .add_unbound_field(
3051                UnboundPartitionField::builder()
3052                    .source_ids(vec![1])
3053                    .field_id(1000)
3054                    .name("x".to_string())
3055                    .transform(Transform::Identity)
3056                    .build()
3057                    .unwrap(),
3058            )
3059            .unwrap()
3060            .build()
3061            .unwrap();
3062
3063        let sort_order = SortOrder::builder()
3064            .with_order_id(3)
3065            .with_sort_field(SortField {
3066                source_id: 2,
3067                transform: Transform::Identity,
3068                direction: SortDirection::Ascending,
3069                null_order: NullOrder::First,
3070            })
3071            .with_sort_field(SortField {
3072                source_id: 3,
3073                transform: Transform::Bucket(4),
3074                direction: SortDirection::Descending,
3075                null_order: NullOrder::Last,
3076            })
3077            .build_unbound()
3078            .unwrap();
3079
3080        let snapshot1 = Snapshot::builder()
3081            .with_snapshot_id(3051729675574597004)
3082            .with_timestamp_ms(1515100955770)
3083            .with_sequence_number(0)
3084            .with_manifest_list("s3://a/b/1.avro")
3085            .with_summary(Summary {
3086                operation: Operation::Append,
3087                additional_properties: HashMap::new(),
3088            })
3089            .build();
3090
3091        let snapshot2 = Snapshot::builder()
3092            .with_snapshot_id(3055729675574597004)
3093            .with_parent_snapshot_id(Some(3051729675574597004))
3094            .with_timestamp_ms(1555100955770)
3095            .with_sequence_number(1)
3096            .with_schema_id(1)
3097            .with_manifest_list("s3://a/b/2.avro")
3098            .with_summary(Summary {
3099                operation: Operation::Append,
3100                additional_properties: HashMap::new(),
3101            })
3102            .build();
3103
3104        let default_partition_type = partition_spec.partition_type(&schema2).unwrap();
3105        let expected = TableMetadata {
3106            format_version: FormatVersion::V2,
3107            table_uuid: Uuid::parse_str("9c12d441-03fe-4693-9a96-a0705ddf69c1").unwrap(),
3108            location: "s3://bucket/test/location".to_string(),
3109            last_updated_ms: 1602638573590,
3110            last_column_id: 3,
3111            schemas: HashMap::from_iter(vec![(0, Arc::new(schema1)), (1, Arc::new(schema2))]),
3112            current_schema_id: 1,
3113            partition_specs: HashMap::from_iter(vec![(0, partition_spec.clone().into())]),
3114            default_spec: Arc::new(partition_spec),
3115            default_partition_type,
3116            last_partition_id: 1000,
3117            default_sort_order_id: 3,
3118            sort_orders: HashMap::from_iter(vec![(3, sort_order.into())]),
3119            snapshots: HashMap::from_iter(vec![
3120                (3051729675574597004, Arc::new(snapshot1)),
3121                (3055729675574597004, Arc::new(snapshot2)),
3122            ]),
3123            current_snapshot_id: Some(3055729675574597004),
3124            last_sequence_number: 34,
3125            properties: HashMap::new(),
3126            snapshot_log: vec![
3127                SnapshotLog {
3128                    snapshot_id: 3051729675574597004,
3129                    timestamp_ms: 1515100955770,
3130                },
3131                SnapshotLog {
3132                    snapshot_id: 3055729675574597004,
3133                    timestamp_ms: 1555100955770,
3134                },
3135            ],
3136            metadata_log: Vec::new(),
3137            refs: HashMap::from_iter(vec![("main".to_string(), SnapshotReference {
3138                snapshot_id: 3055729675574597004,
3139                retention: SnapshotRetention::Branch {
3140                    min_snapshots_to_keep: None,
3141                    max_snapshot_age_ms: None,
3142                    max_ref_age_ms: None,
3143                },
3144            })]),
3145            statistics: HashMap::new(),
3146            partition_statistics: HashMap::new(),
3147            encryption_keys: HashMap::new(),
3148            next_row_id: INITIAL_ROW_ID,
3149        };
3150
3151        check_table_metadata_serde(&metadata, expected);
3152    }
3153
3154    #[test]
3155    fn test_table_metadata_v2_file_valid_minimal() {
3156        let metadata =
3157            fs::read_to_string("testdata/table_metadata/TableMetadataV2ValidMinimal.json").unwrap();
3158
3159        let schema = Schema::builder()
3160            .with_schema_id(0)
3161            .with_fields(vec![
3162                Arc::new(NestedField::required(
3163                    1,
3164                    "x",
3165                    Type::Primitive(PrimitiveType::Long),
3166                )),
3167                Arc::new(
3168                    NestedField::required(2, "y", Type::Primitive(PrimitiveType::Long))
3169                        .with_doc("comment"),
3170                ),
3171                Arc::new(NestedField::required(
3172                    3,
3173                    "z",
3174                    Type::Primitive(PrimitiveType::Long),
3175                )),
3176            ])
3177            .build()
3178            .unwrap();
3179
3180        let partition_spec = PartitionSpec::builder(schema.clone())
3181            .with_spec_id(0)
3182            .add_unbound_field(
3183                UnboundPartitionField::builder()
3184                    .source_ids(vec![1])
3185                    .field_id(1000)
3186                    .name("x".to_string())
3187                    .transform(Transform::Identity)
3188                    .build()
3189                    .unwrap(),
3190            )
3191            .unwrap()
3192            .build()
3193            .unwrap();
3194
3195        let sort_order = SortOrder::builder()
3196            .with_order_id(3)
3197            .with_sort_field(SortField {
3198                source_id: 2,
3199                transform: Transform::Identity,
3200                direction: SortDirection::Ascending,
3201                null_order: NullOrder::First,
3202            })
3203            .with_sort_field(SortField {
3204                source_id: 3,
3205                transform: Transform::Bucket(4),
3206                direction: SortDirection::Descending,
3207                null_order: NullOrder::Last,
3208            })
3209            .build_unbound()
3210            .unwrap();
3211
3212        let default_partition_type = partition_spec.partition_type(&schema).unwrap();
3213        let expected = TableMetadata {
3214            format_version: FormatVersion::V2,
3215            table_uuid: Uuid::parse_str("9c12d441-03fe-4693-9a96-a0705ddf69c1").unwrap(),
3216            location: "s3://bucket/test/location".to_string(),
3217            last_updated_ms: 1602638573590,
3218            last_column_id: 3,
3219            schemas: HashMap::from_iter(vec![(0, Arc::new(schema))]),
3220            current_schema_id: 0,
3221            partition_specs: HashMap::from_iter(vec![(0, partition_spec.clone().into())]),
3222            default_partition_type,
3223            default_spec: Arc::new(partition_spec),
3224            last_partition_id: 1000,
3225            default_sort_order_id: 3,
3226            sort_orders: HashMap::from_iter(vec![(3, sort_order.into())]),
3227            snapshots: HashMap::default(),
3228            current_snapshot_id: None,
3229            last_sequence_number: 34,
3230            properties: HashMap::new(),
3231            snapshot_log: vec![],
3232            metadata_log: Vec::new(),
3233            refs: HashMap::new(),
3234            statistics: HashMap::new(),
3235            partition_statistics: HashMap::new(),
3236            encryption_keys: HashMap::new(),
3237            next_row_id: INITIAL_ROW_ID,
3238        };
3239
3240        check_table_metadata_serde(&metadata, expected);
3241    }
3242
3243    #[test]
3244    fn test_table_metadata_v1_file_valid() {
3245        let metadata =
3246            fs::read_to_string("testdata/table_metadata/TableMetadataV1Valid.json").unwrap();
3247
3248        let schema = Schema::builder()
3249            .with_schema_id(0)
3250            .with_fields(vec![
3251                Arc::new(NestedField::required(
3252                    1,
3253                    "x",
3254                    Type::Primitive(PrimitiveType::Long),
3255                )),
3256                Arc::new(
3257                    NestedField::required(2, "y", Type::Primitive(PrimitiveType::Long))
3258                        .with_doc("comment"),
3259                ),
3260                Arc::new(NestedField::required(
3261                    3,
3262                    "z",
3263                    Type::Primitive(PrimitiveType::Long),
3264                )),
3265            ])
3266            .build()
3267            .unwrap();
3268
3269        let partition_spec = PartitionSpec::builder(schema.clone())
3270            .with_spec_id(0)
3271            .add_unbound_field(
3272                UnboundPartitionField::builder()
3273                    .source_ids(vec![1])
3274                    .field_id(1000)
3275                    .name("x".to_string())
3276                    .transform(Transform::Identity)
3277                    .build()
3278                    .unwrap(),
3279            )
3280            .unwrap()
3281            .build()
3282            .unwrap();
3283
3284        let default_partition_type = partition_spec.partition_type(&schema).unwrap();
3285        let expected = TableMetadata {
3286            format_version: FormatVersion::V1,
3287            table_uuid: Uuid::parse_str("d20125c8-7284-442c-9aea-15fee620737c").unwrap(),
3288            location: "s3://bucket/test/location".to_string(),
3289            last_updated_ms: 1602638573874,
3290            last_column_id: 3,
3291            schemas: HashMap::from_iter(vec![(0, Arc::new(schema))]),
3292            current_schema_id: 0,
3293            partition_specs: HashMap::from_iter(vec![(0, partition_spec.clone().into())]),
3294            default_spec: Arc::new(partition_spec),
3295            default_partition_type,
3296            last_partition_id: 0,
3297            default_sort_order_id: 0,
3298            // Sort order is added during deserialization for V2 compatibility
3299            sort_orders: HashMap::from_iter(vec![(0, SortOrder::unsorted_order().into())]),
3300            snapshots: HashMap::new(),
3301            current_snapshot_id: None,
3302            last_sequence_number: 0,
3303            properties: HashMap::new(),
3304            snapshot_log: vec![],
3305            metadata_log: Vec::new(),
3306            refs: HashMap::new(),
3307            statistics: HashMap::new(),
3308            partition_statistics: HashMap::new(),
3309            encryption_keys: HashMap::new(),
3310            next_row_id: INITIAL_ROW_ID,
3311        };
3312
3313        check_table_metadata_serde(&metadata, expected);
3314    }
3315
3316    #[test]
3317    fn test_table_metadata_v1_legacy_partition_spec_with_void_fields() {
3318        let mut metadata: serde_json::Value = serde_json::from_str(
3319            &fs::read_to_string("testdata/table_metadata/TableMetadataV1Valid.json").unwrap(),
3320        )
3321        .unwrap();
3322        // What a format version 1 spec keeps after identity(x) was removed twice.
3323        metadata["partition-spec"] = serde_json::json!([
3324            {"name": "x", "transform": "void", "source-id": 1, "field-id": 1000},
3325            {"name": "x_null", "transform": "void", "source-id": 1, "field-id": 1001},
3326        ]);
3327
3328        let metadata: TableMetadata = serde_json::from_value(metadata).unwrap();
3329
3330        let fields = metadata.default_partition_spec().fields();
3331        assert_eq!(
3332            fields
3333                .iter()
3334                .map(|f| (f.field_id, f.name.as_str(), f.transform))
3335                .collect::<Vec<_>>(),
3336            vec![
3337                (1000, "x", Transform::Void),
3338                (1001, "x_null", Transform::Void)
3339            ]
3340        );
3341    }
3342
3343    #[test]
3344    fn test_empty_snapshot_id_is_normalized_to_none() {
3345        let metadata =
3346            fs::read_to_string("testdata/table_metadata/TableMetadataV1Valid.json").unwrap();
3347        let deserialized: TableMetadata = serde_json::from_str(&metadata).unwrap();
3348        assert_eq!(
3349            deserialized.current_snapshot_id(),
3350            None,
3351            "current_snapshot_id of -1 should be deserialized as None"
3352        );
3353    }
3354
3355    #[test]
3356    fn test_table_metadata_v1_compat() {
3357        let metadata =
3358            fs::read_to_string("testdata/table_metadata/TableMetadataV1Compat.json").unwrap();
3359
3360        // Deserialize the JSON to verify it works
3361        let desered_type: TableMetadata = serde_json::from_str(&metadata)
3362            .expect("Failed to deserialize TableMetadataV1Compat.json");
3363
3364        // Verify some key fields match
3365        assert_eq!(desered_type.format_version(), FormatVersion::V1);
3366        assert_eq!(
3367            desered_type.uuid(),
3368            Uuid::parse_str("3276010d-7b1d-488c-98d8-9025fc4fde6b").unwrap()
3369        );
3370        assert_eq!(
3371            desered_type.location(),
3372            "s3://bucket/warehouse/iceberg/glue.db/table_name"
3373        );
3374        assert_eq!(desered_type.last_updated_ms(), 1727773114005);
3375        assert_eq!(desered_type.current_schema_id(), 0);
3376    }
3377
3378    #[test]
3379    fn test_table_metadata_v1_schemas_without_current_id() {
3380        let metadata = fs::read_to_string(
3381            "testdata/table_metadata/TableMetadataV1SchemasWithoutCurrentId.json",
3382        )
3383        .unwrap();
3384
3385        // Deserialize the JSON - this should succeed by using the 'schema' field instead of 'schemas'
3386        let desered_type: TableMetadata = serde_json::from_str(&metadata)
3387            .expect("Failed to deserialize TableMetadataV1SchemasWithoutCurrentId.json");
3388
3389        // Verify it used the 'schema' field
3390        assert_eq!(desered_type.format_version(), FormatVersion::V1);
3391        assert_eq!(
3392            desered_type.uuid(),
3393            Uuid::parse_str("d20125c8-7284-442c-9aea-15fee620737c").unwrap()
3394        );
3395
3396        // Get the schema and verify it has the expected fields
3397        let schema = desered_type.current_schema();
3398        assert_eq!(schema.as_struct().fields().len(), 3);
3399        assert_eq!(schema.as_struct().fields()[0].name, "x");
3400        assert_eq!(schema.as_struct().fields()[1].name, "y");
3401        assert_eq!(schema.as_struct().fields()[2].name, "z");
3402    }
3403
3404    #[test]
3405    fn test_table_metadata_v1_no_valid_schema() {
3406        let metadata =
3407            fs::read_to_string("testdata/table_metadata/TableMetadataV1NoValidSchema.json")
3408                .unwrap();
3409
3410        // Deserialize the JSON - this should fail because neither schemas + current_schema_id nor schema is valid
3411        let desered: Result<TableMetadata, serde_json::Error> = serde_json::from_str(&metadata);
3412
3413        assert!(desered.is_err());
3414        let error_message = desered.unwrap_err().to_string();
3415        assert!(
3416            error_message.contains("No valid schema configuration found"),
3417            "Expected error about no valid schema configuration, got: {error_message}"
3418        );
3419    }
3420
3421    #[test]
3422    fn test_table_metadata_v1_partition_specs_without_default_id() {
3423        let metadata = fs::read_to_string(
3424            "testdata/table_metadata/TableMetadataV1PartitionSpecsWithoutDefaultId.json",
3425        )
3426        .unwrap();
3427
3428        // Deserialize the JSON - this should succeed by inferring default_spec_id as the max spec ID
3429        let desered_type: TableMetadata = serde_json::from_str(&metadata)
3430            .expect("Failed to deserialize TableMetadataV1PartitionSpecsWithoutDefaultId.json");
3431
3432        // Verify basic metadata
3433        assert_eq!(desered_type.format_version(), FormatVersion::V1);
3434        assert_eq!(
3435            desered_type.uuid(),
3436            Uuid::parse_str("d20125c8-7284-442c-9aea-15fee620737c").unwrap()
3437        );
3438
3439        // Verify partition specs
3440        assert_eq!(desered_type.default_partition_spec_id(), 2); // Should pick the largest spec ID (2)
3441        assert_eq!(desered_type.partition_specs.len(), 2);
3442
3443        // Verify the default spec has the expected fields
3444        let default_spec = &desered_type.default_spec;
3445        assert_eq!(default_spec.spec_id(), 2);
3446        assert_eq!(default_spec.fields().len(), 1);
3447        assert_eq!(default_spec.fields()[0].name, "y");
3448        assert_eq!(default_spec.fields()[0].transform, Transform::Identity);
3449        assert_eq!(default_spec.fields()[0].source_id, 2);
3450    }
3451
3452    #[test]
3453    fn test_table_metadata_v2_schema_not_found() {
3454        let metadata =
3455            fs::read_to_string("testdata/table_metadata/TableMetadataV2CurrentSchemaNotFound.json")
3456                .unwrap();
3457
3458        let desered: Result<TableMetadata, serde_json::Error> = serde_json::from_str(&metadata);
3459
3460        assert_eq!(
3461            desered.unwrap_err().to_string(),
3462            "DataInvalid => No schema exists with the current schema id 2."
3463        )
3464    }
3465
3466    #[test]
3467    fn test_table_metadata_v2_missing_sort_order() {
3468        let metadata =
3469            fs::read_to_string("testdata/table_metadata/TableMetadataV2MissingSortOrder.json")
3470                .unwrap();
3471
3472        let desered: Result<TableMetadata, serde_json::Error> = serde_json::from_str(&metadata);
3473
3474        assert_eq!(
3475            desered.unwrap_err().to_string(),
3476            "data did not match any variant of untagged enum TableMetadataEnum"
3477        )
3478    }
3479
3480    #[test]
3481    fn test_table_metadata_v2_missing_partition_specs() {
3482        let metadata =
3483            fs::read_to_string("testdata/table_metadata/TableMetadataV2MissingPartitionSpecs.json")
3484                .unwrap();
3485
3486        let desered: Result<TableMetadata, serde_json::Error> = serde_json::from_str(&metadata);
3487
3488        assert_eq!(
3489            desered.unwrap_err().to_string(),
3490            "data did not match any variant of untagged enum TableMetadataEnum"
3491        )
3492    }
3493
3494    #[test]
3495    fn test_table_metadata_v2_missing_last_partition_id() {
3496        let metadata = fs::read_to_string(
3497            "testdata/table_metadata/TableMetadataV2MissingLastPartitionId.json",
3498        )
3499        .unwrap();
3500
3501        let desered: Result<TableMetadata, serde_json::Error> = serde_json::from_str(&metadata);
3502
3503        assert_eq!(
3504            desered.unwrap_err().to_string(),
3505            "data did not match any variant of untagged enum TableMetadataEnum"
3506        )
3507    }
3508
3509    #[test]
3510    fn test_table_metadata_v2_missing_schemas() {
3511        let metadata =
3512            fs::read_to_string("testdata/table_metadata/TableMetadataV2MissingSchemas.json")
3513                .unwrap();
3514
3515        let desered: Result<TableMetadata, serde_json::Error> = serde_json::from_str(&metadata);
3516
3517        assert_eq!(
3518            desered.unwrap_err().to_string(),
3519            "data did not match any variant of untagged enum TableMetadataEnum"
3520        )
3521    }
3522
3523    #[test]
3524    fn test_table_metadata_v2_unsupported_version() {
3525        let metadata =
3526            fs::read_to_string("testdata/table_metadata/TableMetadataUnsupportedVersion.json")
3527                .unwrap();
3528
3529        let desered: Result<TableMetadata, serde_json::Error> = serde_json::from_str(&metadata);
3530
3531        assert_eq!(
3532            desered.unwrap_err().to_string(),
3533            "data did not match any variant of untagged enum TableMetadataEnum"
3534        )
3535    }
3536
3537    #[test]
3538    fn test_order_of_format_version() {
3539        assert!(FormatVersion::V1 < FormatVersion::V2);
3540        assert_eq!(FormatVersion::V1, FormatVersion::V1);
3541        assert_eq!(FormatVersion::V2, FormatVersion::V2);
3542    }
3543
3544    #[test]
3545    fn test_deserialize_default_partition_spec_with_dropped_source() {
3546        for version in [1, 2, 3] {
3547            for transform in ["identity", "truncate[4]", "bucket[16]"] {
3548                let mut metadata = serde_json::json!({
3549                    "format-version": version,
3550                    "table-uuid": "9c12d441-03fe-4693-9a96-a0705ddf69c1",
3551                    "location": "s3://bucket/table",
3552                    "last-sequence-number": 0,
3553                    "last-updated-ms": 1602638573590_i64,
3554                    "last-column-id": 2,
3555                    "current-schema-id": 1,
3556                    "schemas": [
3557                        {
3558                            "schema-id": 0,
3559                            "type": "struct",
3560                            "fields": [
3561                                {"id": 1, "name": "x", "required": false, "type": "long"},
3562                                {"id": 2, "name": "y", "required": false, "type": "long"}
3563                            ]
3564                        },
3565                        {
3566                            "schema-id": 1,
3567                            "type": "struct",
3568                            "fields": [
3569                                {"id": 2, "name": "y", "required": false, "type": "long"}
3570                            ]
3571                        }
3572                    ],
3573                    "default-spec-id": 0,
3574                    "partition-specs": [{"spec-id": 0, "fields": [{
3575                        "source-id": 1, "field-id": 1000, "name": "x_partition",
3576                        "transform": transform
3577                    }]}],
3578                    "last-partition-id": 1000,
3579                    "default-sort-order-id": 0,
3580                    "sort-orders": [{"order-id": 0, "fields": []}],
3581                    "next-row-id": 0
3582                });
3583
3584                // Retaining the source in history does not make it writable using
3585                // the current schema and default spec.
3586                let err = serde_json::from_value::<TableMetadata>(metadata.clone()).unwrap_err();
3587                assert!(err.to_string().contains(
3588                    "Default partition spec 0 references missing source field 1 in current schema 1"
3589                ));
3590
3591                // The same spec is allowed once it becomes historical.
3592                metadata["partition-specs"]
3593                    .as_array_mut()
3594                    .unwrap()
3595                    .push(serde_json::json!({"spec-id": 1, "fields": []}));
3596                metadata["default-spec-id"] = serde_json::json!(1);
3597                serde_json::from_value::<TableMetadata>(metadata.clone()).unwrap();
3598
3599                // A voided field does not require its dropped source, even in the default spec.
3600                metadata["default-spec-id"] = serde_json::json!(0);
3601                metadata["partition-specs"][0]["fields"][0]["transform"] =
3602                    serde_json::json!("void");
3603                serde_json::from_value::<TableMetadata>(metadata).unwrap();
3604            }
3605        }
3606    }
3607
3608    #[test]
3609    fn test_default_partition_spec() {
3610        let default_spec_id = 1234;
3611        let mut table_meta_data = get_test_table_metadata("TableMetadataV2Valid.json");
3612        let partition_spec = PartitionSpec::unpartition_spec();
3613        table_meta_data.default_spec = partition_spec.clone().into();
3614        table_meta_data
3615            .partition_specs
3616            .insert(default_spec_id, Arc::new(partition_spec));
3617
3618        assert_eq!(
3619            (*table_meta_data.default_partition_spec().clone()).clone(),
3620            (*table_meta_data
3621                .partition_spec_by_id(default_spec_id)
3622                .unwrap()
3623                .clone())
3624            .clone()
3625        );
3626    }
3627    #[test]
3628    fn test_default_sort_order() {
3629        let default_sort_order_id = 1234;
3630        let mut table_meta_data = get_test_table_metadata("TableMetadataV2Valid.json");
3631        table_meta_data.default_sort_order_id = default_sort_order_id;
3632        table_meta_data
3633            .sort_orders
3634            .insert(default_sort_order_id, Arc::new(SortOrder::default()));
3635
3636        assert_eq!(
3637            table_meta_data.default_sort_order(),
3638            table_meta_data
3639                .sort_orders
3640                .get(&default_sort_order_id)
3641                .unwrap()
3642        )
3643    }
3644
3645    #[test]
3646    fn test_table_metadata_builder_from_table_creation() {
3647        let table_creation = TableCreation::builder()
3648            .location("s3://db/table".to_string())
3649            .name("table".to_string())
3650            .properties(HashMap::new())
3651            .schema(Schema::builder().build().unwrap())
3652            .build();
3653        let table_metadata = TableMetadataBuilder::from_table_creation(table_creation)
3654            .unwrap()
3655            .build()
3656            .unwrap()
3657            .metadata;
3658        assert_eq!(table_metadata.location, "s3://db/table");
3659        assert_eq!(table_metadata.schemas.len(), 1);
3660        assert_eq!(
3661            table_metadata
3662                .schemas
3663                .get(&0)
3664                .unwrap()
3665                .as_struct()
3666                .fields()
3667                .len(),
3668            0
3669        );
3670        assert_eq!(table_metadata.properties.len(), 0);
3671        assert_eq!(
3672            table_metadata.partition_specs,
3673            HashMap::from([(
3674                0,
3675                Arc::new(
3676                    PartitionSpec::builder(table_metadata.schemas.get(&0).unwrap().clone())
3677                        .with_spec_id(0)
3678                        .build()
3679                        .unwrap()
3680                )
3681            )])
3682        );
3683        assert_eq!(
3684            table_metadata.sort_orders,
3685            HashMap::from([(
3686                0,
3687                Arc::new(SortOrder {
3688                    order_id: 0,
3689                    fields: vec![]
3690                })
3691            )])
3692        );
3693    }
3694
3695    #[tokio::test]
3696    async fn test_table_metadata_read_write() {
3697        // Create a temporary directory for our test
3698        let temp_dir = TempDir::new().unwrap();
3699        let temp_path = temp_dir.path().to_str().unwrap();
3700
3701        // Create a FileIO instance
3702        let file_io = FileIO::new_with_fs();
3703
3704        // Use an existing test metadata from the test files
3705        let original_metadata: TableMetadata =
3706            get_test_table_metadata_at("TableMetadataV2Valid.json", temp_path);
3707
3708        // Define the metadata location
3709        let metadata_location =
3710            MetadataLocation::try_new_with_metadata(&original_metadata).unwrap();
3711        let metadata_location_str = metadata_location.to_string();
3712
3713        // Write the metadata
3714        original_metadata
3715            .write_to(&file_io, &metadata_location)
3716            .await
3717            .unwrap();
3718
3719        // Verify the file exists
3720        assert!(fs::metadata(&metadata_location_str).is_ok());
3721
3722        // Read the metadata back
3723        let read_metadata = TableMetadata::read_from(&file_io, &metadata_location_str)
3724            .await
3725            .unwrap();
3726
3727        // Verify the metadata matches
3728        assert_eq!(read_metadata, original_metadata);
3729    }
3730
3731    #[tokio::test]
3732    async fn test_table_metadata_read_compressed() {
3733        let temp_dir = TempDir::new().unwrap();
3734        let metadata_location = temp_dir.path().join("v1.gz.metadata.json");
3735
3736        let original_metadata: TableMetadata = get_test_table_metadata("TableMetadataV2Valid.json");
3737        let json = serde_json::to_string(&original_metadata).unwrap();
3738
3739        let compressed = CompressionCodec::gzip_default()
3740            .compress(json.into_bytes())
3741            .expect("failed to compress metadata");
3742        fs::write(&metadata_location, &compressed).expect("failed to write metadata");
3743
3744        // Read the metadata back
3745        let file_io = FileIO::new_with_fs();
3746        let metadata_location = metadata_location.to_str().unwrap();
3747        let read_metadata = TableMetadata::read_from(&file_io, metadata_location)
3748            .await
3749            .unwrap();
3750
3751        // Verify the metadata matches
3752        assert_eq!(read_metadata, original_metadata);
3753    }
3754
3755    #[tokio::test]
3756    async fn test_table_metadata_read_nonexistent_file() {
3757        // Create a FileIO instance
3758        let file_io = FileIO::new_with_fs();
3759
3760        // Try to read a non-existent file
3761        let result = TableMetadata::read_from(&file_io, "/nonexistent/path/metadata.json").await;
3762
3763        // Verify it returns an error
3764        assert!(result.is_err());
3765    }
3766
3767    #[tokio::test]
3768    async fn test_table_metadata_write_with_gzip_compression() {
3769        let temp_dir = TempDir::new().unwrap();
3770        let temp_path = temp_dir.path().to_str().unwrap();
3771        let file_io = FileIO::new_with_fs();
3772
3773        // Get a test metadata and add gzip compression property
3774        let original_metadata: TableMetadata =
3775            get_test_table_metadata_at("TableMetadataV2Valid.json", temp_path);
3776
3777        // Modify properties to enable gzip compression (using mixed case to test case-insensitive matching)
3778        let mut props = original_metadata.properties.clone();
3779        props.insert(
3780            TableProperties::PROPERTY_METADATA_COMPRESSION_CODEC.to_string(),
3781            "GziP".to_string(),
3782        );
3783        // Use builder to create new metadata with updated properties
3784        let compressed_metadata =
3785            TableMetadataBuilder::new_from_metadata(original_metadata.clone(), None)
3786                .assign_uuid(original_metadata.table_uuid)
3787                .set_properties(props.clone())
3788                .unwrap()
3789                .build()
3790                .unwrap()
3791                .metadata;
3792
3793        // Create MetadataLocation with compression codec from metadata
3794        let metadata_location =
3795            MetadataLocation::try_new_with_metadata(&compressed_metadata).unwrap();
3796        let metadata_location_str = metadata_location.to_string();
3797
3798        // Verify the location has the .gz extension
3799        assert!(metadata_location_str.contains(".gz.metadata.json"));
3800
3801        // Write the metadata with compression
3802        compressed_metadata
3803            .write_to(&file_io, &metadata_location)
3804            .await
3805            .unwrap();
3806
3807        // Verify the compressed file exists
3808        assert!(std::path::Path::new(&metadata_location_str).exists());
3809
3810        // Read the raw file and check it's gzip compressed
3811        let raw_content = fs::read(&metadata_location_str).unwrap();
3812        assert!(raw_content.len() > 2);
3813        assert_eq!(raw_content[0], 0x1F); // gzip magic number
3814        assert_eq!(raw_content[1], 0x8B); // gzip magic number
3815
3816        // Read the metadata back using the compressed location
3817        let read_metadata = TableMetadata::read_from(&file_io, &metadata_location_str)
3818            .await
3819            .unwrap();
3820
3821        // Verify the complete round-trip: read metadata should match what we wrote
3822        assert_eq!(read_metadata, compressed_metadata);
3823    }
3824
3825    #[test]
3826    fn test_name_exists_in_any_schema() {
3827        // Create multiple schemas with different fields
3828        let schema1 = Schema::builder()
3829            .with_schema_id(1)
3830            .with_fields(vec![
3831                NestedField::required(1, "field1", Type::Primitive(PrimitiveType::String)).into(),
3832                NestedField::required(2, "field2", Type::Primitive(PrimitiveType::Int)).into(),
3833            ])
3834            .build()
3835            .unwrap();
3836
3837        let schema2 = Schema::builder()
3838            .with_schema_id(2)
3839            .with_fields(vec![
3840                NestedField::required(1, "field1", Type::Primitive(PrimitiveType::String)).into(),
3841                NestedField::required(3, "field3", Type::Primitive(PrimitiveType::Long)).into(),
3842            ])
3843            .build()
3844            .unwrap();
3845
3846        let metadata = TableMetadataBuilder::new(
3847            schema1,
3848            PartitionSpec::unpartition_spec().into_unbound(),
3849            SortOrder::unsorted_order(),
3850            "s3://test/location".to_string(),
3851            FormatVersion::V2,
3852            HashMap::new(),
3853        )
3854        .unwrap()
3855        .add_current_schema(schema2)
3856        .unwrap()
3857        .build()
3858        .unwrap()
3859        .metadata;
3860
3861        assert!(metadata.name_exists_in_any_schema("field1")); // exists in both schemas
3862        assert!(metadata.name_exists_in_any_schema("field2")); // exists only in schema1 (historical)
3863        assert!(metadata.name_exists_in_any_schema("field3")); // exists only in schema2 (current)
3864
3865        assert!(!metadata.name_exists_in_any_schema("nonexistent_field"));
3866        assert!(!metadata.name_exists_in_any_schema("field4"));
3867        assert!(!metadata.name_exists_in_any_schema(""));
3868    }
3869
3870    #[test]
3871    fn test_name_exists_in_any_schema_empty_schemas() {
3872        let schema = Schema::builder().with_fields(vec![]).build().unwrap();
3873
3874        let metadata = TableMetadataBuilder::new(
3875            schema,
3876            PartitionSpec::unpartition_spec().into_unbound(),
3877            SortOrder::unsorted_order(),
3878            "s3://test/location".to_string(),
3879            FormatVersion::V2,
3880            HashMap::new(),
3881        )
3882        .unwrap()
3883        .build()
3884        .unwrap()
3885        .metadata;
3886
3887        assert!(!metadata.name_exists_in_any_schema("any_field"));
3888    }
3889
3890    #[test]
3891    fn test_helper_methods_multi_version_scenario() {
3892        // Test a realistic multi-version scenario
3893        let initial_schema = Schema::builder()
3894            .with_fields(vec![
3895                NestedField::required(1, "id", Type::Primitive(PrimitiveType::Long)).into(),
3896                NestedField::required(2, "name", Type::Primitive(PrimitiveType::String)).into(),
3897                NestedField::required(
3898                    3,
3899                    "deprecated_field",
3900                    Type::Primitive(PrimitiveType::String),
3901                )
3902                .into(),
3903            ])
3904            .build()
3905            .unwrap();
3906
3907        let metadata = TableMetadataBuilder::new(
3908            initial_schema,
3909            PartitionSpec::unpartition_spec().into_unbound(),
3910            SortOrder::unsorted_order(),
3911            "s3://test/location".to_string(),
3912            FormatVersion::V2,
3913            HashMap::new(),
3914        )
3915        .unwrap();
3916
3917        let evolved_schema = Schema::builder()
3918            .with_fields(vec![
3919                NestedField::required(1, "id", Type::Primitive(PrimitiveType::Long)).into(),
3920                NestedField::required(2, "name", Type::Primitive(PrimitiveType::String)).into(),
3921                NestedField::required(
3922                    3,
3923                    "deprecated_field",
3924                    Type::Primitive(PrimitiveType::String),
3925                )
3926                .into(),
3927                NestedField::required(4, "new_field", Type::Primitive(PrimitiveType::Double))
3928                    .into(),
3929            ])
3930            .build()
3931            .unwrap();
3932
3933        // Then add a third schema that removes the deprecated field
3934        let _final_schema = Schema::builder()
3935            .with_fields(vec![
3936                NestedField::required(1, "id", Type::Primitive(PrimitiveType::Long)).into(),
3937                NestedField::required(2, "name", Type::Primitive(PrimitiveType::String)).into(),
3938                NestedField::required(4, "new_field", Type::Primitive(PrimitiveType::Double))
3939                    .into(),
3940                NestedField::required(5, "latest_field", Type::Primitive(PrimitiveType::Boolean))
3941                    .into(),
3942            ])
3943            .build()
3944            .unwrap();
3945
3946        let final_metadata = metadata
3947            .add_current_schema(evolved_schema)
3948            .unwrap()
3949            .build()
3950            .unwrap()
3951            .metadata;
3952
3953        assert!(final_metadata.name_exists_in_any_schema("id")); // exists in both schemas
3954        assert!(final_metadata.name_exists_in_any_schema("name")); // exists in both schemas
3955        assert!(final_metadata.name_exists_in_any_schema("deprecated_field")); // exists in both schemas
3956        assert!(final_metadata.name_exists_in_any_schema("new_field")); // only in current schema
3957        assert!(!final_metadata.name_exists_in_any_schema("never_existed"));
3958    }
3959
3960    #[test]
3961    fn test_invalid_sort_order_id_zero_with_fields() {
3962        let metadata = r#"
3963        {
3964            "format-version": 2,
3965            "table-uuid": "9c12d441-03fe-4693-9a96-a0705ddf69c1",
3966            "location": "s3://bucket/test/location",
3967            "last-sequence-number": 111,
3968            "last-updated-ms": 1600000000000,
3969            "last-column-id": 3,
3970            "current-schema-id": 1,
3971            "schemas": [
3972                {
3973                    "type": "struct",
3974                    "schema-id": 1,
3975                    "fields": [
3976                        {"id": 1, "name": "x", "required": true, "type": "long"},
3977                        {"id": 2, "name": "y", "required": true, "type": "long"}
3978                    ]
3979                }
3980            ],
3981            "default-spec-id": 0,
3982            "partition-specs": [{"spec-id": 0, "fields": []}],
3983            "last-partition-id": 999,
3984            "default-sort-order-id": 0,
3985            "sort-orders": [
3986                {
3987                    "order-id": 0,
3988                    "fields": [
3989                        {
3990                            "transform": "identity",
3991                            "source-id": 1,
3992                            "direction": "asc",
3993                            "null-order": "nulls-first"
3994                        }
3995                    ]
3996                }
3997            ],
3998            "properties": {},
3999            "current-snapshot-id": -1,
4000            "snapshots": []
4001        }
4002        "#;
4003
4004        let result: Result<TableMetadata, serde_json::Error> = serde_json::from_str(metadata);
4005
4006        // Should fail because sort order ID 0 is reserved for unsorted order and cannot have fields
4007        assert!(
4008            result.is_err(),
4009            "Parsing should fail for sort order ID 0 with fields"
4010        );
4011    }
4012
4013    #[test]
4014    fn test_table_properties_with_defaults() {
4015        use crate::spec::TableProperties;
4016
4017        let schema = Schema::builder()
4018            .with_fields(vec![
4019                NestedField::required(1, "id", Type::Primitive(PrimitiveType::Long)).into(),
4020            ])
4021            .build()
4022            .unwrap();
4023
4024        let metadata = TableMetadataBuilder::new(
4025            schema,
4026            PartitionSpec::unpartition_spec().into_unbound(),
4027            SortOrder::unsorted_order(),
4028            "s3://test/location".to_string(),
4029            FormatVersion::V2,
4030            HashMap::new(),
4031        )
4032        .unwrap()
4033        .build()
4034        .unwrap()
4035        .metadata;
4036
4037        let props = metadata.table_properties();
4038
4039        assert_eq!(
4040            props.commit_num_retries().unwrap(),
4041            TableProperties::PROPERTY_COMMIT_NUM_RETRIES_DEFAULT
4042        );
4043        assert_eq!(
4044            props.write_target_file_size_bytes().unwrap(),
4045            TableProperties::PROPERTY_WRITE_TARGET_FILE_SIZE_BYTES_DEFAULT
4046        );
4047    }
4048
4049    #[test]
4050    fn test_table_properties_with_custom_values() {
4051        use crate::spec::TableProperties;
4052
4053        let schema = Schema::builder()
4054            .with_fields(vec![
4055                NestedField::required(1, "id", Type::Primitive(PrimitiveType::Long)).into(),
4056            ])
4057            .build()
4058            .unwrap();
4059
4060        let properties = HashMap::from([
4061            (
4062                TableProperties::PROPERTY_COMMIT_NUM_RETRIES.to_string(),
4063                "10".to_string(),
4064            ),
4065            (
4066                TableProperties::PROPERTY_WRITE_TARGET_FILE_SIZE_BYTES.to_string(),
4067                "1024".to_string(),
4068            ),
4069        ]);
4070
4071        let metadata = TableMetadataBuilder::new(
4072            schema,
4073            PartitionSpec::unpartition_spec().into_unbound(),
4074            SortOrder::unsorted_order(),
4075            "s3://test/location".to_string(),
4076            FormatVersion::V2,
4077            properties,
4078        )
4079        .unwrap()
4080        .build()
4081        .unwrap()
4082        .metadata;
4083
4084        let props = metadata.table_properties();
4085
4086        assert_eq!(props.commit_num_retries().unwrap(), 10);
4087        assert_eq!(props.write_target_file_size_bytes().unwrap(), 1024);
4088    }
4089
4090    #[test]
4091    fn test_deserialize_metadata_defers_invalid_table_property_errors() {
4092        let invalid_retries = "not_a_number";
4093        let invalid_codec = "unknown";
4094        let target_file_size = "1024";
4095
4096        for file_name in [
4097            "TableMetadataV1Valid.json",
4098            "TableMetadataV2ValidMinimal.json",
4099            "TableMetadataV3ValidMinimal.json",
4100        ] {
4101            let path = format!("testdata/table_metadata/{file_name}");
4102            let mut json: serde_json::Value =
4103                serde_json::from_str(&fs::read_to_string(path).unwrap()).unwrap();
4104            json["properties"] = serde_json::json!({
4105                (TableProperties::PROPERTY_COMMIT_NUM_RETRIES): invalid_retries,
4106                (TableProperties::PROPERTY_METADATA_COMPRESSION_CODEC): invalid_codec,
4107                (TableProperties::PROPERTY_WRITE_TARGET_FILE_SIZE_BYTES): target_file_size,
4108            });
4109
4110            let metadata: TableMetadata = serde_json::from_value(json).unwrap();
4111            assert_eq!(
4112                metadata
4113                    .properties()
4114                    .get(TableProperties::PROPERTY_COMMIT_NUM_RETRIES)
4115                    .map(String::as_str),
4116                Some(invalid_retries)
4117            );
4118
4119            let table_properties = metadata.table_properties();
4120            let error = table_properties.commit_num_retries().unwrap_err();
4121            assert!(
4122                error
4123                    .message()
4124                    .contains(TableProperties::PROPERTY_COMMIT_NUM_RETRIES)
4125            );
4126            assert_eq!(
4127                table_properties.write_target_file_size_bytes().unwrap(),
4128                1024
4129            );
4130            let error = table_properties.metadata_compression_codec().unwrap_err();
4131            assert!(
4132                format!("{error}").contains(TableProperties::PROPERTY_METADATA_COMPRESSION_CODEC)
4133            );
4134
4135            let serialized = serde_json::to_value(metadata).unwrap();
4136            assert_eq!(
4137                serialized["properties"][TableProperties::PROPERTY_COMMIT_NUM_RETRIES],
4138                invalid_retries
4139            );
4140            assert_eq!(
4141                serialized["properties"][TableProperties::PROPERTY_METADATA_COMPRESSION_CODEC],
4142                invalid_codec
4143            );
4144        }
4145    }
4146
4147    #[test]
4148    fn test_table_properties_with_invalid_value() {
4149        let schema = Schema::builder()
4150            .with_fields(vec![
4151                NestedField::required(1, "id", Type::Primitive(PrimitiveType::Long)).into(),
4152            ])
4153            .build()
4154            .unwrap();
4155
4156        let properties = HashMap::from([
4157            (
4158                TableProperties::PROPERTY_COMMIT_NUM_RETRIES.to_string(),
4159                "not_a_number".to_string(),
4160            ),
4161            (
4162                TableProperties::PROPERTY_WRITE_TARGET_FILE_SIZE_BYTES.to_string(),
4163                "1024".to_string(),
4164            ),
4165        ]);
4166
4167        let metadata = TableMetadataBuilder::new(
4168            schema,
4169            PartitionSpec::unpartition_spec().into_unbound(),
4170            SortOrder::unsorted_order(),
4171            "s3://test/location".to_string(),
4172            FormatVersion::V2,
4173            properties,
4174        )
4175        .unwrap()
4176        .build()
4177        .unwrap()
4178        .metadata;
4179
4180        let table_properties = metadata.table_properties();
4181        let err = table_properties.commit_num_retries().unwrap_err();
4182        assert_eq!(err.kind(), ErrorKind::DataInvalid);
4183        assert!(
4184            err.message()
4185                .contains(TableProperties::PROPERTY_COMMIT_NUM_RETRIES)
4186        );
4187        assert_eq!(
4188            table_properties.write_target_file_size_bytes().unwrap(),
4189            1024
4190        );
4191    }
4192
4193    #[test]
4194    fn test_v2_to_v3_upgrade_preserves_existing_snapshots_without_row_lineage() {
4195        // Create a v2 table metadata
4196        let schema = Schema::builder()
4197            .with_fields(vec![
4198                NestedField::required(1, "id", Type::Primitive(PrimitiveType::Long)).into(),
4199            ])
4200            .build()
4201            .unwrap();
4202
4203        let v2_metadata = TableMetadataBuilder::new(
4204            schema,
4205            PartitionSpec::unpartition_spec().into_unbound(),
4206            SortOrder::unsorted_order(),
4207            "s3://bucket/test/location".to_string(),
4208            FormatVersion::V2,
4209            HashMap::new(),
4210        )
4211        .unwrap()
4212        .build()
4213        .unwrap()
4214        .metadata;
4215
4216        // Add a v2 snapshot
4217        let snapshot = Snapshot::builder()
4218            .with_snapshot_id(1)
4219            .with_timestamp_ms(v2_metadata.last_updated_ms + 1)
4220            .with_sequence_number(1)
4221            .with_schema_id(0)
4222            .with_manifest_list("s3://bucket/test/metadata/snap-1.avro")
4223            .with_summary(Summary {
4224                operation: Operation::Append,
4225                additional_properties: HashMap::from([(
4226                    "added-data-files".to_string(),
4227                    "1".to_string(),
4228                )]),
4229            })
4230            .build();
4231
4232        let v2_with_snapshot = v2_metadata
4233            .into_builder(Some("s3://bucket/test/metadata/v00001.json".to_string()))
4234            .add_snapshot(snapshot)
4235            .unwrap()
4236            .set_ref("main", SnapshotReference {
4237                snapshot_id: 1,
4238                retention: SnapshotRetention::Branch {
4239                    min_snapshots_to_keep: None,
4240                    max_snapshot_age_ms: None,
4241                    max_ref_age_ms: None,
4242                },
4243            })
4244            .unwrap()
4245            .build()
4246            .unwrap()
4247            .metadata;
4248
4249        // Verify v2 serialization works fine
4250        let v2_json = serde_json::to_string(&v2_with_snapshot);
4251        assert!(v2_json.is_ok(), "v2 serialization should work");
4252
4253        // Upgrade to v3
4254        let v3_metadata = v2_with_snapshot
4255            .into_builder(Some("s3://bucket/test/metadata/v00002.json".to_string()))
4256            .upgrade_format_version(FormatVersion::V3)
4257            .unwrap()
4258            .build()
4259            .unwrap()
4260            .metadata;
4261
4262        assert_eq!(v3_metadata.format_version, FormatVersion::V3);
4263        assert_eq!(v3_metadata.next_row_id, INITIAL_ROW_ID);
4264        assert_eq!(v3_metadata.snapshots.len(), 1);
4265
4266        // Verify the snapshot has no row_range
4267        let snapshot = v3_metadata.snapshots.values().next().unwrap();
4268        assert!(
4269            snapshot.row_range().is_none(),
4270            "Snapshot should have no row_range after upgrade"
4271        );
4272
4273        // Try to serialize v3 metadata - this should now work
4274        let v3_json = serde_json::to_string(&v3_metadata);
4275        assert!(
4276            v3_json.is_ok(),
4277            "v3 serialization should work for upgraded tables"
4278        );
4279
4280        // Verify we can deserialize it back
4281        let deserialized: TableMetadata = serde_json::from_str(&v3_json.unwrap()).unwrap();
4282        assert_eq!(deserialized.format_version, FormatVersion::V3);
4283        assert_eq!(deserialized.snapshots.len(), 1);
4284
4285        // Verify the deserialized snapshot still has no row_range
4286        let deserialized_snapshot = deserialized.snapshots.values().next().unwrap();
4287        assert!(
4288            deserialized_snapshot.row_range().is_none(),
4289            "Deserialized snapshot should have no row_range"
4290        );
4291    }
4292
4293    #[test]
4294    fn test_v3_snapshot_with_row_lineage_serialization() {
4295        // Create a v3 table metadata
4296        let schema = Schema::builder()
4297            .with_fields(vec![
4298                NestedField::required(1, "id", Type::Primitive(PrimitiveType::Long)).into(),
4299            ])
4300            .build()
4301            .unwrap();
4302
4303        let v3_metadata = TableMetadataBuilder::new(
4304            schema,
4305            PartitionSpec::unpartition_spec().into_unbound(),
4306            SortOrder::unsorted_order(),
4307            "s3://bucket/test/location".to_string(),
4308            FormatVersion::V3,
4309            HashMap::new(),
4310        )
4311        .unwrap()
4312        .build()
4313        .unwrap()
4314        .metadata;
4315
4316        // Add a v3 snapshot with row lineage
4317        let snapshot = Snapshot::builder()
4318            .with_snapshot_id(1)
4319            .with_timestamp_ms(v3_metadata.last_updated_ms + 1)
4320            .with_sequence_number(1)
4321            .with_schema_id(0)
4322            .with_manifest_list("s3://bucket/test/metadata/snap-1.avro")
4323            .with_summary(Summary {
4324                operation: Operation::Append,
4325                additional_properties: HashMap::from([(
4326                    "added-data-files".to_string(),
4327                    "1".to_string(),
4328                )]),
4329            })
4330            .with_row_range(100, 50) // first_row_id=100, added_rows=50
4331            .build();
4332
4333        let v3_with_snapshot = v3_metadata
4334            .into_builder(Some("s3://bucket/test/metadata/v00001.json".to_string()))
4335            .add_snapshot(snapshot)
4336            .unwrap()
4337            .set_ref("main", SnapshotReference {
4338                snapshot_id: 1,
4339                retention: SnapshotRetention::Branch {
4340                    min_snapshots_to_keep: None,
4341                    max_snapshot_age_ms: None,
4342                    max_ref_age_ms: None,
4343                },
4344            })
4345            .unwrap()
4346            .build()
4347            .unwrap()
4348            .metadata;
4349
4350        // Verify the snapshot has row_range
4351        let snapshot = v3_with_snapshot.snapshots.values().next().unwrap();
4352        assert!(
4353            snapshot.row_range().is_some(),
4354            "Snapshot should have row_range"
4355        );
4356        let (first_row_id, added_rows) = snapshot.row_range().unwrap();
4357        assert_eq!(first_row_id, 100);
4358        assert_eq!(added_rows, 50);
4359
4360        // Serialize v3 metadata - this should work
4361        let v3_json = serde_json::to_string(&v3_with_snapshot);
4362        assert!(
4363            v3_json.is_ok(),
4364            "v3 serialization should work for snapshots with row lineage"
4365        );
4366
4367        // Verify we can deserialize it back
4368        let deserialized: TableMetadata = serde_json::from_str(&v3_json.unwrap()).unwrap();
4369        assert_eq!(deserialized.format_version, FormatVersion::V3);
4370        assert_eq!(deserialized.snapshots.len(), 1);
4371
4372        // Verify the deserialized snapshot has the correct row_range
4373        let deserialized_snapshot = deserialized.snapshots.values().next().unwrap();
4374        assert!(
4375            deserialized_snapshot.row_range().is_some(),
4376            "Deserialized snapshot should have row_range"
4377        );
4378        let (deserialized_first_row_id, deserialized_added_rows) =
4379            deserialized_snapshot.row_range().unwrap();
4380        assert_eq!(deserialized_first_row_id, 100);
4381        assert_eq!(deserialized_added_rows, 50);
4382    }
4383
4384    #[test]
4385    fn test_metadata_location_default() {
4386        // Verify metadata files go to `<location>/metadata` when `write.metadata.path` is not set
4387        let metadata = get_test_table_metadata("TableMetadataV2Valid.json");
4388        assert_eq!(metadata.location(), "s3://bucket/test/location");
4389        assert_eq!(
4390            metadata.metadata_location().unwrap(),
4391            "s3://bucket/test/location/metadata"
4392        );
4393    }
4394
4395    #[test]
4396    fn test_metadata_location_honors_write_metadata_path() {
4397        let metadata = get_test_table_metadata("TableMetadataV2Valid.json")
4398            .into_builder(None)
4399            .set_properties(HashMap::from([(
4400                TableProperties::PROPERTY_WRITE_METADATA_PATH.to_string(),
4401                "s3://other-bucket/custom-meta".to_string(),
4402            )]))
4403            .unwrap()
4404            .build()
4405            .unwrap()
4406            .metadata;
4407        assert_eq!(
4408            metadata.metadata_location().unwrap(),
4409            "s3://other-bucket/custom-meta"
4410        );
4411    }
4412
4413    #[test]
4414    fn test_metadata_location_trims_trailing_slash() {
4415        // A configured path with a trailing slash must not yield a doubled separator
4416        let metadata = get_test_table_metadata("TableMetadataV2Valid.json")
4417            .into_builder(None)
4418            .set_properties(HashMap::from([(
4419                TableProperties::PROPERTY_WRITE_METADATA_PATH.to_string(),
4420                "s3://other-bucket/custom-meta/".to_string(),
4421            )]))
4422            .unwrap()
4423            .build()
4424            .unwrap()
4425            .metadata;
4426        assert_eq!(
4427            metadata.metadata_location().unwrap(),
4428            "s3://other-bucket/custom-meta"
4429        );
4430    }
4431
4432    #[test]
4433    fn test_unified_partition_type_spans_all_specs() {
4434        let schema = Schema::builder()
4435            .with_fields(vec![
4436                NestedField::required(1, "x", Type::Primitive(PrimitiveType::Long)).into(),
4437                NestedField::required(2, "y", Type::Primitive(PrimitiveType::Long)).into(),
4438                NestedField::required(3, "z", Type::Primitive(PrimitiveType::Long)).into(),
4439            ])
4440            .build()
4441            .unwrap();
4442
4443        let metadata = TableMetadataBuilder::new(
4444            schema.clone(),
4445            UnboundPartitionSpec::builder()
4446                .with_spec_id(0)
4447                .add_partition_field(
4448                    UnboundPartitionField::builder()
4449                        .source_ids(vec![2])
4450                        .name("y")
4451                        .transform(Transform::Identity)
4452                        .build()
4453                        .unwrap(),
4454                )
4455                .unwrap()
4456                .build(),
4457            SortOrder::unsorted_order(),
4458            "s3://bucket/table".to_string(),
4459            FormatVersion::V2,
4460            HashMap::new(),
4461        )
4462        .unwrap()
4463        .build()
4464        .unwrap()
4465        .metadata
4466        .into_builder(None)
4467        .add_partition_spec(
4468            UnboundPartitionSpec::builder()
4469                .add_partition_field(
4470                    UnboundPartitionField::builder()
4471                        .source_ids(vec![3])
4472                        .name("z")
4473                        .transform(Transform::Identity)
4474                        .build()
4475                        .unwrap(),
4476                )
4477                .unwrap()
4478                .build(),
4479        )
4480        .unwrap()
4481        .build()
4482        .unwrap()
4483        .metadata;
4484
4485        // The default spec only knows about `y`, but `_partition` must expose both.
4486        assert_eq!(metadata.default_partition_type().fields().len(), 1);
4487
4488        let unified = metadata.unified_partition_type(&schema).unwrap();
4489        let names: Vec<&str> = unified
4490            .fields()
4491            .iter()
4492            .map(|field| field.name.as_str())
4493            .collect();
4494        assert_eq!(names, vec!["y", "z"]);
4495    }
4496}