Skip to content

Commit 5971b06

Browse files
emilkclaude
andcommitted
Add AnyBinary: one logical type for any binary encoding
Mirrors `AnyList`: `Column<AnyBinary>` accepts any of arrow's byte-string encodings — `Binary`, `LargeBinary`, `BinaryView`, or `FixedSizeBinary` (any size) — and reads them all uniformly as `&[u8]` (a `FixedSizeBinary<N>`'s fixed-width elements are seen as plain `&[u8]` slices, not `&[u8; N]`). Like `AnyList` it is parse-only: implements `LogicalType` but not `ConcreteType`, so no `from_values`/`Default`/schema — build a concrete encoding such as `Column<Binary>`. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 3e2e02c commit 5971b06

4 files changed

Lines changed: 161 additions & 1 deletion

File tree

README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,7 @@ The supported logical types:
197197
| `Utf8`, `LargeUtf8`, `Utf8View` | The same | `&str` |
198198
| `FixedSizeBinary<N>` | `FixedSizeBinary(N)` | `&[u8; N]` |
199199
| `Binary`, `LargeBinary`, `BinaryView` | The same | `&[u8]` |
200+
| `AnyBinary` | *any* binary encoding (incl. `FixedSizeBinary`) | `&[u8]` |
200201
| `Date32`, `Date64` | `Date32`, `Date64` | `i32` days / `i64` ms |
201202
| `Time32Second``Time64Nanosecond` | `Time32(…)`, `Time64(…)` | `i32` / `i64` |
202203
| `TimestampNanosecond<Utc>` | `Timestamp(Nanosecond, UTC)` | `i64` |
@@ -240,6 +241,10 @@ Because it has no single arrow datatype, `AnyList` is **parse-only**: it impleme
240241
`datatype()`, `from_values`, `Default`, or schema generation. To *build* a column,
241242
pick a concrete encoding such as `Column<List<L>>`.
242243

244+
`AnyBinary` is the same idea for byte strings: it accepts any of `Binary`,
245+
`LargeBinary`, `BinaryView`, or `FixedSizeBinary` (any size) and reads them all
246+
as `&[u8]`. Also parse-only.
247+
243248
### What is *not* supported
244249

245250
These datatypes have no logical type yet, so there is no `Column<L>` for them:

crates/quiver/tests/column.rs

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -622,6 +622,58 @@ fn binary_view_columns() {
622622
assert_eq!(column.to_vec(), [short, long]);
623623
}
624624

