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