Skip to content

Commit 946c95f

Browse files
emilkclaude
andauthored
Add AnyUtf8: one logical type for any UTF-8 encoding (#10)
* Add `AnyUtf8`: one logical type for any UTF-8 encoding The string sibling of `AnyBinary`/`AnyList`: `Column<AnyUtf8>` accepts any of arrow's UTF-8 encodings — `Utf8`, `LargeUtf8`, or `Utf8View` — and reads them all uniformly as `&str`. Parse-only: implements `LogicalType` (and `RefType`) but not `ConcreteType`, so no `from_values`/`Default`/schema; build a concrete encoding such as `Column<Utf8>`. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Improve readme --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent e57d395 commit 946c95f

4 files changed

Lines changed: 134 additions & 4 deletions

File tree

README.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,7 @@ The supported logical types:
195195
|----------------------------------------------|------------------------------|---------------------------|
196196
| `bool`, `i8``i64`, `u8``u64`, `f16``f64` | The same | By value |
197197
| `Utf8`, `LargeUtf8`, `Utf8View` | The same | `&str` |
198+
| `AnyUtf8` | *any* UTF-8 encoding above | `&str` |
198199
| `FixedSizeBinary<N>` | `FixedSizeBinary(N)` | `&[u8; N]` |
199200
| `Binary`, `LargeBinary`, `BinaryView` | The same | `&[u8]` |
200201
| `AnyBinary` | *any* binary encoding (incl. `FixedSizeBinary`) | `&[u8]` |
@@ -211,7 +212,7 @@ The supported logical types:
211212
| `Map<K, V>` | `Map(…)`, recursively | An iterator over `(key, value)` pairs |
212213
| `Option<L>` | Nullable at this level | `Option<…>` |
213214

214-
### `AnyList<L>`: one logical type for any list encoding
215+
### Semi-dynamic logical types
215216

216217
Arrow has five physically different ways to store the same logical thing — a
217218
column of lists of `L`: `List`, `LargeList`, `ListView`, `LargeListView`, and
@@ -243,7 +244,8 @@ pick a concrete encoding such as `Column<List<L>>`.
243244

244245
`AnyBinary` is the same idea for byte strings: it accepts any of `Binary`,
245246
`LargeBinary`, `BinaryView`, or `FixedSizeBinary` (any size) and reads them all
246-
as `&[u8]`. Also parse-only.
247+
as `&[u8]`. `AnyUtf8` likewise accepts any of `Utf8`, `LargeUtf8`, or `Utf8View`
248+
and reads them as `&str`. Both are also parse-only.
247249

248250
### What is *not* supported
249251

crates/quiver/tests/column.rs

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -674,6 +674,45 @@ fn any_binary_columns() {
674674
));
675675
}
676676