625+
#[test]
626+
fn any_binary_columns() {
627+
use quiver::arrow::array::{BinaryViewArray, FixedSizeBinaryArray, LargeBinaryArray};
628+
use quiver::{AnyBinary, Binary, BinaryView, FixedSizeBinary, LargeBinary};
629+
630+
// `try_from` accepts every byte-string encoding, read uniformly as `&[u8]`:
631+
let encodings = [
632+
Column::<Binary>::from_values([b"ab".to_vec(), vec![3_u8, 4]]).into_arrow(),
633+
Column::<LargeBinary>::from_values([b"ab".to_vec(), vec![3_u8, 4]]).into_arrow(),
634+
Column::<BinaryView>::from_values([b"ab".to_vec(), vec![3_u8, 4]]).into_arrow(),
635+
// FixedSizeBinary too (any size) — its `&[u8; N]` reads here as `&[u8]`:
636+
Column::<FixedSizeBinary<2>>::from_values([[b'a', b'b'], [3, 4]]).into_arrow(),
637+
];
638+
for array in encodings {
639+
let column = Column::<AnyBinary>::try_from(array).unwrap();
640+
assert_eq!(column.value(0), b"ab");
641+
assert_eq!(&column[1], &[3_u8, 4]); // `RefType` indexing
642+
assert_eq!(column.to_vec(), [b"ab".to_vec(), vec![3, 4]]);
643+
}
644+
645+
// A non-binary array is rejected:
646+
let ints = Column::<i64>::from_values([1, 2]).into_arrow();
647+
assert!(matches!(
648+
Column::<AnyBinary>::try_from(ints),
649+
Err(ColumnError::WrongDatatype { .. })
650+
));
651+
652+
// Nullable rows via the column-level `Option`:
653+
let array = LargeBinaryArray::from_iter([Some(b"x".as_slice()), None]);
654+
let column = Column::<Option<AnyBinary>>::try_from(Arc::new(array) as ArrayRef).unwrap();
655+
let values: Vec<Option<&[u8]>> = column.iter().collect();
656+
assert_eq!(values, [Some(b"x".as_slice()), None]);
657+
658+
// A null at a non-nullable level is rejected:
659+
let array = BinaryViewArray::from_iter([Some(b"x".as_slice()), None]);
660+
assert!(matches!(
661+
Column::<AnyBinary>::try_from(Arc::new(array) as ArrayRef),
662+
Err(ColumnError::UnexpectedNulls { null_count: 1 })
663+
));
664+
665+
// A FixedSizeBinary with a null is also rejected when non-nullable:
666+
let array = FixedSizeBinaryArray::try_from_sparse_iter_with_size(
667+
[Some([1_u8, 2]), None].into_iter(),
668+
2,
669+
)
670+
.unwrap();
671+
assert!(matches!(
672+
Column::<AnyBinary>::try_from(Arc::new(array) as ArrayRef),
673+
Err(ColumnError::UnexpectedNulls { null_count: 1 })
674+
));
675+
}
676+
625677
#[test]
626678
fn f16_column() {
627679
use quiver::half::f16;

crates/quiver_types/src/binary.rs

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@
99
//! [`arrow::array::BinaryViewArray`] for the newer "view" encoding
1010
//! ([`DataType::BinaryView`]), optimized for comparisons and out-of-order writes.
1111
//! Reading is zero-copy: the element values are `&[u8]`.
12+
//!
13+
//! [`AnyBinary`] accepts *any* binary encoding — these three plus
14+
//! [`FixedSizeBinary`](crate::FixedSizeBinary) — all read as `&[u8]`, for when
15+
//! the encoding is decided at runtime.
1216
1317
use arrow::array::{Array, ArrayRef};
1418
use arrow::datatypes::DataType;
@@ -116,3 +120,102 @@ impl_binary_datatype!(
116120
arrow::array::BinaryViewArray,
117121
DataType::BinaryView
118122
);
123+
124+
/// Marker for a binary column in *any* of arrow's byte-string encodings.
125+
///
126+
/// Accepts [`Binary`], [`LargeBinary`], [`BinaryView`], or
127+
/// [`FixedSizeBinary`](crate::FixedSizeBinary) (of any size). They all read as
128+
/// `&[u8]` — a `FixedSizeBinary<N>`'s fixed-width elements are seen here as
129+
/// plain `&[u8]` slices (length `N`), not `&[u8; N]`.
130+
///
131+
/// Like [`AnyList`](crate::AnyList), this is a quiver-only logical type with no
132+
/// single arrow datatype: `Column<AnyBinary>` accepts whichever encoding it is
133+
/// handed and reads them all uniformly. It is *parse-only* — it implements
134+
/// [`LogicalType`] (so `try_from`/reading work) but not
135+
/// [`ConcreteType`](crate::ConcreteType), so there is no `from_values`/`Default`/
136+
/// schema; to build, pick a concrete encoding such as `Column<Binary>`.
137+
///
138+
/// ```
139+
/// use quiver::{AnyBinary, Column};
140+
/// use quiver::arrow::array::{ArrayRef, LargeBinaryArray};
141+
/// # use std::sync::Arc;
142+
///
143+
/// // `array` may be a Binary / LargeBinary / BinaryView:
144+
/// let array: ArrayRef = Arc::new(LargeBinaryArray::from_iter_values([b"abc"]));
145+
/// let column = Column::<AnyBinary>::try_from(array).unwrap();
146+
/// assert_eq!(column.value(0), b"abc");
147+
/// ```
148+
///
149+
/// This type is never instantiated — it only appears as a type parameter.
150+
pub struct AnyBinary;
151+
152+
/// The validated representation of an [`AnyBinary`] column: one of the
153+
/// per-encoding binary arrays.
154+
#[derive(Clone)]
155+
pub enum AnyTypedBinary {
156+
Binary(arrow::array::BinaryArray),
157+
LargeBinary(arrow::array::LargeBinaryArray),
158+
BinaryView(arrow::array::BinaryViewArray),
159+
FixedSizeBinary(arrow::array::FixedSizeBinaryArray),
160+
}
161+
162+
impl LogicalType for AnyBinary {
163+
type Typed = AnyTypedBinary;
164+
type Value<'a> = &'a [u8];
165+
type Owned = Vec<u8>;
166+
167+
fn downcast(array: &dyn Array) -> Result<Self::Typed, ColumnError> {
168+
match array.data_type() {
169+
DataType::Binary => Ok(AnyTypedBinary::Binary(downcast_array(array, || {
170+
"Binary".to_owned()
171+
})?)),
172+
DataType::LargeBinary => {
173+
Ok(AnyTypedBinary::LargeBinary(downcast_array(array, || {
174+
"LargeBinary".to_owned()
175+
})?))
176+
}
177+
DataType::BinaryView => Ok(AnyTypedBinary::BinaryView(downcast_array(array, || {
178+
"BinaryView".to_owned()
179+
})?)),
180+
DataType::FixedSizeBinary(_) => Ok(AnyTypedBinary::FixedSizeBinary(downcast_array(
181+
array,
182+
|| "FixedSizeBinary".to_owned(),
183+
)?)),
184+
actual => Err(ColumnError::WrongDatatype {
185+
expected: "a binary array (Binary/LargeBinary/BinaryView/FixedSizeBinary)"
186+
.to_owned(),
187+
actual: actual.clone(),
188+
}),
189+
}
190+
}
191+
192+
fn is_null(typed: &Self::Typed, index: usize) -> bool {
193+
match typed {
194+
AnyTypedBinary::Binary(array) => array.is_null(index),
195+
AnyTypedBinary::LargeBinary(array) => array.is_null(index),
196+
AnyTypedBinary::BinaryView(array) => array.is_null(index),
197+
AnyTypedBinary::FixedSizeBinary(array) => array.is_null(index),
198+
}
199+
}
200+
201+
fn value(typed: &Self::Typed, index: usize) -> Self::Value<'_> {
202+
match typed {
203+
AnyTypedBinary::Binary(array) => array.value(index),
204+
AnyTypedBinary::LargeBinary(array) => array.value(index),
205+
AnyTypedBinary::BinaryView(array) => array.value(index),
206+
AnyTypedBinary::FixedSizeBinary(array) => array.value(index),
207+
}
208+
}
209+
210+
fn to_owned_value(value: Self::Value<'_>) -> Self::Owned {
211+
value.to_vec()
212+
}
213+
}
214+
215+
impl RefType for AnyBinary {
216+
type Ref = [u8];
217+
218+
fn value_ref(typed: &Self::Typed, index: usize) -> &[u8] {
219+
Self::value(typed, index)
220+
}
221+
}

crates/quiver_types/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ mod timestamp;
3232
mod typed_array;
3333

3434
pub use self::any_list::{AnyList, AnyTypedList};
35-
pub use self::binary::{Binary, BinaryView, LargeBinary};
35+
pub use self::binary::{AnyBinary, AnyTypedBinary, Binary, BinaryView, LargeBinary};
3636
pub use self::column::{Column, ColumnIntoIter, ColumnIter};
3737
pub use self::column_desc::{ColumnDesc, DynColumnDesc};
3838
pub use self::datatype::{

0 commit comments

Comments
 (0)