diff --git a/crates/quiver/tests/quiver.rs b/crates/quiver/tests/quiver.rs index 3c0ab33..c3a27ec 100644 --- a/crates/quiver/tests/quiver.rs +++ b/crates/quiver/tests/quiver.rs @@ -826,6 +826,41 @@ fn column_desc_is_parameterized_by_the_logical_type() { assert!(MAYBE_AGE.metadata.is_empty()); } +#[test] +fn column_descs_are_copy_debug_and_comparable() { + // `Utf8` and friends derive nothing, so a descriptor over one only compiles + // here because the impls put no bound on `L`: + const NAME: quiver::ColumnDesc = quiver::ColumnDesc::new("Typed", "name"); + const OTHER: quiver::ColumnDesc> = quiver::ColumnDesc::new("Other", "maybe_age"); + assert_eq!(NAME, NAME); + assert!(format!("{NAME:?}").contains("name")); + + let desc: quiver::ColumnDesc> = Typed::COLUMN_MAYBE_AGE; + let copy = desc; // `Copy`, so `desc` stays usable below. + assert_eq!(desc, copy); + assert_eq!(desc, Typed::COLUMN_MAYBE_AGE); + + // Same name and type, different owner → not equal. + assert_ne!(desc, OTHER); + + // Declared metadata takes part in the comparison. + assert_ne!( + Annotated::COLUMN_CHUNK_ID, + quiver::ColumnDesc::>::new("Annotated", "chunk_id") + ); + + // `Debug` names the fields, and skips the `PhantomData`: + let shown = format!("{desc:?}"); + assert!(shown.contains("maybe_age"), "{shown}"); + assert!(!shown.contains("PhantomData"), "{shown}"); + + // `DynColumnDesc` gets the same treatment: + let dynamic = desc.to_dyn(); + assert_eq!(dynamic, desc.to_dyn()); + assert_ne!(dynamic, OTHER.to_dyn()); + assert!(format!("{dynamic:?}").contains("maybe_age")); +} + /// All columns required: unlike `Typed`, this gets `empty_record_batch`. #[derive(Quiver)] struct AllRequired { diff --git a/crates/quiver_types/src/column_desc.rs b/crates/quiver_types/src/column_desc.rs index 1d85916..bedc573 100644 --- a/crates/quiver_types/src/column_desc.rs +++ b/crates/quiver_types/src/column_desc.rs @@ -60,6 +60,47 @@ pub struct ColumnDesc { _marker: PhantomData L>, } +// Hand-written rather than derived: `#[derive]` would put the trait's own bound +// on `L`, but a descriptor holds no `L` — only a `PhantomData L>`, which +// is `Copy`, `Eq`, and `Debug` whatever `L` is. +impl Clone for ColumnDesc { + fn clone(&self) -> Self { + *self + } +} + +impl Copy for ColumnDesc {} + +impl std::fmt::Debug for ColumnDesc { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let Self { + record_type, + name, + metadata, + _marker, + } = self; + f.debug_struct("ColumnDesc") + .field("record_type", record_type) + .field("name", name) + .field("metadata", metadata) + .finish() + } +} + +impl PartialEq for ColumnDesc { + fn eq(&self, other: &Self) -> bool { + let Self { + record_type, + name, + metadata, + _marker, + } = self; + *record_type == other.record_type && *name == other.name && *metadata == other.metadata + } +} + +impl Eq for ColumnDesc {} + impl ColumnDesc { /// Describes the column `name` of `record_type`, which labels the errors /// (the name of the `#[derive(Quiver)]` struct, when there is one). @@ -142,7 +183,7 @@ impl ColumnDesc { /// The declared [`metadata`](ColumnDesc::metadata) is dropped, since /// [`DynColumnDesc`] does not carry any. #[must_use] - pub const fn to_dyn(&self) -> DynColumnDesc { + pub const fn to_dyn(self) -> DynColumnDesc { DynColumnDesc::new(self.record_type, self.name) } } @@ -162,6 +203,7 @@ impl From<&ColumnDesc> for DynColumnDesc { /// The untyped counterpart of [`ColumnDesc`]: it extracts a /// [`DynColumn`] (field plus array), with no datatype or /// nullability validation. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] pub struct DynColumnDesc { /// The name of the `#[derive(Quiver)]` struct, for error messages. pub record_type: &'static str,