Skip to content

Commit 52296eb

Browse files
authored
feat: Implement TableRequirement checks (#689)
* Impelment TableRequirement check * Address comments
1 parent 213f84e commit 52296eb

File tree

2 files changed

+303
-8
lines changed

2 files changed

+303
-8
lines changed

crates/iceberg/src/catalog/mod.rs

Lines changed: 302 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,8 @@ use typed_builder::TypedBuilder;
2929
use uuid::Uuid;
3030

3131
use crate::spec::{
32-
FormatVersion, Schema, Snapshot, SnapshotReference, SortOrder, TableMetadataBuilder,
33-
UnboundPartitionSpec, ViewRepresentations,
32+
FormatVersion, Schema, SchemaId, Snapshot, SnapshotReference, SortOrder, TableMetadata,
33+
TableMetadataBuilder, UnboundPartitionSpec, ViewRepresentations,
3434
};
3535
use crate::table::Table;
3636
use crate::{Error, ErrorKind, Result};
@@ -312,29 +312,29 @@ pub enum TableRequirement {
312312
LastAssignedFieldIdMatch {
313313
/// The last assigned field id of the table to assert.
314314
#[serde(rename = "last-assigned-field-id")]
315-
last_assigned_field_id: i64,
315+
last_assigned_field_id: i32,
316316
},
317317
/// The table's current schema id must match the requirement.
318318
#[serde(rename = "assert-current-schema-id")]
319319
CurrentSchemaIdMatch {
320320
/// Current schema id of the table to assert.
321321
#[serde(rename = "current-schema-id")]
322-
current_schema_id: i64,
322+
current_schema_id: SchemaId,
323323
},
324324
/// The table's last assigned partition id must match the
325325
/// requirement.
326326
#[serde(rename = "assert-last-assigned-partition-id")]
327327
LastAssignedPartitionIdMatch {
328328
/// Last assigned partition id of the table to assert.
329329
#[serde(rename = "last-assigned-partition-id")]
330-
last_assigned_partition_id: i64,
330+
last_assigned_partition_id: i32,
331331
},
332332
/// The table's default spec id must match the requirement.
333333
#[serde(rename = "assert-default-spec-id")]
334334
DefaultSpecIdMatch {
335335
/// Default spec id of the table to assert.
336336
#[serde(rename = "default-spec-id")]
337-
default_spec_id: i64,
337+
default_spec_id: i32,
338338
},
339339
/// The table's default sort order id must match the requirement.
340340
#[serde(rename = "assert-default-sort-order-id")]
@@ -453,6 +453,140 @@ impl TableUpdate {
453453
}
454454
}
455455

456+
impl TableRequirement {
457+
/// Check that the requirement is met by the table metadata.
458+
/// If the requirement is not met, an appropriate error is returned.
459+
///
460+
/// Provide metadata as `None` if the table does not exist.
461+
pub fn check(&self, metadata: Option<&TableMetadata>) -> Result<()> {
462+
if let Some(metadata) = metadata {
463+
match self {
464+
TableRequirement::NotExist => {
465+
return Err(Error::new(
466+
ErrorKind::DataInvalid,
467+
format!(
468+
"Requirement failed: Table with id {} already exists",
469+
metadata.uuid()
470+
),
471+
));
472+
}
473+
TableRequirement::UuidMatch { uuid } => {
474+
if &metadata.uuid() != uuid {
475+
return Err(Error::new(
476+
ErrorKind::DataInvalid,
477+
"Requirement failed: Table UUID does not match",
478+
)
479+
.with_context("expected", *uuid)
480+
.with_context("found", metadata.uuid()));
481+
}
482+
}
483+
TableRequirement::CurrentSchemaIdMatch { current_schema_id } => {
484+
// ToDo: Harmonize the types of current_schema_id
485+
if metadata.current_schema_id != *current_schema_id {
486+
return Err(Error::new(
487+
ErrorKind::DataInvalid,
488+
"Requirement failed: Current schema id does not match",
489+
)
490+
.with_context("expected", current_schema_id.to_string())
491+
.with_context("found", metadata.current_schema_id.to_string()));
492+
}
493+
}
494+
TableRequirement::DefaultSortOrderIdMatch {
495+
default_sort_order_id,
496+
} => {
497+
if metadata.default_sort_order().order_id != *default_sort_order_id {
498+
return Err(Error::new(
499+
ErrorKind::DataInvalid,
500+
"Requirement failed: Default sort order id does not match",
501+
)
502+
.with_context("expected", default_sort_order_id.to_string())
503+
.with_context(
504+
"found",
505+
metadata.default_sort_order().order_id.to_string(),
506+
));
507+
}
508+
}
509+
TableRequirement::RefSnapshotIdMatch { r#ref, snapshot_id } => {
510+
let snapshot_ref = metadata.snapshot_for_ref(r#ref);
511+
if let Some(snapshot_id) = snapshot_id {
512+
let snapshot_ref = snapshot_ref.ok_or(Error::new(
513+
ErrorKind::DataInvalid,
514+
format!("Requirement failed: Branch or tag `{}` not found", r#ref),
515+
))?;
516+
if snapshot_ref.snapshot_id() != *snapshot_id {
517+
return Err(Error::new(
518+
ErrorKind::DataInvalid,
519+
format!(
520+
"Requirement failed: Branch or tag `{}`'s snapshot has changed",
521+
r#ref
522+
),
523+
)
524+
.with_context("expected", snapshot_id.to_string())
525+
.with_context("found", snapshot_ref.snapshot_id().to_string()));
526+
}
527+
} else if snapshot_ref.is_some() {
528+
// a null snapshot ID means the ref should not exist already
529+
return Err(Error::new(
530+
ErrorKind::DataInvalid,
531+
format!(
532+
"Requirement failed: Branch or tag `{}` already exists",
533+
r#ref
534+
),
535+
));
536+
}
537+
}
538+
TableRequirement::DefaultSpecIdMatch { default_spec_id } => {
539+
// ToDo: Harmonize the types of default_spec_id
540+
if metadata.default_partition_spec_id() != *default_spec_id {
541+
return Err(Error::new(
542+
ErrorKind::DataInvalid,
543+
"Requirement failed: Default partition spec id does not match",
544+
)
545+
.with_context("expected", default_spec_id.to_string())
546+
.with_context("found", metadata.default_partition_spec_id().to_string()));
547+
}
548+
}
549+
TableRequirement::LastAssignedPartitionIdMatch {
550+
last_assigned_partition_id,
551+
} => {
552+
if metadata.last_partition_id != *last_assigned_partition_id {
553+
return Err(Error::new(
554+
ErrorKind::DataInvalid,
555+
"Requirement failed: Last assigned partition id does not match",
556+
)
557+
.with_context("expected", last_assigned_partition_id.to_string())
558+
.with_context("found", metadata.last_partition_id.to_string()));
559+
}
560+
}
561+
TableRequirement::LastAssignedFieldIdMatch {
562+
last_assigned_field_id,
563+
} => {
564+
if &metadata.last_column_id != last_assigned_field_id {
565+
return Err(Error::new(
566+
ErrorKind::DataInvalid,
567+
"Requirement failed: Last assigned field id does not match",
568+
)
569+
.with_context("expected", last_assigned_field_id.to_string())
570+
.with_context("found", metadata.last_column_id.to_string()));
571+
}
572+
}
573+
};
574+
} else {
575+
match self {
576+
TableRequirement::NotExist => {}
577+
_ => {
578+
return Err(Error::new(
579+
ErrorKind::DataInvalid,
580+
"Requirement failed: Table does not exist",
581+
));
582+
}
583+
}
584+
}
585+
586+
Ok(())
587+
}
588+
}
589+
456590
pub(super) mod _serde {
457591
use serde::{Deserialize as _, Deserializer};
458592

@@ -549,7 +683,7 @@ mod tests {
549683
use crate::spec::{
550684
FormatVersion, NestedField, NullOrder, Operation, PrimitiveType, Schema, Snapshot,
551685
SnapshotReference, SnapshotRetention, SortDirection, SortField, SortOrder, Summary,
552-
TableMetadataBuilder, Transform, Type, UnboundPartitionSpec,
686+
TableMetadata, TableMetadataBuilder, Transform, Type, UnboundPartitionSpec,
553687
};
554688
use crate::{NamespaceIdent, TableCreation, TableIdent, TableRequirement, TableUpdate};
555689

@@ -593,6 +727,167 @@ mod tests {
593727
);
594728
}
595729

730+
fn metadata() -> TableMetadata {
731+
let tbl_creation = TableCreation::builder()
732+
.name("table".to_string())
733+
.location("/path/to/table".to_string())
734+
.schema(Schema::builder().build().unwrap())
735+
.build();
736+
737+
TableMetadataBuilder::from_table_creation(tbl_creation)
738+
.unwrap()
739+
.assign_uuid(uuid::Uuid::nil())
740+
.unwrap()
741+
.build()
742+
.unwrap()
743+
}
744+
745+
#[test]
746+
fn test_check_requirement_not_exist() {
747+
let metadata = metadata();
748+
let requirement = TableRequirement::NotExist;
749+
750+
assert!(requirement.check(Some(&metadata)).is_err());
751+
assert!(requirement.check(None).is_ok());
752+
}
753+
754+
#[test]
755+
fn test_check_table_uuid() {
756+
let metadata = metadata();
757+
758+
let requirement = TableRequirement::UuidMatch {
759+
uuid: uuid::Uuid::now_v7(),
760+
};
761+
assert!(requirement.check(Some(&metadata)).is_err());
762+
763+
let requirement = TableRequirement::UuidMatch {
764+
uuid: uuid::Uuid::nil(),
765+
};
766+
assert!(requirement.check(Some(&metadata)).is_ok());
767+
}
768+
769+
#[test]
770+
fn test_check_ref_snapshot_id() {
771+
let metadata = metadata();
772+
773+
// Ref does not exist but should
774+
let requirement = TableRequirement::RefSnapshotIdMatch {
775+
r#ref: "my_branch".to_string(),
776+
snapshot_id: Some(1),
777+
};
778+
assert!(requirement.check(Some(&metadata)).is_err());
779+
780+
// Ref does not exist and should not
781+
let requirement = TableRequirement::RefSnapshotIdMatch {
782+
r#ref: "my_branch".to_string(),
783+
snapshot_id: None,
784+
};
785+
assert!(requirement.check(Some(&metadata)).is_ok());
786+
787+
// Add snapshot
788+
let record = r#"
789+
{
790+
"snapshot-id": 3051729675574597004,
791+
"sequence-number": 10,
792+
"timestamp-ms": 1515100955770,
793+
"summary": {
794+
"operation": "append"
795+
},
796+
"manifest-list": "s3://b/wh/.../s1.avro",
797+
"schema-id": 0
798+
}
799+
"#;
800+
801+
let snapshot = serde_json::from_str::<Snapshot>(record).unwrap();
802+
let mut metadata = metadata;
803+
metadata.append_snapshot(snapshot);
804+
805+
// Ref exists and should matches
806+
let requirement = TableRequirement::RefSnapshotIdMatch {
807+
r#ref: "main".to_string(),
808+
snapshot_id: Some(3051729675574597004),
809+
};
810+
assert!(requirement.check(Some(&metadata)).is_ok());
811+
812+
// Ref exists but does not match
813+
let requirement = TableRequirement::RefSnapshotIdMatch {
814+
r#ref: "main".to_string(),
815+
snapshot_id: Some(1),
816+
};
817+
assert!(requirement.check(Some(&metadata)).is_err());
818+
}
819+
820+
#[test]
821+
fn test_check_last_assigned_field_id() {
822+
let metadata = metadata();
823+
824+
let requirement = TableRequirement::LastAssignedFieldIdMatch {
825+
last_assigned_field_id: 1,
826+
};
827+
assert!(requirement.check(Some(&metadata)).is_err());
828+
829+
let requirement = TableRequirement::LastAssignedFieldIdMatch {
830+
last_assigned_field_id: 0,
831+
};
832+
assert!(requirement.check(Some(&metadata)).is_ok());
833+
}
834+
835+
#[test]
836+
fn test_check_current_schema_id() {
837+
let metadata = metadata();
838+
839+
let requirement = TableRequirement::CurrentSchemaIdMatch {
840+
current_schema_id: 1,
841+
};
842+
assert!(requirement.check(Some(&metadata)).is_err());
843+
844+
let requirement = TableRequirement::CurrentSchemaIdMatch {
845+
current_schema_id: 0,
846+
};
847+
assert!(requirement.check(Some(&metadata)).is_ok());
848+
}
849+
850+
#[test]
851+
fn test_check_last_assigned_partition_id() {
852+
let metadata = metadata();
853+
854+
let requirement = TableRequirement::LastAssignedPartitionIdMatch {
855+
last_assigned_partition_id: 1,
856+
};
857+
assert!(requirement.check(Some(&metadata)).is_err());
858+
859+
let requirement = TableRequirement::LastAssignedPartitionIdMatch {
860+
last_assigned_partition_id: 0,
861+
};
862+
assert!(requirement.check(Some(&metadata)).is_ok());
863+
}
864+
865+
#[test]
866+
fn test_check_default_spec_id() {
867+
let metadata = metadata();
868+
869+
let requirement = TableRequirement::DefaultSpecIdMatch { default_spec_id: 1 };
870+
assert!(requirement.check(Some(&metadata)).is_err());
871+
872+
let requirement = TableRequirement::DefaultSpecIdMatch { default_spec_id: 0 };
873+
assert!(requirement.check(Some(&metadata)).is_ok());
874+
}
875+
876+
#[test]
877+
fn test_check_default_sort_order_id() {
878+
let metadata = metadata();
879+
880+
let requirement = TableRequirement::DefaultSortOrderIdMatch {
881+
default_sort_order_id: 1,
882+
};
883+
assert!(requirement.check(Some(&metadata)).is_err());
884+
885+
let requirement = TableRequirement::DefaultSortOrderIdMatch {
886+
default_sort_order_id: 0,
887+
};
888+
assert!(requirement.check(Some(&metadata)).is_ok());
889+
}
890+
596891
#[test]
597892
fn test_table_uuid() {
598893
test_serde_json(

crates/iceberg/src/transaction.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,7 @@ impl<'a> ReplaceSortOrderAction<'a> {
154154

155155
let requirements = vec![
156156
TableRequirement::CurrentSchemaIdMatch {
157-
current_schema_id: self.tx.table.metadata().current_schema().schema_id() as i64,
157+
current_schema_id: self.tx.table.metadata().current_schema().schema_id(),
158158
},
159159
TableRequirement::DefaultSortOrderIdMatch {
160160
default_sort_order_id: self.tx.table.metadata().default_sort_order().order_id,

0 commit comments

Comments
 (0)