From b9cf979f0aad533b6773e2f2a4dfca5fb45406f8 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Thu, 27 Aug 2026 17:37:58 +0200 Subject: [PATCH 1/2] Give `ColumnDesc` and `DynColumnDesc` the usual derives MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `DynColumnDesc` derives `Clone, Copy, Debug, Eq, PartialEq`. `ColumnDesc` gets them hand-written: `#[derive]` would put each trait's bound on `L`, but a descriptor holds no `L` — only a `PhantomData L>`, which is `Copy`, `Eq`, and `Debug` whatever `L` is. `Debug` skips the `PhantomData`. `to_dyn` now takes `self` by value, since `Self: Copy`. Co-Authored-By: Claude Opus 5 (1M context) --- crates/quiver_types/src/column_desc.rs | 44 +++++++++++++++++++++++++- 1 file changed, 43 insertions(+), 1 deletion(-) 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, From ee7e3f0c39c472272adff3138b1e5144a85b2b11 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Thu, 27 Aug 2026 17:37:58 +0200 Subject: [PATCH 2/2] Test the `ColumnDesc` / `DynColumnDesc` derives Uses a `ColumnDesc` for the `Debug` and `Eq` checks: `Utf8` itself derives nothing, so those lines only compile because the impls put no bound on `L`. Co-Authored-By: Claude Opus 5 (1M context) --- crates/quiver/tests/quiver.rs | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) 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 {