Skip to main content

iceberg/catalog/
mod.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//! Catalog API for Apache Iceberg
19
20pub mod memory;
21mod metadata_location;
22mod session;
23pub(crate) mod utils;
24
25use std::collections::HashMap;
26use std::fmt::{Debug, Display};
27use std::future::Future;
28use std::mem::take;
29use std::ops::Deref;
30use std::str::FromStr;
31use std::sync::Arc;
32
33use _serde::{deserialize_snapshot, serialize_snapshot};
34use async_trait::async_trait;
35pub use memory::MemoryCatalog;
36pub use metadata_location::*;
37#[cfg(test)]
38use mockall::automock;
39use serde_derive::{Deserialize, Serialize};
40pub use session::*;
41use typed_builder::TypedBuilder;
42use uuid::Uuid;
43
44use crate::encryption::kms::KmsClientFactory;
45use crate::io::StorageFactory;
46use crate::runtime::Runtime;
47use crate::spec::{
48    EncryptedKey, FormatVersion, PartitionStatisticsFile, Schema, SchemaId, Snapshot,
49    SnapshotReference, SortOrder, StatisticsFile, TableMetadata, TableMetadataBuilder,
50    UnboundPartitionSpec, ViewFormatVersion, ViewRepresentations, ViewVersion,
51};
52use crate::table::Table;
53use crate::{Error, ErrorKind, Result};
54
55/// The catalog API for Iceberg Rust.
56#[async_trait]
57#[cfg_attr(test, automock)]
58pub trait Catalog: Debug + Sync + Send {
59    /// List namespaces inside the catalog.
60    async fn list_namespaces(&self, parent: Option<&NamespaceIdent>)
61    -> Result<Vec<NamespaceIdent>>;
62
63    /// Create a new namespace inside the catalog.
64    async fn create_namespace(
65        &self,
66        namespace: &NamespaceIdent,
67        properties: HashMap<String, String>,
68    ) -> Result<Namespace>;
69
70    /// Get a namespace information from the catalog.
71    async fn get_namespace(&self, namespace: &NamespaceIdent) -> Result<Namespace>;
72
73    /// Check if namespace exists in catalog.
74    async fn namespace_exists(&self, namespace: &NamespaceIdent) -> Result<bool>;
75
76    /// Update a namespace inside the catalog.
77    ///
78    /// # Behavior
79    ///
80    /// The properties must be the full set of namespace.
81    async fn update_namespace(
82        &self,
83        namespace: &NamespaceIdent,
84        properties: HashMap<String, String>,
85    ) -> Result<()>;
86
87    /// Drop a namespace from the catalog, or returns error if it doesn't exist.
88    async fn drop_namespace(&self, namespace: &NamespaceIdent) -> Result<()>;
89
90    /// List tables from namespace.
91    async fn list_tables(&self, namespace: &NamespaceIdent) -> Result<Vec<TableIdent>>;
92
93    /// Create a new table inside the namespace.
94    async fn create_table(
95        &self,
96        namespace: &NamespaceIdent,
97        creation: TableCreation,
98    ) -> Result<Table>;
99
100    /// Load table from the catalog.
101    async fn load_table(&self, table: &TableIdent) -> Result<Table>;
102
103    /// Drop a table from the catalog, or returns error if it doesn't exist.
104    async fn drop_table(&self, table: &TableIdent) -> Result<()>;
105
106    /// Drop a table from the catalog and delete the underlying table data.
107    ///
108    /// Implementations should load the table metadata, drop the table
109    /// from the catalog, then delete all associated data and metadata files.
110    /// The [`drop_table_data`](utils::drop_table_data) utility function can
111    /// be used for the file cleanup step.
112    async fn purge_table(&self, table: &TableIdent) -> Result<()>;
113
114    /// Check if a table exists in the catalog.
115    async fn table_exists(&self, table: &TableIdent) -> Result<bool>;
116
117    /// Rename a table in the catalog.
118    async fn rename_table(&self, src: &TableIdent, dest: &TableIdent) -> Result<()>;
119
120    /// Register an existing table to the catalog.
121    async fn register_table(&self, table: &TableIdent, metadata_location: String) -> Result<Table>;
122
123    /// Update a table to the catalog.
124    async fn update_table(&self, commit: TableCommit) -> Result<Table>;
125}
126
127/// Common interface for all catalog builders.
128pub trait CatalogBuilder: Default + Debug + Send + Sync {
129    /// The catalog type that this builder creates.
130    type C: Catalog;
131
132    /// Set a custom StorageFactory to use for storage operations.
133    ///
134    /// When a StorageFactory is provided, the catalog will use it to build FileIO
135    /// instances for all storage operations instead of using the default factory.
136    ///
137    /// # Arguments
138    ///
139    /// * `storage_factory` - The StorageFactory to use for creating storage instances
140    ///
141    /// # Example
142    ///
143    /// ```rust,ignore
144    /// use iceberg::CatalogBuilder;
145    /// use iceberg::io::StorageFactory;
146    /// use iceberg_storage_opendal::OpenDalStorageFactory;
147    /// use std::sync::Arc;
148    ///
149    /// let catalog = MyCatalogBuilder::default()
150    ///     .with_storage_factory(Arc::new(OpenDalStorageFactory::S3 {
151    ///         customized_credential_load: None,
152    ///     }))
153    ///     .load("my_catalog", props)
154    ///     .await?;
155    /// ```
156    fn with_storage_factory(self, storage_factory: Arc<dyn StorageFactory>) -> Self;
157
158    /// Set a [`KmsClientFactory`] to enable table encryption.
159    ///
160    /// When provided, the catalog calls the factory once during
161    /// [`load`](Self::load) with the catalog properties to create a shared
162    /// [`KeyManagementClient`](crate::encryption::KeyManagementClient).
163    /// That client is then passed to each table's `TableBuilder` so tables
164    /// with `encryption.key-id` set can construct an `EncryptionManager`.
165    ///
166    /// # Example
167    ///
168    /// ```rust,ignore
169    /// use iceberg::CatalogBuilder;
170    /// use iceberg::encryption::kms::KmsClientFactory;
171    /// use std::sync::Arc;
172    ///
173    /// let catalog = MyCatalogBuilder::default()
174    ///     .with_kms_client_factory(Arc::new(MyKmsClientFactory))
175    ///     .load("my_catalog", props)
176    ///     .await?;
177    /// ```
178    fn with_kms_client_factory(self, kms_client_factory: Arc<dyn KmsClientFactory>) -> Self;
179
180    /// Set a custom tokio Runtime to use for spawning async tasks.
181    ///
182    /// When a Runtime is provided, the catalog will propagate it to all tables
183    /// it creates. Tasks such as scan planning and delete file processing
184    /// will be spawned on this runtime.
185    fn with_runtime(self, runtime: Runtime) -> Self;
186
187    /// Create a new catalog instance.
188    fn load(
189        self,
190        name: impl Into<String>,
191        props: HashMap<String, String>,
192    ) -> impl Future<Output = Result<Self::C>> + Send;
193}
194
195/// NamespaceIdent represents the identifier of a namespace in the catalog.
196///
197/// The namespace identifier is a list of strings, where each string is a
198/// component of the namespace. It's the catalog implementer's responsibility to
199/// handle the namespace identifier correctly.
200#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
201pub struct NamespaceIdent(Vec<String>);
202
203impl NamespaceIdent {
204    /// Create a new namespace identifier with only one level.
205    pub fn new(name: String) -> Self {
206        Self(vec![name])
207    }
208
209    /// Create a multi-level namespace identifier from vector.
210    pub fn from_vec(names: Vec<String>) -> Result<Self> {
211        if names.is_empty() {
212            return Err(Error::new(
213                ErrorKind::DataInvalid,
214                "Namespace identifier can't be empty!",
215            ));
216        }
217        Ok(Self(names))
218    }
219
220    /// Try to create namespace identifier from an iterator of string.
221    pub fn from_strs(iter: impl IntoIterator<Item = impl ToString>) -> Result<Self> {
222        Self::from_vec(iter.into_iter().map(|s| s.to_string()).collect())
223    }
224
225    /// Returns a string for used in url.
226    pub fn to_url_string(&self) -> String {
227        self.as_ref().join("\u{001f}")
228    }
229
230    /// Returns inner strings.
231    pub fn inner(self) -> Vec<String> {
232        self.0
233    }
234
235    /// Get the parent of this namespace.
236    /// Returns None if this namespace only has a single element and thus has no parent.
237    pub fn parent(&self) -> Option<Self> {
238        self.0.split_last().and_then(|(_, parent)| {
239            if parent.is_empty() {
240                None
241            } else {
242                Some(Self(parent.to_vec()))
243            }
244        })
245    }
246}
247
248impl AsRef<Vec<String>> for NamespaceIdent {
249    fn as_ref(&self) -> &Vec<String> {
250        &self.0
251    }
252}
253
254impl Deref for NamespaceIdent {
255    type Target = [String];
256
257    fn deref(&self) -> &Self::Target {
258        &self.0
259    }
260}
261
262/// Namespace represents a namespace in the catalog.
263#[derive(Debug, Clone, PartialEq, Eq)]
264pub struct Namespace {
265    name: NamespaceIdent,
266    properties: HashMap<String, String>,
267}
268
269impl Namespace {
270    /// Create a new namespace.
271    pub fn new(name: NamespaceIdent) -> Self {
272        Self::with_properties(name, HashMap::default())
273    }
274
275    /// Create a new namespace with properties.
276    pub fn with_properties(name: NamespaceIdent, properties: HashMap<String, String>) -> Self {
277        Self { name, properties }
278    }
279
280    /// Get the name of the namespace.
281    pub fn name(&self) -> &NamespaceIdent {
282        &self.name
283    }
284
285    /// Get the properties of the namespace.
286    pub fn properties(&self) -> &HashMap<String, String> {
287        &self.properties
288    }
289}
290
291impl Display for NamespaceIdent {
292    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
293        write!(f, "{}", self.0.join("."))
294    }
295}
296
297/// TableIdent represents the identifier of a table in the catalog.
298#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
299pub struct TableIdent {
300    /// Namespace of the table.
301    pub namespace: NamespaceIdent,
302    /// Table name.
303    pub name: String,
304}
305
306impl TableIdent {
307    /// Create a new table identifier.
308    pub fn new(namespace: NamespaceIdent, name: String) -> Self {
309        Self { namespace, name }
310    }
311
312    /// Get the namespace of the table.
313    pub fn namespace(&self) -> &NamespaceIdent {
314        &self.namespace
315    }
316
317    /// Get the name of the table.
318    pub fn name(&self) -> &str {
319        &self.name
320    }
321
322    /// Try to create table identifier from an iterator of string.
323    pub fn from_strs(iter: impl IntoIterator<Item = impl ToString>) -> Result<Self> {
324        let mut vec: Vec<String> = iter.into_iter().map(|s| s.to_string()).collect();
325        let table_name = vec.pop().ok_or_else(|| {
326            Error::new(ErrorKind::DataInvalid, "Table identifier can't be empty!")
327        })?;
328        let namespace_ident = NamespaceIdent::from_vec(vec)?;
329
330        Ok(Self {
331            namespace: namespace_ident,
332            name: table_name,
333        })
334    }
335}
336
337impl Display for TableIdent {
338    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
339        write!(f, "{}.{}", self.namespace, self.name)
340    }
341}
342
343/// TableCreation represents the creation of a table in the catalog.
344#[derive(Debug, TypedBuilder)]
345pub struct TableCreation {
346    /// The name of the table.
347    pub name: String,
348    /// The location of the table.
349    #[builder(default, setter(strip_option(fallback = location_opt)))]
350    pub location: Option<String>,
351    /// The schema of the table.
352    pub schema: Schema,
353    /// The partition spec of the table, could be None.
354    #[builder(default, setter(strip_option(fallback = partition_spec_opt), into))]
355    pub partition_spec: Option<UnboundPartitionSpec>,
356    /// The sort order of the table.
357    #[builder(default, setter(strip_option(fallback = sort_order_opt)))]
358    pub sort_order: Option<SortOrder>,
359    /// The properties of the table.
360    #[builder(default, setter(transform = |props: impl IntoIterator<Item=(String, String)>| {
361        props.into_iter().collect()
362    }))]
363    pub properties: HashMap<String, String>,
364    /// Format version of the table. Defaults to V2.
365    #[builder(default = FormatVersion::V2)]
366    pub format_version: FormatVersion,
367}
368
369/// TableCommit represents the commit of a table in the catalog.
370///
371/// The builder is marked as private since it's dangerous and error-prone to construct
372/// [`TableCommit`] directly.
373/// Users are supposed to use [`crate::transaction::Transaction`] to update table.
374#[derive(Debug, TypedBuilder)]
375#[builder(build_method(vis = "pub(crate)"))]
376pub struct TableCommit {
377    /// The table ident.
378    ident: TableIdent,
379    /// The requirements of the table.
380    ///
381    /// Commit will fail if the requirements are not met.
382    requirements: Vec<TableRequirement>,
383    /// The updates of the table.
384    updates: Vec<TableUpdate>,
385}
386
387impl TableCommit {
388    /// Return the table identifier.
389    pub fn identifier(&self) -> &TableIdent {
390        &self.ident
391    }
392
393    /// Take all requirements.
394    pub fn take_requirements(&mut self) -> Vec<TableRequirement> {
395        take(&mut self.requirements)
396    }
397
398    /// Take all updates.
399    pub fn take_updates(&mut self) -> Vec<TableUpdate> {
400        take(&mut self.updates)
401    }
402
403    /// Applies this [`TableCommit`] to the given [`Table`] as part of a catalog update.
404    /// Typically used by [`Catalog::update_table`] to validate requirements and apply metadata updates.
405    ///
406    /// Returns a new [`Table`] with updated metadata,
407    /// or an error if validation or application fails.
408    pub fn apply(self, table: Table) -> Result<Table> {
409        // check requirements
410        for requirement in self.requirements {
411            requirement.check(Some(table.metadata()))?;
412        }
413
414        // get current metadata location
415        let current_metadata_location = table.metadata_location_result()?;
416
417        // apply updates to metadata builder
418        let mut metadata_builder = table
419            .metadata()
420            .clone()
421            .into_builder(Some(current_metadata_location.to_string()));
422        for update in self.updates {
423            metadata_builder = update.apply(metadata_builder)?;
424        }
425
426        // Build the new metadata
427        let new_metadata = metadata_builder.build()?.metadata;
428
429        let new_metadata_location = MetadataLocation::from_str(current_metadata_location)?
430            .with_next_version()
431            .try_with_new_metadata(&new_metadata)?
432            .to_string();
433
434        Ok(table
435            .with_metadata(Arc::new(new_metadata))
436            .with_metadata_location(new_metadata_location))
437    }
438}
439
440/// TableRequirement represents a requirement for a table in the catalog.
441#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
442#[serde(tag = "type")]
443pub enum TableRequirement {
444    /// The table must not already exist; used for create transactions
445    #[serde(rename = "assert-create")]
446    NotExist,
447    /// The table UUID must match the requirement.
448    #[serde(rename = "assert-table-uuid")]
449    UuidMatch {
450        /// Uuid of original table.
451        uuid: Uuid,
452    },
453    /// The table branch or tag identified by the requirement's `reference` must
454    /// reference the requirement's `snapshot-id`.
455    #[serde(rename = "assert-ref-snapshot-id")]
456    RefSnapshotIdMatch {
457        /// The reference of the table to assert.
458        r#ref: String,
459        /// The snapshot id of the table to assert.
460        /// If the id is `None`, the ref must not already exist.
461        #[serde(rename = "snapshot-id")]
462        snapshot_id: Option<i64>,
463    },
464    /// The table's last assigned column id must match the requirement.
465    #[serde(rename = "assert-last-assigned-field-id")]
466    LastAssignedFieldIdMatch {
467        /// The last assigned field id of the table to assert.
468        #[serde(rename = "last-assigned-field-id")]
469        last_assigned_field_id: i32,
470    },
471    /// The table's current schema id must match the requirement.
472    #[serde(rename = "assert-current-schema-id")]
473    CurrentSchemaIdMatch {
474        /// Current schema id of the table to assert.
475        #[serde(rename = "current-schema-id")]
476        current_schema_id: SchemaId,
477    },
478    /// The table's last assigned partition id must match the
479    /// requirement.
480    #[serde(rename = "assert-last-assigned-partition-id")]
481    LastAssignedPartitionIdMatch {
482        /// Last assigned partition id of the table to assert.
483        #[serde(rename = "last-assigned-partition-id")]
484        last_assigned_partition_id: i32,
485    },
486    /// The table's default spec id must match the requirement.
487    #[serde(rename = "assert-default-spec-id")]
488    DefaultSpecIdMatch {
489        /// Default spec id of the table to assert.
490        #[serde(rename = "default-spec-id")]
491        default_spec_id: i32,
492    },
493    /// The table's default sort order id must match the requirement.
494    #[serde(rename = "assert-default-sort-order-id")]
495    DefaultSortOrderIdMatch {
496        /// Default sort order id of the table to assert.
497        #[serde(rename = "default-sort-order-id")]
498        default_sort_order_id: i64,
499    },
500}
501
502/// TableUpdate represents an update to a table in the catalog.
503#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
504#[serde(tag = "action", rename_all = "kebab-case")]
505#[allow(clippy::large_enum_variant)]
506pub enum TableUpdate {
507    /// Upgrade table's format version
508    #[serde(rename_all = "kebab-case")]
509    UpgradeFormatVersion {
510        /// Target format upgrade to.
511        format_version: FormatVersion,
512    },
513    /// Assign a new UUID to the table
514    #[serde(rename_all = "kebab-case")]
515    AssignUuid {
516        /// The new UUID to assign.
517        uuid: Uuid,
518    },
519    /// Add a new schema to the table
520    #[serde(rename_all = "kebab-case")]
521    AddSchema {
522        /// The schema to add.
523        schema: Schema,
524    },
525    /// Set table's current schema
526    #[serde(rename_all = "kebab-case")]
527    SetCurrentSchema {
528        /// Schema ID to set as current, or -1 to set last added schema
529        schema_id: i32,
530    },
531    /// Add a new partition spec to the table
532    AddSpec {
533        /// The partition spec to add.
534        spec: UnboundPartitionSpec,
535    },
536    /// Set table's default spec
537    #[serde(rename_all = "kebab-case")]
538    SetDefaultSpec {
539        /// Partition spec ID to set as the default, or -1 to set last added spec
540        spec_id: i32,
541    },
542    /// Add sort order to table.
543    #[serde(rename_all = "kebab-case")]
544    AddSortOrder {
545        /// Sort order to add.
546        sort_order: SortOrder,
547    },
548    /// Set table's default sort order
549    #[serde(rename_all = "kebab-case")]
550    SetDefaultSortOrder {
551        /// Sort order ID to set as the default, or -1 to set last added sort order
552        sort_order_id: i64,
553    },
554    /// Add snapshot to table.
555    #[serde(rename_all = "kebab-case")]
556    AddSnapshot {
557        /// Snapshot to add.
558        #[serde(
559            deserialize_with = "deserialize_snapshot",
560            serialize_with = "serialize_snapshot"
561        )]
562        snapshot: Snapshot,
563    },
564    /// Set table's snapshot ref.
565    #[serde(rename_all = "kebab-case")]
566    SetSnapshotRef {
567        /// Name of snapshot reference to set.
568        ref_name: String,
569        /// Snapshot reference to set.
570        #[serde(flatten)]
571        reference: SnapshotReference,
572    },
573    /// Remove table's snapshots
574    #[serde(rename_all = "kebab-case")]
575    RemoveSnapshots {
576        /// Snapshot ids to remove.
577        snapshot_ids: Vec<i64>,
578    },
579    /// Remove snapshot reference
580    #[serde(rename_all = "kebab-case")]
581    RemoveSnapshotRef {
582        /// Name of snapshot reference to remove.
583        ref_name: String,
584    },
585    /// Update table's location
586    SetLocation {
587        /// New location for table.
588        location: String,
589    },
590    /// Update table's properties
591    SetProperties {
592        /// Properties to update for table.
593        updates: HashMap<String, String>,
594    },
595    /// Remove table's properties
596    RemoveProperties {
597        /// Properties to remove
598        removals: Vec<String>,
599    },
600    /// Remove partition specs
601    #[serde(rename_all = "kebab-case")]
602    RemovePartitionSpecs {
603        /// Partition spec ids to remove.
604        spec_ids: Vec<i32>,
605    },
606    /// Set statistics for a snapshot
607    #[serde(with = "_serde_set_statistics")]
608    SetStatistics {
609        /// File containing the statistics
610        statistics: StatisticsFile,
611    },
612    /// Remove statistics for a snapshot
613    #[serde(rename_all = "kebab-case")]
614    RemoveStatistics {
615        /// Snapshot id to remove statistics for.
616        snapshot_id: i64,
617    },
618    /// Set partition statistics for a snapshot
619    #[serde(rename_all = "kebab-case")]
620    SetPartitionStatistics {
621        /// File containing the partition statistics
622        partition_statistics: PartitionStatisticsFile,
623    },
624    /// Remove partition statistics for a snapshot
625    #[serde(rename_all = "kebab-case")]
626    RemovePartitionStatistics {
627        /// Snapshot id to remove partition statistics for.
628        snapshot_id: i64,
629    },
630    /// Remove schemas
631    #[serde(rename_all = "kebab-case")]
632    RemoveSchemas {
633        /// Schema IDs to remove.
634        schema_ids: Vec<i32>,
635    },
636    /// Add an encryption key
637    #[serde(rename_all = "kebab-case")]
638    AddEncryptionKey {
639        /// The encryption key to add.
640        encryption_key: EncryptedKey,
641    },
642    /// Remove an encryption key
643    #[serde(rename_all = "kebab-case")]
644    RemoveEncryptionKey {
645        /// The id of the encryption key to remove.
646        key_id: String,
647    },
648}
649
650impl TableUpdate {
651    /// Applies the update to the table metadata builder.
652    pub fn apply(self, builder: TableMetadataBuilder) -> Result<TableMetadataBuilder> {
653        match self {
654            TableUpdate::AssignUuid { uuid } => Ok(builder.assign_uuid(uuid)),
655            TableUpdate::AddSchema { schema, .. } => Ok(builder.add_schema(schema)?),
656            TableUpdate::SetCurrentSchema { schema_id } => builder.set_current_schema(schema_id),
657            TableUpdate::AddSpec { spec } => builder.add_partition_spec(spec),
658            TableUpdate::SetDefaultSpec { spec_id } => builder.set_default_partition_spec(spec_id),
659            TableUpdate::AddSortOrder { sort_order } => builder.add_sort_order(sort_order),
660            TableUpdate::SetDefaultSortOrder { sort_order_id } => {
661                builder.set_default_sort_order(sort_order_id)
662            }
663            TableUpdate::AddSnapshot { snapshot } => builder.add_snapshot(snapshot),
664            TableUpdate::SetSnapshotRef {
665                ref_name,
666                reference,
667            } => builder.set_ref(&ref_name, reference),
668            TableUpdate::RemoveSnapshots { snapshot_ids } => {
669                Ok(builder.remove_snapshots(&snapshot_ids))
670            }
671            TableUpdate::RemoveSnapshotRef { ref_name } => Ok(builder.remove_ref(&ref_name)),
672            TableUpdate::SetLocation { location } => Ok(builder.set_location(location)),
673            TableUpdate::SetProperties { updates } => builder.set_properties(updates),
674            TableUpdate::RemoveProperties { removals } => builder.remove_properties(&removals),
675            TableUpdate::UpgradeFormatVersion { format_version } => {
676                builder.upgrade_format_version(format_version)
677            }
678            TableUpdate::RemovePartitionSpecs { spec_ids } => {
679                builder.remove_partition_specs(&spec_ids)
680            }
681            TableUpdate::SetStatistics { statistics } => Ok(builder.set_statistics(statistics)),
682            TableUpdate::RemoveStatistics { snapshot_id } => {
683                Ok(builder.remove_statistics(snapshot_id))
684            }
685            TableUpdate::SetPartitionStatistics {
686                partition_statistics,
687            } => Ok(builder.set_partition_statistics(partition_statistics)),
688            TableUpdate::RemovePartitionStatistics { snapshot_id } => {
689                Ok(builder.remove_partition_statistics(snapshot_id))
690            }
691            TableUpdate::RemoveSchemas { schema_ids } => builder.remove_schemas(&schema_ids),
692            TableUpdate::AddEncryptionKey { encryption_key } => {
693                Ok(builder.add_encryption_key(encryption_key))
694            }
695            TableUpdate::RemoveEncryptionKey { key_id } => {
696                Ok(builder.remove_encryption_key(&key_id))
697            }
698        }
699    }
700}
701
702impl TableRequirement {
703    /// Check that the requirement is met by the table metadata.
704    /// If the requirement is not met, an appropriate error is returned.
705    ///
706    /// Provide metadata as `None` if the table does not exist.
707    pub fn check(&self, metadata: Option<&TableMetadata>) -> Result<()> {
708        if let Some(metadata) = metadata {
709            match self {
710                TableRequirement::NotExist => {
711                    return Err(Error::new(
712                        ErrorKind::CatalogCommitConflicts,
713                        format!(
714                            "Requirement failed: Table with id {} already exists",
715                            metadata.uuid()
716                        ),
717                    )
718                    .with_retryable(true));
719                }
720                TableRequirement::UuidMatch { uuid } => {
721                    if &metadata.uuid() != uuid {
722                        return Err(Error::new(
723                            ErrorKind::CatalogCommitConflicts,
724                            "Requirement failed: Table UUID does not match",
725                        )
726                        .with_context("expected", *uuid)
727                        .with_context("found", metadata.uuid())
728                        .with_retryable(true));
729                    }
730                }
731                TableRequirement::CurrentSchemaIdMatch { current_schema_id } => {
732                    // ToDo: Harmonize the types of current_schema_id
733                    if metadata.current_schema_id != *current_schema_id {
734                        return Err(Error::new(
735                            ErrorKind::CatalogCommitConflicts,
736                            "Requirement failed: Current schema id does not match",
737                        )
738                        .with_context("expected", current_schema_id.to_string())
739                        .with_context("found", metadata.current_schema_id.to_string())
740                        .with_retryable(true));
741                    }
742                }
743                TableRequirement::DefaultSortOrderIdMatch {
744                    default_sort_order_id,
745                } => {
746                    if metadata.default_sort_order().order_id != *default_sort_order_id {
747                        return Err(Error::new(
748                            ErrorKind::CatalogCommitConflicts,
749                            "Requirement failed: Default sort order id does not match",
750                        )
751                        .with_context("expected", default_sort_order_id.to_string())
752                        .with_context("found", metadata.default_sort_order().order_id.to_string())
753                        .with_retryable(true));
754                    }
755                }
756                TableRequirement::RefSnapshotIdMatch { r#ref, snapshot_id } => {
757                    let snapshot_ref = metadata.snapshot_for_ref(r#ref);
758                    if let Some(snapshot_id) = snapshot_id {
759                        let snapshot_ref = snapshot_ref.ok_or(
760                            Error::new(
761                                ErrorKind::CatalogCommitConflicts,
762                                format!("Requirement failed: Branch or tag `{ref}` not found"),
763                            )
764                            .with_retryable(true),
765                        )?;
766                        if snapshot_ref.snapshot_id() != *snapshot_id {
767                            return Err(Error::new(
768                                ErrorKind::CatalogCommitConflicts,
769                                format!(
770                                    "Requirement failed: Branch or tag `{ref}`'s snapshot has changed"
771                                ),
772                            )
773                            .with_context("expected", snapshot_id.to_string())
774                            .with_context("found", snapshot_ref.snapshot_id().to_string())
775                            .with_retryable(true));
776                        }
777                    } else if snapshot_ref.is_some() {
778                        // a null snapshot ID means the ref should not exist already
779                        return Err(Error::new(
780                            ErrorKind::CatalogCommitConflicts,
781                            format!("Requirement failed: Branch or tag `{ref}` already exists"),
782                        )
783                        .with_retryable(true));
784                    }
785                }
786                TableRequirement::DefaultSpecIdMatch { default_spec_id } => {
787                    // ToDo: Harmonize the types of default_spec_id
788                    if metadata.default_partition_spec_id() != *default_spec_id {
789                        return Err(Error::new(
790                            ErrorKind::CatalogCommitConflicts,
791                            "Requirement failed: Default partition spec id does not match",
792                        )
793                        .with_context("expected", default_spec_id.to_string())
794                        .with_context("found", metadata.default_partition_spec_id().to_string())
795                        .with_retryable(true));
796                    }
797                }
798                TableRequirement::LastAssignedPartitionIdMatch {
799                    last_assigned_partition_id,
800                } => {
801                    if metadata.last_partition_id != *last_assigned_partition_id {
802                        return Err(Error::new(
803                            ErrorKind::CatalogCommitConflicts,
804                            "Requirement failed: Last assigned partition id does not match",
805                        )
806                        .with_context("expected", last_assigned_partition_id.to_string())
807                        .with_context("found", metadata.last_partition_id.to_string())
808                        .with_retryable(true));
809                    }
810                }
811                TableRequirement::LastAssignedFieldIdMatch {
812                    last_assigned_field_id,
813                } => {
814                    if &metadata.last_column_id != last_assigned_field_id {
815                        return Err(Error::new(
816                            ErrorKind::CatalogCommitConflicts,
817                            "Requirement failed: Last assigned field id does not match",
818                        )
819                        .with_context("expected", last_assigned_field_id.to_string())
820                        .with_context("found", metadata.last_column_id.to_string())
821                        .with_retryable(true));
822                    }
823                }
824            };
825        } else {
826            match self {
827                TableRequirement::NotExist => {}
828                _ => {
829                    return Err(Error::new(
830                        ErrorKind::TableNotFound,
831                        "Requirement failed: Table does not exist",
832                    ));
833                }
834            }
835        }
836
837        Ok(())
838    }
839}
840
841pub(super) mod _serde {
842    use serde::{Deserialize as _, Deserializer, Serialize as _};
843
844    use super::*;
845    use crate::spec::{SchemaId, Summary};
846
847    pub(super) fn deserialize_snapshot<'de, D>(
848        deserializer: D,
849    ) -> std::result::Result<Snapshot, D::Error>
850    where D: Deserializer<'de> {
851        let buf = CatalogSnapshot::deserialize(deserializer)?;
852        Ok(buf.into())
853    }
854
855    pub(super) fn serialize_snapshot<S>(
856        snapshot: &Snapshot,
857        serializer: S,
858    ) -> std::result::Result<S::Ok, S::Error>
859    where
860        S: serde::Serializer,
861    {
862        let buf: CatalogSnapshot = snapshot.clone().into();
863        buf.serialize(serializer)
864    }
865
866    #[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
867    #[serde(rename_all = "kebab-case")]
868    /// Defines the structure of a v2 snapshot for the catalog.
869    /// Main difference to SnapshotV2 is that sequence-number is optional
870    /// in the rest catalog spec to allow for backwards compatibility with v1.
871    struct CatalogSnapshot {
872        snapshot_id: i64,
873        #[serde(skip_serializing_if = "Option::is_none")]
874        parent_snapshot_id: Option<i64>,
875        #[serde(default)]
876        sequence_number: i64,
877        timestamp_ms: i64,
878        manifest_list: String,
879        summary: Summary,
880        #[serde(skip_serializing_if = "Option::is_none")]
881        schema_id: Option<SchemaId>,
882        #[serde(skip_serializing_if = "Option::is_none")]
883        first_row_id: Option<u64>,
884        #[serde(skip_serializing_if = "Option::is_none")]
885        added_rows: Option<u64>,
886        #[serde(skip_serializing_if = "Option::is_none")]
887        key_id: Option<String>,
888    }
889
890    impl From<CatalogSnapshot> for Snapshot {
891        fn from(snapshot: CatalogSnapshot) -> Self {
892            let CatalogSnapshot {
893                snapshot_id,
894                parent_snapshot_id,
895                sequence_number,
896                timestamp_ms,
897                manifest_list,
898                schema_id,
899                summary,
900                first_row_id,
901                added_rows,
902                key_id,
903            } = snapshot;
904            let builder = Snapshot::builder()
905                .with_snapshot_id(snapshot_id)
906                .with_parent_snapshot_id(parent_snapshot_id)
907                .with_sequence_number(sequence_number)
908                .with_timestamp_ms(timestamp_ms)
909                .with_manifest_list(manifest_list)
910                .with_summary(summary)
911                .with_encryption_key_id(key_id);
912            let row_range = first_row_id.zip(added_rows);
913            match (schema_id, row_range) {
914                (None, None) => builder.build(),
915                (Some(schema_id), None) => builder.with_schema_id(schema_id).build(),
916                (None, Some((first_row_id, last_row_id))) => {
917                    builder.with_row_range(first_row_id, last_row_id).build()
918                }
919                (Some(schema_id), Some((first_row_id, last_row_id))) => builder
920                    .with_schema_id(schema_id)
921                    .with_row_range(first_row_id, last_row_id)
922                    .build(),
923            }
924        }
925    }
926
927    impl From<Snapshot> for CatalogSnapshot {
928        fn from(snapshot: Snapshot) -> Self {
929            let first_row_id = snapshot.first_row_id();
930            let added_rows = snapshot.added_rows_count();
931            let Snapshot {
932                snapshot_id,
933                parent_snapshot_id,
934                sequence_number,
935                timestamp_ms,
936                manifest_list,
937                summary,
938                schema_id,
939                row_range: _,
940                encryption_key_id: key_id,
941            } = snapshot;
942            CatalogSnapshot {
943                snapshot_id,
944                parent_snapshot_id,
945                sequence_number,
946                timestamp_ms,
947                manifest_list,
948                summary,
949                schema_id,
950                first_row_id,
951                added_rows,
952                key_id,
953            }
954        }
955    }
956}
957
958/// ViewCreation represents the creation of a view in the catalog.
959#[derive(Debug, TypedBuilder)]
960pub struct ViewCreation {
961    /// The name of the view.
962    pub name: String,
963    /// The view's base location; used to create metadata file locations
964    pub location: String,
965    /// Representations for the view.
966    pub representations: ViewRepresentations,
967    /// The schema of the view.
968    pub schema: Schema,
969    /// The properties of the view.
970    #[builder(default)]
971    pub properties: HashMap<String, String>,
972    /// The default namespace to use when a reference in the SELECT is a single identifier
973    pub default_namespace: NamespaceIdent,
974    /// Default catalog to use when a reference in the SELECT does not contain a catalog
975    #[builder(default)]
976    pub default_catalog: Option<String>,
977    /// A string to string map of summary metadata about the version
978    /// Typical keys are "engine-name" and "engine-version"
979    #[builder(default)]
980    pub summary: HashMap<String, String>,
981}
982
983/// ViewUpdate represents an update to a view in the catalog.
984#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
985#[serde(tag = "action", rename_all = "kebab-case")]
986#[allow(clippy::large_enum_variant)]
987pub enum ViewUpdate {
988    /// Assign a new UUID to the view
989    #[serde(rename_all = "kebab-case")]
990    AssignUuid {
991        /// The new UUID to assign.
992        uuid: Uuid,
993    },
994    /// Upgrade view's format version
995    #[serde(rename_all = "kebab-case")]
996    UpgradeFormatVersion {
997        /// Target format upgrade to.
998        format_version: ViewFormatVersion,
999    },
1000    /// Add a new schema to the view
1001    #[serde(rename_all = "kebab-case")]
1002    AddSchema {
1003        /// The schema to add.
1004        schema: Schema,
1005        /// The last column id of the view.
1006        last_column_id: Option<i32>,
1007    },
1008    /// Set view's current schema
1009    #[serde(rename_all = "kebab-case")]
1010    SetLocation {
1011        /// New location for view.
1012        location: String,
1013    },
1014    /// Set view's properties
1015    ///
1016    /// Matching keys are updated, and non-matching keys are left unchanged.
1017    #[serde(rename_all = "kebab-case")]
1018    SetProperties {
1019        /// Properties to update for view.
1020        updates: HashMap<String, String>,
1021    },
1022    /// Remove view's properties
1023    #[serde(rename_all = "kebab-case")]
1024    RemoveProperties {
1025        /// Properties to remove
1026        removals: Vec<String>,
1027    },
1028    /// Add a new version to the view
1029    #[serde(rename_all = "kebab-case")]
1030    AddViewVersion {
1031        /// The view version to add.
1032        view_version: ViewVersion,
1033    },
1034    /// Set view's current version
1035    #[serde(rename_all = "kebab-case")]
1036    SetCurrentViewVersion {
1037        /// View version id to set as current, or -1 to set last added version
1038        view_version_id: i32,
1039    },
1040}
1041
1042mod _serde_set_statistics {
1043    // The rest spec requires an additional field `snapshot-id`
1044    // that is redundant with the `snapshot_id` field in the statistics file.
1045    use serde::{Deserialize, Deserializer, Serialize, Serializer};
1046
1047    use super::*;
1048
1049    #[derive(Debug, Serialize, Deserialize)]
1050    #[serde(rename_all = "kebab-case")]
1051    struct SetStatistics {
1052        snapshot_id: Option<i64>,
1053        statistics: StatisticsFile,
1054    }
1055
1056    pub fn serialize<S>(
1057        value: &StatisticsFile,
1058        serializer: S,
1059    ) -> std::result::Result<S::Ok, S::Error>
1060    where
1061        S: Serializer,
1062    {
1063        SetStatistics {
1064            snapshot_id: Some(value.snapshot_id),
1065            statistics: value.clone(),
1066        }
1067        .serialize(serializer)
1068    }
1069
1070    pub fn deserialize<'de, D>(deserializer: D) -> std::result::Result<StatisticsFile, D::Error>
1071    where D: Deserializer<'de> {
1072        let SetStatistics {
1073            snapshot_id,
1074            statistics,
1075        } = SetStatistics::deserialize(deserializer)?;
1076        if let Some(snapshot_id) = snapshot_id
1077            && snapshot_id != statistics.snapshot_id
1078        {
1079            return Err(serde::de::Error::custom(format!(
1080                "Snapshot id to set {snapshot_id} does not match the statistics file snapshot id {}",
1081                statistics.snapshot_id
1082            )));
1083        }
1084
1085        Ok(statistics)
1086    }
1087}
1088
1089#[cfg(test)]
1090mod tests {
1091    use std::collections::HashMap;
1092    use std::fmt::Debug;
1093    use std::fs::File;
1094    use std::io::BufReader;
1095
1096    use base64::Engine as _;
1097    use serde::Serialize;
1098    use serde::de::DeserializeOwned;
1099    use uuid::uuid;
1100
1101    use super::ViewUpdate;
1102    use crate::io::FileIO;
1103    use crate::spec::{
1104        BlobMetadata, EncryptedKey, FormatVersion, MAIN_BRANCH, NestedField, NullOrder, Operation,
1105        PartitionStatisticsFile, PrimitiveType, Schema, Snapshot, SnapshotReference,
1106        SnapshotRetention, SortDirection, SortField, SortOrder, SqlViewRepresentation,
1107        StatisticsFile, Summary, TableMetadata, TableMetadataBuilder, Transform, Type,
1108        UnboundPartitionSpec, ViewFormatVersion, ViewRepresentation, ViewRepresentations,
1109        ViewVersion,
1110    };
1111    use crate::table::Table;
1112    use crate::test_utils::test_runtime;
1113    use crate::{
1114        NamespaceIdent, TableCommit, TableCreation, TableIdent, TableRequirement, TableUpdate,
1115    };
1116
1117    #[test]
1118    fn test_parent_namespace() {
1119        let ns1 = NamespaceIdent::from_strs(vec!["ns1"]).unwrap();
1120        let ns2 = NamespaceIdent::from_strs(vec!["ns1", "ns2"]).unwrap();
1121        let ns3 = NamespaceIdent::from_strs(vec!["ns1", "ns2", "ns3"]).unwrap();
1122
1123        assert_eq!(ns1.parent(), None);
1124        assert_eq!(ns2.parent(), Some(ns1.clone()));
1125        assert_eq!(ns3.parent(), Some(ns2.clone()));
1126    }
1127
1128    #[test]
1129    fn test_create_table_id() {
1130        let table_id = TableIdent {
1131            namespace: NamespaceIdent::from_strs(vec!["ns1"]).unwrap(),
1132            name: "t1".to_string(),
1133        };
1134
1135        assert_eq!(table_id, TableIdent::from_strs(vec!["ns1", "t1"]).unwrap());
1136    }
1137
1138    #[test]
1139    fn test_table_creation_iterator_properties() {
1140        let builder = TableCreation::builder()
1141            .name("table".to_string())
1142            .schema(Schema::builder().build().unwrap());
1143
1144        fn s(k: &str, v: &str) -> (String, String) {
1145            (k.to_string(), v.to_string())
1146        }
1147
1148        let table_creation = builder
1149            .properties([s("key", "value"), s("foo", "bar")])
1150            .build();
1151
1152        assert_eq!(
1153            HashMap::from([s("key", "value"), s("foo", "bar")]),
1154            table_creation.properties
1155        );
1156    }
1157
1158    fn test_serde_json<T: Serialize + DeserializeOwned + PartialEq + Debug>(
1159        json: impl ToString,
1160        expected: T,
1161    ) {
1162        let json_str = json.to_string();
1163        let actual: T = serde_json::from_str(&json_str).expect("Failed to parse from json");
1164        assert_eq!(actual, expected, "Parsed value is not equal to expected");
1165
1166        let restored: T = serde_json::from_str(
1167            &serde_json::to_string(&actual).expect("Failed to serialize to json"),
1168        )
1169        .expect("Failed to parse from serialized json");
1170
1171        assert_eq!(
1172            restored, expected,
1173            "Parsed restored value is not equal to expected"
1174        );
1175    }
1176
1177    fn metadata() -> TableMetadata {
1178        let tbl_creation = TableCreation::builder()
1179            .name("table".to_string())
1180            .location("/path/to/table".to_string())
1181            .schema(Schema::builder().build().unwrap())
1182            .build();
1183
1184        TableMetadataBuilder::from_table_creation(tbl_creation)
1185            .unwrap()
1186            .assign_uuid(uuid::Uuid::nil())
1187            .build()
1188            .unwrap()
1189            .metadata
1190    }
1191
1192    #[test]
1193    fn test_check_requirement_not_exist() {
1194        let metadata = metadata();
1195        let requirement = TableRequirement::NotExist;
1196
1197        assert!(requirement.check(Some(&metadata)).is_err());
1198        assert!(requirement.check(None).is_ok());
1199    }
1200
1201    #[test]
1202    fn test_check_table_uuid() {
1203        let metadata = metadata();
1204
1205        let requirement = TableRequirement::UuidMatch {
1206            uuid: uuid::Uuid::now_v7(),
1207        };
1208        assert!(requirement.check(Some(&metadata)).is_err());
1209
1210        let requirement = TableRequirement::UuidMatch {
1211            uuid: uuid::Uuid::nil(),
1212        };
1213        assert!(requirement.check(Some(&metadata)).is_ok());
1214    }
1215
1216    #[test]
1217    fn test_check_ref_snapshot_id() {
1218        let metadata = metadata();
1219
1220        // Ref does not exist but should
1221        let requirement = TableRequirement::RefSnapshotIdMatch {
1222            r#ref: "my_branch".to_string(),
1223            snapshot_id: Some(1),
1224        };
1225        assert!(requirement.check(Some(&metadata)).is_err());
1226
1227        // Ref does not exist and should not
1228        let requirement = TableRequirement::RefSnapshotIdMatch {
1229            r#ref: "my_branch".to_string(),
1230            snapshot_id: None,
1231        };
1232        assert!(requirement.check(Some(&metadata)).is_ok());
1233
1234        // Add snapshot
1235        let snapshot = Snapshot::builder()
1236            .with_snapshot_id(3051729675574597004)
1237            .with_sequence_number(10)
1238            .with_timestamp_ms(9992191116217)
1239            .with_manifest_list("s3://b/wh/.../s1.avro".to_string())
1240            .with_schema_id(0)
1241            .with_summary(Summary {
1242                operation: Operation::Append,
1243                additional_properties: HashMap::new(),
1244            })
1245            .build();
1246
1247        let builder = metadata.into_builder(None);
1248        let builder = TableUpdate::AddSnapshot {
1249            snapshot: snapshot.clone(),
1250        }
1251        .apply(builder)
1252        .unwrap();
1253        let metadata = TableUpdate::SetSnapshotRef {
1254            ref_name: MAIN_BRANCH.to_string(),
1255            reference: SnapshotReference {
1256                snapshot_id: snapshot.snapshot_id(),
1257                retention: SnapshotRetention::Branch {
1258                    min_snapshots_to_keep: Some(10),
1259                    max_snapshot_age_ms: None,
1260                    max_ref_age_ms: None,
1261                },
1262            },
1263        }
1264        .apply(builder)
1265        .unwrap()
1266        .build()
1267        .unwrap()
1268        .metadata;
1269
1270        // Ref exists and should match
1271        let requirement = TableRequirement::RefSnapshotIdMatch {
1272            r#ref: "main".to_string(),
1273            snapshot_id: Some(3051729675574597004),
1274        };
1275        assert!(requirement.check(Some(&metadata)).is_ok());
1276
1277        // Ref exists but does not match
1278        let requirement = TableRequirement::RefSnapshotIdMatch {
1279            r#ref: "main".to_string(),
1280            snapshot_id: Some(1),
1281        };
1282        assert!(requirement.check(Some(&metadata)).is_err());
1283    }
1284
1285    #[test]
1286    fn test_check_last_assigned_field_id() {
1287        let metadata = metadata();
1288
1289        let requirement = TableRequirement::LastAssignedFieldIdMatch {
1290            last_assigned_field_id: 1,
1291        };
1292        assert!(requirement.check(Some(&metadata)).is_err());
1293
1294        let requirement = TableRequirement::LastAssignedFieldIdMatch {
1295            last_assigned_field_id: 0,
1296        };
1297        assert!(requirement.check(Some(&metadata)).is_ok());
1298    }
1299
1300    #[test]
1301    fn test_check_current_schema_id() {
1302        let metadata = metadata();
1303
1304        let requirement = TableRequirement::CurrentSchemaIdMatch {
1305            current_schema_id: 1,
1306        };
1307        assert!(requirement.check(Some(&metadata)).is_err());
1308
1309        let requirement = TableRequirement::CurrentSchemaIdMatch {
1310            current_schema_id: 0,
1311        };
1312        assert!(requirement.check(Some(&metadata)).is_ok());
1313    }
1314
1315    #[test]
1316    fn test_check_last_assigned_partition_id() {
1317        let metadata = metadata();
1318        let requirement = TableRequirement::LastAssignedPartitionIdMatch {
1319            last_assigned_partition_id: 0,
1320        };
1321        assert!(requirement.check(Some(&metadata)).is_err());
1322
1323        let requirement = TableRequirement::LastAssignedPartitionIdMatch {
1324            last_assigned_partition_id: 999,
1325        };
1326        assert!(requirement.check(Some(&metadata)).is_ok());
1327    }
1328
1329    #[test]
1330    fn test_check_default_spec_id() {
1331        let metadata = metadata();
1332
1333        let requirement = TableRequirement::DefaultSpecIdMatch { default_spec_id: 1 };
1334        assert!(requirement.check(Some(&metadata)).is_err());
1335
1336        let requirement = TableRequirement::DefaultSpecIdMatch { default_spec_id: 0 };
1337        assert!(requirement.check(Some(&metadata)).is_ok());
1338    }
1339
1340    #[test]
1341    fn test_check_default_sort_order_id() {
1342        let metadata = metadata();
1343
1344        let requirement = TableRequirement::DefaultSortOrderIdMatch {
1345            default_sort_order_id: 1,
1346        };
1347        assert!(requirement.check(Some(&metadata)).is_err());
1348
1349        let requirement = TableRequirement::DefaultSortOrderIdMatch {
1350            default_sort_order_id: 0,
1351        };
1352        assert!(requirement.check(Some(&metadata)).is_ok());
1353    }
1354
1355    #[test]
1356    fn test_table_uuid() {
1357        test_serde_json(
1358            r#"
1359{
1360    "type": "assert-table-uuid",
1361    "uuid": "2cc52516-5e73-41f2-b139-545d41a4e151"
1362}
1363        "#,
1364            TableRequirement::UuidMatch {
1365                uuid: uuid!("2cc52516-5e73-41f2-b139-545d41a4e151"),
1366            },
1367        );
1368    }
1369
1370    #[test]
1371    fn test_assert_table_not_exists() {
1372        test_serde_json(
1373            r#"
1374{
1375    "type": "assert-create"
1376}
1377        "#,
1378            TableRequirement::NotExist,
1379        );
1380    }
1381
1382    #[test]
1383    fn test_assert_ref_snapshot_id() {
1384        test_serde_json(
1385            r#"
1386{
1387    "type": "assert-ref-snapshot-id",
1388    "ref": "snapshot-name",
1389    "snapshot-id": null
1390}
1391        "#,
1392            TableRequirement::RefSnapshotIdMatch {
1393                r#ref: "snapshot-name".to_string(),
1394                snapshot_id: None,
1395            },
1396        );
1397
1398        test_serde_json(
1399            r#"
1400{
1401    "type": "assert-ref-snapshot-id",
1402    "ref": "snapshot-name",
1403    "snapshot-id": 1
1404}
1405        "#,
1406            TableRequirement::RefSnapshotIdMatch {
1407                r#ref: "snapshot-name".to_string(),
1408                snapshot_id: Some(1),
1409            },
1410        );
1411    }
1412
1413    #[test]
1414    fn test_assert_last_assigned_field_id() {
1415        test_serde_json(
1416            r#"
1417{
1418    "type": "assert-last-assigned-field-id",
1419    "last-assigned-field-id": 12
1420}
1421        "#,
1422            TableRequirement::LastAssignedFieldIdMatch {
1423                last_assigned_field_id: 12,
1424            },
1425        );
1426    }
1427
1428    #[test]
1429    fn test_assert_current_schema_id() {
1430        test_serde_json(
1431            r#"
1432{
1433    "type": "assert-current-schema-id",
1434    "current-schema-id": 4
1435}
1436        "#,
1437            TableRequirement::CurrentSchemaIdMatch {
1438                current_schema_id: 4,
1439            },
1440        );
1441    }
1442
1443    #[test]
1444    fn test_assert_last_assigned_partition_id() {
1445        test_serde_json(
1446            r#"
1447{
1448    "type": "assert-last-assigned-partition-id",
1449    "last-assigned-partition-id": 1004
1450}
1451        "#,
1452            TableRequirement::LastAssignedPartitionIdMatch {
1453                last_assigned_partition_id: 1004,
1454            },
1455        );
1456    }
1457
1458    #[test]
1459    fn test_assert_default_spec_id() {
1460        test_serde_json(
1461            r#"
1462{
1463    "type": "assert-default-spec-id",
1464    "default-spec-id": 5
1465}
1466        "#,
1467            TableRequirement::DefaultSpecIdMatch { default_spec_id: 5 },
1468        );
1469    }
1470
1471    #[test]
1472    fn test_assert_default_sort_order() {
1473        let json = r#"
1474{
1475    "type": "assert-default-sort-order-id",
1476    "default-sort-order-id": 10
1477}
1478        "#;
1479
1480        let update = TableRequirement::DefaultSortOrderIdMatch {
1481            default_sort_order_id: 10,
1482        };
1483
1484        test_serde_json(json, update);
1485    }
1486
1487    #[test]
1488    fn test_parse_assert_invalid() {
1489        assert!(
1490            serde_json::from_str::<TableRequirement>(
1491                r#"
1492{
1493    "default-sort-order-id": 10
1494}
1495"#
1496            )
1497            .is_err(),
1498            "Table requirements should not be parsed without type."
1499        );
1500    }
1501
1502    #[test]
1503    fn test_assign_uuid() {
1504        test_serde_json(
1505            r#"
1506{
1507    "action": "assign-uuid",
1508    "uuid": "2cc52516-5e73-41f2-b139-545d41a4e151"
1509}
1510        "#,
1511            TableUpdate::AssignUuid {
1512                uuid: uuid!("2cc52516-5e73-41f2-b139-545d41a4e151"),
1513            },
1514        );
1515    }
1516
1517    #[test]
1518    fn test_upgrade_format_version() {
1519        test_serde_json(
1520            r#"
1521{
1522    "action": "upgrade-format-version",
1523    "format-version": 2
1524}
1525        "#,
1526            TableUpdate::UpgradeFormatVersion {
1527                format_version: FormatVersion::V2,
1528            },
1529        );
1530    }
1531
1532    #[test]
1533    fn test_add_schema() {
1534        let test_schema = Schema::builder()
1535            .with_schema_id(1)
1536            .with_identifier_field_ids(vec![2])
1537            .with_fields(vec![
1538                NestedField::optional(1, "foo", Type::Primitive(PrimitiveType::String)).into(),
1539                NestedField::required(2, "bar", Type::Primitive(PrimitiveType::Int)).into(),
1540                NestedField::optional(3, "baz", Type::Primitive(PrimitiveType::Boolean)).into(),
1541            ])
1542            .build()
1543            .unwrap();
1544        test_serde_json(
1545            r#"
1546{
1547    "action": "add-schema",
1548    "schema": {
1549        "type": "struct",
1550        "schema-id": 1,
1551        "fields": [
1552            {
1553                "id": 1,
1554                "name": "foo",
1555                "required": false,
1556                "type": "string"
1557            },
1558            {
1559                "id": 2,
1560                "name": "bar",
1561                "required": true,
1562                "type": "int"
1563            },
1564            {
1565                "id": 3,
1566                "name": "baz",
1567                "required": false,
1568                "type": "boolean"
1569            }
1570        ],
1571        "identifier-field-ids": [
1572            2
1573        ]
1574    },
1575    "last-column-id": 3
1576}
1577        "#,
1578            TableUpdate::AddSchema {
1579                schema: test_schema.clone(),
1580            },
1581        );
1582
1583        test_serde_json(
1584            r#"
1585{
1586    "action": "add-schema",
1587    "schema": {
1588        "type": "struct",
1589        "schema-id": 1,
1590        "fields": [
1591            {
1592                "id": 1,
1593                "name": "foo",
1594                "required": false,
1595                "type": "string"
1596            },
1597            {
1598                "id": 2,
1599                "name": "bar",
1600                "required": true,
1601                "type": "int"
1602            },
1603            {
1604                "id": 3,
1605                "name": "baz",
1606                "required": false,
1607                "type": "boolean"
1608            }
1609        ],
1610        "identifier-field-ids": [
1611            2
1612        ]
1613    }
1614}
1615        "#,
1616            TableUpdate::AddSchema {
1617                schema: test_schema.clone(),
1618            },
1619        );
1620    }
1621
1622    #[test]
1623    fn test_set_current_schema() {
1624        test_serde_json(
1625            r#"
1626{
1627   "action": "set-current-schema",
1628   "schema-id": 23
1629}
1630        "#,
1631            TableUpdate::SetCurrentSchema { schema_id: 23 },
1632        );
1633    }
1634
1635    #[test]
1636    fn test_add_spec() {
1637        test_serde_json(
1638            r#"
1639{
1640    "action": "add-spec",
1641    "spec": {
1642        "fields": [
1643            {
1644                "source-id": 4,
1645                "name": "ts_day",
1646                "transform": "day"
1647            },
1648            {
1649                "source-id": 1,
1650                "name": "id_bucket",
1651                "transform": "bucket[16]"
1652            },
1653            {
1654                "source-id": 2,
1655                "name": "id_truncate",
1656                "transform": "truncate[4]"
1657            }
1658        ]
1659    }
1660}
1661        "#,
1662            TableUpdate::AddSpec {
1663                spec: UnboundPartitionSpec::builder()
1664                    .add_partition_field(4, "ts_day".to_string(), Transform::Day)
1665                    .unwrap()
1666                    .add_partition_field(1, "id_bucket".to_string(), Transform::Bucket(16))
1667                    .unwrap()
1668                    .add_partition_field(2, "id_truncate".to_string(), Transform::Truncate(4))
1669                    .unwrap()
1670                    .build(),
1671            },
1672        );
1673    }
1674
1675    #[test]
1676    fn test_set_default_spec() {
1677        test_serde_json(
1678            r#"
1679{
1680    "action": "set-default-spec",
1681    "spec-id": 1
1682}
1683        "#,
1684            TableUpdate::SetDefaultSpec { spec_id: 1 },
1685        )
1686    }
1687
1688    #[test]
1689    fn test_add_sort_order() {
1690        let json = r#"
1691{
1692    "action": "add-sort-order",
1693    "sort-order": {
1694        "order-id": 1,
1695        "fields": [
1696            {
1697                "transform": "identity",
1698                "source-id": 2,
1699                "direction": "asc",
1700                "null-order": "nulls-first"
1701            },
1702            {
1703                "transform": "bucket[4]",
1704                "source-id": 3,
1705                "direction": "desc",
1706                "null-order": "nulls-last"
1707            }
1708        ]
1709    }
1710}
1711        "#;
1712
1713        let update = TableUpdate::AddSortOrder {
1714            sort_order: SortOrder::builder()
1715                .with_order_id(1)
1716                .with_sort_field(
1717                    SortField::builder()
1718                        .source_id(2)
1719                        .direction(SortDirection::Ascending)
1720                        .null_order(NullOrder::First)
1721                        .transform(Transform::Identity)
1722                        .build(),
1723                )
1724                .with_sort_field(
1725                    SortField::builder()
1726                        .source_id(3)
1727                        .direction(SortDirection::Descending)
1728                        .null_order(NullOrder::Last)
1729                        .transform(Transform::Bucket(4))
1730                        .build(),
1731                )
1732                .build_unbound()
1733                .unwrap(),
1734        };
1735
1736        test_serde_json(json, update);
1737    }
1738
1739    #[test]
1740    fn test_set_default_order() {
1741        let json = r#"
1742{
1743    "action": "set-default-sort-order",
1744    "sort-order-id": 2
1745}
1746        "#;
1747        let update = TableUpdate::SetDefaultSortOrder { sort_order_id: 2 };
1748
1749        test_serde_json(json, update);
1750    }
1751
1752    #[test]
1753    fn test_add_snapshot() {
1754        let json = r#"
1755{
1756    "action": "add-snapshot",
1757    "snapshot": {
1758        "snapshot-id": 3055729675574597000,
1759        "parent-snapshot-id": 3051729675574597000,
1760        "timestamp-ms": 1555100955770,
1761        "sequence-number": 1,
1762        "summary": {
1763            "operation": "append"
1764        },
1765        "manifest-list": "s3://a/b/2.avro",
1766        "schema-id": 1
1767    }
1768}
1769        "#;
1770
1771        let update = TableUpdate::AddSnapshot {
1772            snapshot: Snapshot::builder()
1773                .with_snapshot_id(3055729675574597000)
1774                .with_parent_snapshot_id(Some(3051729675574597000))
1775                .with_timestamp_ms(1555100955770)
1776                .with_sequence_number(1)
1777                .with_manifest_list("s3://a/b/2.avro")
1778                .with_schema_id(1)
1779                .with_summary(Summary {
1780                    operation: Operation::Append,
1781                    additional_properties: HashMap::default(),
1782                })
1783                .build(),
1784        };
1785
1786        test_serde_json(json, update);
1787    }
1788
1789    #[test]
1790    fn test_add_snapshot_v1() {
1791        let json = r#"
1792{
1793    "action": "add-snapshot",
1794    "snapshot": {
1795        "snapshot-id": 3055729675574597000,
1796        "parent-snapshot-id": 3051729675574597000,
1797        "timestamp-ms": 1555100955770,
1798        "summary": {
1799            "operation": "append"
1800        },
1801        "manifest-list": "s3://a/b/2.avro"
1802    }
1803}
1804    "#;
1805
1806        let update = TableUpdate::AddSnapshot {
1807            snapshot: Snapshot::builder()
1808                .with_snapshot_id(3055729675574597000)
1809                .with_parent_snapshot_id(Some(3051729675574597000))
1810                .with_timestamp_ms(1555100955770)
1811                .with_sequence_number(0)
1812                .with_manifest_list("s3://a/b/2.avro")
1813                .with_summary(Summary {
1814                    operation: Operation::Append,
1815                    additional_properties: HashMap::default(),
1816                })
1817                .build(),
1818        };
1819
1820        let actual: TableUpdate = serde_json::from_str(json).expect("Failed to parse from json");
1821        assert_eq!(actual, update, "Parsed value is not equal to expected");
1822    }
1823
1824    #[test]
1825    fn test_add_snapshot_v3() {
1826        let json = serde_json::json!(
1827        {
1828            "action": "add-snapshot",
1829            "snapshot": {
1830                "snapshot-id": 3055729675574597000i64,
1831                "parent-snapshot-id": 3051729675574597000i64,
1832                "timestamp-ms": 1555100955770i64,
1833                "first-row-id":0,
1834                "added-rows":2,
1835                "key-id":"key123",
1836                "summary": {
1837                    "operation": "append"
1838                },
1839                "manifest-list": "s3://a/b/2.avro"
1840            }
1841        });
1842
1843        let update = TableUpdate::AddSnapshot {
1844            snapshot: Snapshot::builder()
1845                .with_snapshot_id(3055729675574597000)
1846                .with_parent_snapshot_id(Some(3051729675574597000))
1847                .with_timestamp_ms(1555100955770)
1848                .with_sequence_number(0)
1849                .with_manifest_list("s3://a/b/2.avro")
1850                .with_row_range(0, 2)
1851                .with_encryption_key_id(Some("key123".to_string()))
1852                .with_summary(Summary {
1853                    operation: Operation::Append,
1854                    additional_properties: HashMap::default(),
1855                })
1856                .build(),
1857        };
1858
1859        let actual: TableUpdate = serde_json::from_value(json).expect("Failed to parse from json");
1860        assert_eq!(actual, update, "Parsed value is not equal to expected");
1861        let restored: TableUpdate = serde_json::from_str(
1862            &serde_json::to_string(&actual).expect("Failed to serialize to json"),
1863        )
1864        .expect("Failed to parse from serialized json");
1865        assert_eq!(restored, update);
1866    }
1867
1868    #[test]
1869    fn test_remove_snapshots() {
1870        let json = r#"
1871{
1872    "action": "remove-snapshots",
1873    "snapshot-ids": [
1874        1,
1875        2
1876    ]
1877}
1878        "#;
1879
1880        let update = TableUpdate::RemoveSnapshots {
1881            snapshot_ids: vec![1, 2],
1882        };
1883        test_serde_json(json, update);
1884    }
1885
1886    #[test]
1887    fn test_remove_snapshot_ref() {
1888        let json = r#"
1889{
1890    "action": "remove-snapshot-ref",
1891    "ref-name": "snapshot-ref"
1892}
1893        "#;
1894
1895        let update = TableUpdate::RemoveSnapshotRef {
1896            ref_name: "snapshot-ref".to_string(),
1897        };
1898        test_serde_json(json, update);
1899    }
1900
1901    #[test]
1902    fn test_set_snapshot_ref_tag() {
1903        let json = r#"
1904{
1905    "action": "set-snapshot-ref",
1906    "type": "tag",
1907    "ref-name": "hank",
1908    "snapshot-id": 1,
1909    "max-ref-age-ms": 1
1910}
1911        "#;
1912
1913        let update = TableUpdate::SetSnapshotRef {
1914            ref_name: "hank".to_string(),
1915            reference: SnapshotReference {
1916                snapshot_id: 1,
1917                retention: SnapshotRetention::Tag {
1918                    max_ref_age_ms: Some(1),
1919                },
1920            },
1921        };
1922
1923        test_serde_json(json, update);
1924    }
1925
1926    #[test]
1927    fn test_set_snapshot_ref_branch() {
1928        let json = r#"
1929{
1930    "action": "set-snapshot-ref",
1931    "type": "branch",
1932    "ref-name": "hank",
1933    "snapshot-id": 1,
1934    "min-snapshots-to-keep": 2,
1935    "max-snapshot-age-ms": 3,
1936    "max-ref-age-ms": 4
1937}
1938        "#;
1939
1940        let update = TableUpdate::SetSnapshotRef {
1941            ref_name: "hank".to_string(),
1942            reference: SnapshotReference {
1943                snapshot_id: 1,
1944                retention: SnapshotRetention::Branch {
1945                    min_snapshots_to_keep: Some(2),
1946                    max_snapshot_age_ms: Some(3),
1947                    max_ref_age_ms: Some(4),
1948                },
1949            },
1950        };
1951
1952        test_serde_json(json, update);
1953    }
1954
1955    #[test]
1956    fn test_set_properties() {
1957        let json = r#"
1958{
1959    "action": "set-properties",
1960    "updates": {
1961        "prop1": "v1",
1962        "prop2": "v2"
1963    }
1964}
1965        "#;
1966
1967        let update = TableUpdate::SetProperties {
1968            updates: vec![
1969                ("prop1".to_string(), "v1".to_string()),
1970                ("prop2".to_string(), "v2".to_string()),
1971            ]
1972            .into_iter()
1973            .collect(),
1974        };
1975
1976        test_serde_json(json, update);
1977    }
1978
1979    #[test]
1980    fn test_remove_properties() {
1981        let json = r#"
1982{
1983    "action": "remove-properties",
1984    "removals": [
1985        "prop1",
1986        "prop2"
1987    ]
1988}
1989        "#;
1990
1991        let update = TableUpdate::RemoveProperties {
1992            removals: vec!["prop1".to_string(), "prop2".to_string()],
1993        };
1994
1995        test_serde_json(json, update);
1996    }
1997
1998    #[test]
1999    fn test_set_location() {
2000        let json = r#"
2001{
2002    "action": "set-location",
2003    "location": "s3://bucket/warehouse/tbl_location"
2004}
2005    "#;
2006
2007        let update = TableUpdate::SetLocation {
2008            location: "s3://bucket/warehouse/tbl_location".to_string(),
2009        };
2010
2011        test_serde_json(json, update);
2012    }
2013
2014    #[test]
2015    fn test_table_update_apply() {
2016        let table_creation = TableCreation::builder()
2017            .location("s3://db/table".to_string())
2018            .name("table".to_string())
2019            .properties(HashMap::new())
2020            .schema(Schema::builder().build().unwrap())
2021            .build();
2022        let table_metadata = TableMetadataBuilder::from_table_creation(table_creation)
2023            .unwrap()
2024            .build()
2025            .unwrap()
2026            .metadata;
2027        let table_metadata_builder = TableMetadataBuilder::new_from_metadata(
2028            table_metadata,
2029            Some("s3://db/table/metadata/metadata1.gz.json".to_string()),
2030        );
2031
2032        let uuid = uuid::Uuid::new_v4();
2033        let update = TableUpdate::AssignUuid { uuid };
2034        let updated_metadata = update
2035            .apply(table_metadata_builder)
2036            .unwrap()
2037            .build()
2038            .unwrap()
2039            .metadata;
2040        assert_eq!(updated_metadata.uuid(), uuid);
2041    }
2042
2043    #[test]
2044    fn test_view_assign_uuid() {
2045        test_serde_json(
2046            r#"
2047{
2048    "action": "assign-uuid",
2049    "uuid": "2cc52516-5e73-41f2-b139-545d41a4e151"
2050}
2051        "#,
2052            ViewUpdate::AssignUuid {
2053                uuid: uuid!("2cc52516-5e73-41f2-b139-545d41a4e151"),
2054            },
2055        );
2056    }
2057
2058    #[test]
2059    fn test_view_upgrade_format_version() {
2060        test_serde_json(
2061            r#"
2062{
2063    "action": "upgrade-format-version",
2064    "format-version": 1
2065}
2066        "#,
2067            ViewUpdate::UpgradeFormatVersion {
2068                format_version: ViewFormatVersion::V1,
2069            },
2070        );
2071    }
2072
2073    #[test]
2074    fn test_view_add_schema() {
2075        let test_schema = Schema::builder()
2076            .with_schema_id(1)
2077            .with_identifier_field_ids(vec![2])
2078            .with_fields(vec![
2079                NestedField::optional(1, "foo", Type::Primitive(PrimitiveType::String)).into(),
2080                NestedField::required(2, "bar", Type::Primitive(PrimitiveType::Int)).into(),
2081                NestedField::optional(3, "baz", Type::Primitive(PrimitiveType::Boolean)).into(),
2082            ])
2083            .build()
2084            .unwrap();
2085        test_serde_json(
2086            r#"
2087{
2088    "action": "add-schema",
2089    "schema": {
2090        "type": "struct",
2091        "schema-id": 1,
2092        "fields": [
2093            {
2094                "id": 1,
2095                "name": "foo",
2096                "required": false,
2097                "type": "string"
2098            },
2099            {
2100                "id": 2,
2101                "name": "bar",
2102                "required": true,
2103                "type": "int"
2104            },
2105            {
2106                "id": 3,
2107                "name": "baz",
2108                "required": false,
2109                "type": "boolean"
2110            }
2111        ],
2112        "identifier-field-ids": [
2113            2
2114        ]
2115    },
2116    "last-column-id": 3
2117}
2118        "#,
2119            ViewUpdate::AddSchema {
2120                schema: test_schema.clone(),
2121                last_column_id: Some(3),
2122            },
2123        );
2124    }
2125
2126    #[test]
2127    fn test_view_set_location() {
2128        test_serde_json(
2129            r#"
2130{
2131    "action": "set-location",
2132    "location": "s3://db/view"
2133}
2134        "#,
2135            ViewUpdate::SetLocation {
2136                location: "s3://db/view".to_string(),
2137            },
2138        );
2139    }
2140
2141    #[test]
2142    fn test_view_set_properties() {
2143        test_serde_json(
2144            r#"
2145{
2146    "action": "set-properties",
2147    "updates": {
2148        "prop1": "v1",
2149        "prop2": "v2"
2150    }
2151}
2152        "#,
2153            ViewUpdate::SetProperties {
2154                updates: vec![
2155                    ("prop1".to_string(), "v1".to_string()),
2156                    ("prop2".to_string(), "v2".to_string()),
2157                ]
2158                .into_iter()
2159                .collect(),
2160            },
2161        );
2162    }
2163
2164    #[test]
2165    fn test_view_remove_properties() {
2166        test_serde_json(
2167            r#"
2168{
2169    "action": "remove-properties",
2170    "removals": [
2171        "prop1",
2172        "prop2"
2173    ]
2174}
2175        "#,
2176            ViewUpdate::RemoveProperties {
2177                removals: vec!["prop1".to_string(), "prop2".to_string()],
2178            },
2179        );
2180    }
2181
2182    #[test]
2183    fn test_view_add_view_version() {
2184        test_serde_json(
2185            r#"
2186{
2187    "action": "add-view-version",
2188    "view-version": {
2189            "version-id" : 1,
2190            "timestamp-ms" : 1573518431292,
2191            "schema-id" : 1,
2192            "default-catalog" : "prod",
2193            "default-namespace" : [ "default" ],
2194            "summary" : {
2195              "engine-name" : "Spark"
2196            },
2197            "representations" : [ {
2198              "type" : "sql",
2199              "sql" : "SELECT\n    COUNT(1), CAST(event_ts AS DATE)\nFROM events\nGROUP BY 2",
2200              "dialect" : "spark"
2201            } ]
2202    }
2203}
2204        "#,
2205            ViewUpdate::AddViewVersion {
2206                view_version: ViewVersion::builder()
2207                    .with_version_id(1)
2208                    .with_timestamp_ms(1573518431292)
2209                    .with_schema_id(1)
2210                    .with_default_catalog(Some("prod".to_string()))
2211                    .with_default_namespace(NamespaceIdent::from_strs(vec!["default"]).unwrap())
2212                    .with_summary(
2213                        vec![("engine-name".to_string(), "Spark".to_string())]
2214                            .into_iter()
2215                            .collect(),
2216                    )
2217                    .with_representations(ViewRepresentations(vec![ViewRepresentation::Sql(SqlViewRepresentation {
2218                        sql: "SELECT\n    COUNT(1), CAST(event_ts AS DATE)\nFROM events\nGROUP BY 2".to_string(),
2219                        dialect: "spark".to_string(),
2220                    })]))
2221                    .build(),
2222            },
2223        );
2224    }
2225
2226    #[test]
2227    fn test_view_set_current_view_version() {
2228        test_serde_json(
2229            r#"
2230{
2231    "action": "set-current-view-version",
2232    "view-version-id": 1
2233}
2234        "#,
2235            ViewUpdate::SetCurrentViewVersion { view_version_id: 1 },
2236        );
2237    }
2238
2239    #[test]
2240    fn test_remove_partition_specs_update() {
2241        test_serde_json(
2242            r#"
2243{
2244    "action": "remove-partition-specs",
2245    "spec-ids": [1, 2]
2246}
2247        "#,
2248            TableUpdate::RemovePartitionSpecs {
2249                spec_ids: vec![1, 2],
2250            },
2251        );
2252    }
2253
2254    #[test]
2255    fn test_set_statistics_file() {
2256        test_serde_json(
2257            r#"
2258        {
2259                "action": "set-statistics",
2260                "snapshot-id": 1940541653261589030,
2261                "statistics": {
2262                        "snapshot-id": 1940541653261589030,
2263                        "statistics-path": "s3://bucket/warehouse/stats.puffin",
2264                        "file-size-in-bytes": 124,
2265                        "file-footer-size-in-bytes": 27,
2266                        "blob-metadata": [
2267                                {
2268                                        "type": "boring-type",
2269                                        "snapshot-id": 1940541653261589030,
2270                                        "sequence-number": 2,
2271                                        "fields": [
2272                                                1
2273                                        ],
2274                                        "properties": {
2275                                                "prop-key": "prop-value"
2276                                        }
2277                                }
2278                        ]
2279                }
2280        }
2281        "#,
2282            TableUpdate::SetStatistics {
2283                statistics: StatisticsFile {
2284                    snapshot_id: 1940541653261589030,
2285                    statistics_path: "s3://bucket/warehouse/stats.puffin".to_string(),
2286                    file_size_in_bytes: 124,
2287                    file_footer_size_in_bytes: 27,
2288                    key_metadata: None,
2289                    blob_metadata: vec![BlobMetadata {
2290                        r#type: "boring-type".to_string(),
2291                        snapshot_id: 1940541653261589030,
2292                        sequence_number: 2,
2293                        fields: vec![1],
2294                        properties: vec![("prop-key".to_string(), "prop-value".to_string())]
2295                            .into_iter()
2296                            .collect(),
2297                    }],
2298                },
2299            },
2300        );
2301    }
2302
2303    #[test]
2304    fn test_remove_statistics_file() {
2305        test_serde_json(
2306            r#"
2307        {
2308                "action": "remove-statistics",
2309                "snapshot-id": 1940541653261589030
2310        }
2311        "#,
2312            TableUpdate::RemoveStatistics {
2313                snapshot_id: 1940541653261589030,
2314            },
2315        );
2316    }
2317
2318    #[test]
2319    fn test_set_partition_statistics_file() {
2320        test_serde_json(
2321            r#"
2322            {
2323                "action": "set-partition-statistics",
2324                "partition-statistics": {
2325                    "snapshot-id": 1940541653261589030,
2326                    "statistics-path": "s3://bucket/warehouse/stats1.parquet",
2327                    "file-size-in-bytes": 43
2328                }
2329            }
2330            "#,
2331            TableUpdate::SetPartitionStatistics {
2332                partition_statistics: PartitionStatisticsFile {
2333                    snapshot_id: 1940541653261589030,
2334                    statistics_path: "s3://bucket/warehouse/stats1.parquet".to_string(),
2335                    file_size_in_bytes: 43,
2336                },
2337            },
2338        )
2339    }
2340
2341    #[test]
2342    fn test_remove_partition_statistics_file() {
2343        test_serde_json(
2344            r#"
2345            {
2346                "action": "remove-partition-statistics",
2347                "snapshot-id": 1940541653261589030
2348            }
2349            "#,
2350            TableUpdate::RemovePartitionStatistics {
2351                snapshot_id: 1940541653261589030,
2352            },
2353        )
2354    }
2355
2356    #[test]
2357    fn test_remove_schema_update() {
2358        test_serde_json(
2359            r#"
2360                {
2361                    "action": "remove-schemas",
2362                    "schema-ids": [1, 2]
2363                }        
2364            "#,
2365            TableUpdate::RemoveSchemas {
2366                schema_ids: vec![1, 2],
2367            },
2368        );
2369    }
2370
2371    #[test]
2372    fn test_add_encryption_key() {
2373        let key_bytes = "key".as_bytes();
2374        let encoded_key = base64::engine::general_purpose::STANDARD.encode(key_bytes);
2375        test_serde_json(
2376            format!(
2377                r#"
2378                {{
2379                    "action": "add-encryption-key",
2380                    "encryption-key": {{
2381                        "key-id": "a",
2382                        "encrypted-key-metadata": "{encoded_key}",
2383                        "encrypted-by-id": "b"
2384                    }}
2385                }}        
2386            "#
2387            ),
2388            TableUpdate::AddEncryptionKey {
2389                encryption_key: EncryptedKey::builder()
2390                    .key_id("a")
2391                    .encrypted_key_metadata(key_bytes.to_vec())
2392                    .encrypted_by_id("b")
2393                    .build(),
2394            },
2395        );
2396    }
2397
2398    #[test]
2399    fn test_remove_encryption_key() {
2400        test_serde_json(
2401            r#"
2402                {
2403                    "action": "remove-encryption-key",
2404                    "key-id": "a"
2405                }        
2406            "#,
2407            TableUpdate::RemoveEncryptionKey {
2408                key_id: "a".to_string(),
2409            },
2410        );
2411    }
2412
2413    #[test]
2414    fn test_table_commit() {
2415        let table = {
2416            let file = File::open(format!(
2417                "{}/testdata/table_metadata/{}",
2418                env!("CARGO_MANIFEST_DIR"),
2419                "TableMetadataV2Valid.json"
2420            ))
2421            .unwrap();
2422            let reader = BufReader::new(file);
2423            let resp = serde_json::from_reader::<_, TableMetadata>(reader).unwrap();
2424
2425            Table::builder()
2426                .metadata(resp)
2427                .metadata_location("s3://bucket/test/location/metadata/00000-8a62c37d-4573-4021-952a-c0baef7d21d0.metadata.json")
2428                .identifier(TableIdent::from_strs(["ns1", "test1"]).unwrap())
2429                .file_io(FileIO::new_with_memory())
2430                .runtime(test_runtime())
2431                .build()
2432                .unwrap()
2433        };
2434
2435        let updates = vec![
2436            TableUpdate::SetLocation {
2437                location: "s3://bucket/test/new_location/data".to_string(),
2438            },
2439            TableUpdate::SetProperties {
2440                updates: vec![
2441                    ("prop1".to_string(), "v1".to_string()),
2442                    ("prop2".to_string(), "v2".to_string()),
2443                ]
2444                .into_iter()
2445                .collect(),
2446            },
2447        ];
2448
2449        let requirements = vec![TableRequirement::UuidMatch {
2450            uuid: table.metadata().table_uuid,
2451        }];
2452
2453        let table_commit = TableCommit::builder()
2454            .ident(table.identifier().to_owned())
2455            .updates(updates)
2456            .requirements(requirements)
2457            .build();
2458
2459        let updated_table = table_commit.apply(table).unwrap();
2460
2461        assert_eq!(
2462            updated_table.metadata().properties.get("prop1").unwrap(),
2463            "v1"
2464        );
2465        assert_eq!(
2466            updated_table.metadata().properties.get("prop2").unwrap(),
2467            "v2"
2468        );
2469
2470        // metadata version should be bumped, and the metadata directory should be
2471        // re-derived from the updated table location
2472        assert!(
2473            updated_table
2474                .metadata_location()
2475                .unwrap()
2476                .starts_with("s3://bucket/test/new_location/data/metadata/00001-")
2477        );
2478
2479        assert_eq!(
2480            updated_table.metadata().location,
2481            "s3://bucket/test/new_location/data",
2482        );
2483    }
2484}