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