677+
#[test]
678+
fn any_utf8_columns() {
679+
use quiver::arrow::array::{LargeStringArray, StringViewArray};
680+
use quiver::{AnyUtf8, LargeUtf8, Utf8View};
681+
682+
// `try_from` accepts every string encoding, read uniformly as `&str`:
683+
let encodings = [
684+
Column::<Utf8>::from_values(["alice", "bob"]).into_arrow(),
685+
Column::<LargeUtf8>::from_values(["alice", "bob"]).into_arrow(),
686+
Column::<Utf8View>::from_values(["alice", "bob"]).into_arrow(),
687+
];
688+
for array in encodings {
689+
let column = Column::<AnyUtf8>::try_from(array).unwrap();
690+
assert_eq!(column.value(0), "alice");
691+
assert_eq!(&column[1], "bob"); // `RefType` indexing
692+
assert_eq!(column.to_vec(), ["alice", "bob"]);
693+
}
694+
695+
// A non-string array is rejected:
696+
let ints = Column::<i64>::from_values([1, 2]).into_arrow();
697+
assert!(matches!(
698+
Column::<AnyUtf8>::try_from(ints),
699+
Err(ColumnError::WrongDatatype { .. })
700+
));
701+
702+
// Nullable rows via the column-level `Option`:
703+
let array = LargeStringArray::from(vec![Some("x"), None]);
704+
let column = Column::<Option<AnyUtf8>>::try_from(Arc::new(array) as ArrayRef).unwrap();
705+
let values: Vec<Option<&str>> = column.iter().collect();
706+
assert_eq!(values, [Some("x"), None]);
707+
708+
// A null at a non-nullable level is rejected:
709+
let array = StringViewArray::from(vec![Some("x"), None]);
710+
assert!(matches!(
711+
Column::<AnyUtf8>::try_from(Arc::new(array) as ArrayRef),
712+
Err(ColumnError::UnexpectedNulls { null_count: 1 })
713+
));
714+
}
715+
677716
#[test]
678717
fn f16_column() {
679718
use quiver::half::f16;

crates/quiver_types/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ pub use self::list_view::{LargeListView, ListView, TypedLargeListView, TypedList
5252
pub use self::map::{Map, MapValue, TypedMap};
5353
pub use self::newtype::As;
5454
pub use self::run::{Run, RunEndType, TypedRun};
55-
pub use self::string::{LargeUtf8, Utf8, Utf8View};
55+
pub use self::string::{AnyTypedUtf8, AnyUtf8, LargeUtf8, Utf8, Utf8View};
5656
pub use self::time::{Time32Millisecond, Time32Second, Time64Microsecond, Time64Nanosecond};
5757
pub use self::timestamp::{
5858
Microsecond, Millisecond, Nanosecond, NoTimezone, Second, TimeUnitSpec, Timestamp,

crates/quiver_types/src/string.rs

Lines changed: 90 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,14 @@
1111
//!
1212
//! All three are markers: the owned values are `String`s, and reading is
1313
//! zero-copy — the element values are `&str` borrows into the array.
14+
//!
15+
//! [`AnyUtf8`] accepts *any* of those three encodings (they all read as `&str`),
16+
//! for when the encoding is decided at runtime.
1417
1518
use arrow::array::{Array, ArrayRef};
1619
use arrow::datatypes::DataType;
1720

18-
use crate::datatype::{ColumnError, LogicalType, RefType, impl_marker_datatype};
21+
use crate::datatype::{ColumnError, LogicalType, RefType, downcast_array, impl_marker_datatype};
1922

2023
/// UTF-8 text: an arrow [`DataType::Utf8`] column with `String` values.
2124
///
@@ -103,3 +106,89 @@ impl RefType for Utf8View {
103106
typed.value(index)
104107
}
105108
}
109+
110+
/// Marker for a UTF-8 column in *any* of arrow's string encodings.
111+
///
112+
/// Accepts [`Utf8`], [`LargeUtf8`], or [`Utf8View`] — they all read as `&str`.
113+
///
114+
/// Like [`AnyList`](crate::AnyList), this is a quiver-only logical type with no
115+
/// single arrow datatype: `Column<AnyUtf8>` accepts whichever encoding it is
116+
/// handed and reads them all uniformly. It is *parse-only* — it implements
117+
/// [`LogicalType`] (so `try_from`/reading work) but not
118+
/// [`ConcreteType`](crate::ConcreteType), so there is no `from_values`/`Default`/
119+
/// schema; to build, pick a concrete encoding such as `Column<Utf8>`.
120+
///
121+
/// ```
122+
/// use quiver::{AnyUtf8, Column};
123+
/// use quiver::arrow::array::{ArrayRef, LargeStringArray};
124+
/// # use std::sync::Arc;
125+
///
126+
/// // `array` may be a Utf8 / LargeUtf8 / Utf8View:
127+
/// let array: ArrayRef = Arc::new(LargeStringArray::from(vec!["alice", "bob"]));
128+
/// let column = Column::<AnyUtf8>::try_from(array).unwrap();
129+
/// assert_eq!(column.value(0), "alice");
130+
/// ```
131+
///
132+
/// This type is never instantiated — it only appears as a type parameter.
133+
pub struct AnyUtf8;
134+
135+
/// The validated representation of an [`AnyUtf8`] column: one of the
136+
/// per-encoding string arrays.
137+
#[derive(Clone)]
138+
pub enum AnyTypedUtf8 {
139+
Utf8(arrow::array::StringArray),
140+
LargeUtf8(arrow::array::LargeStringArray),
141+
Utf8View(arrow::array::StringViewArray),
142+
}
143+
144+
impl LogicalType for AnyUtf8 {
145+
type Typed = AnyTypedUtf8;
146+
type Value<'a> = &'a str;
147+
type Owned = String;
148+
149+
fn downcast(array: &dyn Array) -> Result<Self::Typed, ColumnError> {
150+
match array.data_type() {
151+
DataType::Utf8 => Ok(AnyTypedUtf8::Utf8(downcast_array(array, || {
152+
"Utf8".to_owned()
153+
})?)),
154+
DataType::LargeUtf8 => Ok(AnyTypedUtf8::LargeUtf8(downcast_array(array, || {
155+
"LargeUtf8".to_owned()
156+
})?)),
157+
DataType::Utf8View => Ok(AnyTypedUtf8::Utf8View(downcast_array(array, || {
158+
"Utf8View".to_owned()
159+
})?)),
160+
actual => Err(ColumnError::WrongDatatype {
161+
expected: "a string array (Utf8/LargeUtf8/Utf8View)".to_owned(),
162+
actual: actual.clone(),
163+
}),
164+
}
165+
}
166+
167+
fn is_null(typed: &Self::Typed, index: usize) -> bool {
168+
match typed {
169+
AnyTypedUtf8::Utf8(array) => array.is_null(index),
170+
AnyTypedUtf8::LargeUtf8(array) => array.is_null(index),
171+
AnyTypedUtf8::Utf8View(array) => array.is_null(index),
172+
}
173+
}
174+
175+
fn value(typed: &Self::Typed, index: usize) -> Self::Value<'_> {
176+
match typed {
177+
AnyTypedUtf8::Utf8(array) => array.value(index),
178+
AnyTypedUtf8::LargeUtf8(array) => array.value(index),
179+
AnyTypedUtf8::Utf8View(array) => array.value(index),
180+
}
181+
}
182+
183+
fn to_owned_value(value: Self::Value<'_>) -> Self::Owned {
184+
value.to_owned()
185+
}
186+
}
187+
188+
impl RefType for AnyUtf8 {
189+
type Ref = str;
190+
191+
fn value_ref(typed: &Self::Typed, index: usize) -> &str {
192+
Self::value(typed, index)
193+
}
194+
}

0 commit comments

Comments
 (0)