Skip to content

Commit 0931c65

Browse files
emilkclaude
andauthored
Give ListValue a Column-like read API (#11)
* Give `ListValue` a `Column`-like read API `ListValue` (one element of a list column) was iterator-only. It now mirrors `Column`'s read API so a list element behaves like a borrowed, single-typed column: - `len` / `is_empty` - `get` / `value` (zero-copy views) and `get_owned` / `value_owned` - `iter` (non-consuming) / `iter_owned` / `to_vec` - `as_slice` for primitive items (contiguous, zero-copy) - `list[i]` indexing for borrowable items It stays an `Iterator` (so `.map`/`.collect`/`.sum` keep working) and is now `Copy`; the random-access methods operate on the remaining items. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Make `ListValue` iteration fast The items sit in `index..end`, all in bounds, so override the `Iterator` combinators (`count`, `last`, `nth`, `fold`) and add `DoubleEndedIterator` (`next_back`, `nth_back`, `rfold`) plus `FusedIterator`. These give `sum`/`for_each`/`collect`/`skip`/`rev` tight loops instead of the default per-element `Option` plumbing. Primitive items keep the even-faster zero-overhead path via `as_slice`. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 946c95f commit 0931c65

2 files changed

Lines changed: 330 additions & 3 deletions

File tree

crates/quiver/tests/column.rs

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1519,3 +1519,88 @@ fn utf8_string_encodings() {
15191519
let values: Vec<Option<&str>> = nullable.iter().collect();
15201520
assert_eq!(values, [Some("a"), None]);
15211521
}
1522+
1523+
#[test]
1524+
fn list_value_column_like_api() {
1525+
// A `ListValue` (one list element) mirrors `Column`'s read API.
1526+
let column = Column::<List<i64>>::from_values([vec![10, 20, 30], vec![]]);
1527+
1528+
let first = column.value(0);
1529+
assert_eq!(first.len(), 3);
1530+
assert!(!first.is_empty());
1531+
1532+
// Random access by item index:
1533+
assert_eq!(first.value(0), 10);
1534+
assert_eq!(first.value(2), 30);
1535+
assert_eq!(first.get(1), Some(20));
1536+
assert_eq!(first.get(3), None);
1537+
1538+
// `list[i]` borrows from the array (primitive items):
1539+
assert_eq!(first[1], 20);
1540+
1541+
// Bulk zero-copy slice, and owned copies:
1542+
assert_eq!(first.as_slice(), &[10, 20, 30]);
1543+
assert_eq!(first.to_vec(), vec![10, 20, 30]);
1544+
1545+
// `iter` does not consume the view; the struct is `Copy`:
1546+
let sum: i64 = first.iter().sum();
1547+
assert_eq!(sum, 60);
1548+
let sum_again: i64 = first.iter().sum();
1549+
assert_eq!(sum_again, 60);
1550+
1551+
// Iterating still works directly (it is an `Iterator`):
1552+
let collected: Vec<i64> = first.collect();
1553+
assert_eq!(collected, [10, 20, 30]);
1554+
1555+
// Overridden combinators behave like the defaults:
1556+
assert_eq!(first.iter().count(), 3);
1557+
assert_eq!(first.iter().last(), Some(30));
1558+
assert_eq!(first.iter().nth(1), Some(20));
1559+
assert_eq!(first.iter().nth(3), None);
1560+
assert_eq!(
1561+
first
1562+
.iter()
1563+
.fold(String::new(), |acc, x| format!("{acc}{x}")),
1564+
"102030"
1565+
);
1566+
1567+
// Double-ended: `rev`, `next_back`, `nth_back`, `rfold`:
1568+
let rev: Vec<i64> = first.iter().rev().collect();
1569+
assert_eq!(rev, [30, 20, 10]);
1570+
let mut cursor = first.iter();
1571+
assert_eq!(cursor.next_back(), Some(30));
1572+
assert_eq!(cursor.next(), Some(10));
1573+
assert_eq!(cursor.next_back(), Some(20));
1574+
assert_eq!(cursor.next_back(), None);
1575+
assert_eq!(first.iter().nth_back(1), Some(20));
1576+
assert_eq!(first.iter().nth_back(3), None);
1577+
assert_eq!(
1578+
first
1579+
.iter()
1580+
.rfold(String::new(), |acc, x| format!("{acc}{x}")),
1581+
"302010"
1582+
);
1583+
1584+
// Empty element:
1585+
let second = column.value(1);
1586+
assert!(second.is_empty());
1587+
assert_eq!(second.len(), 0);
1588+
assert_eq!(second.get(0), None);
1589+
assert_eq!(second.as_slice(), &[] as &[i64]);
1590+
1591+
// String items: owned access and indexing.
1592+
let strings = Column::<List<Utf8>>::from_values([vec!["a".to_owned(), "b".to_owned()]]);
1593+
let row = strings.value(0);
1594+
assert_eq!(&row[0], "a");
1595+
assert_eq!(row.value_owned(1), "b".to_owned());
1596+
assert_eq!(row.get_owned(0), Some("a".to_owned()));
1597+
assert_eq!(row.to_vec(), vec!["a".to_owned(), "b".to_owned()]);
1598+
}
1599+
1600+
#[test]
1601+
#[should_panic(expected = "out of bounds for length 2")]
1602+
fn list_value_index_out_of_bounds() {
1603+
let column = Column::<List<i64>>::from_values([vec![1, 2]]);
1604+
let value: i64 = column.value(0).value(2);
1605+
assert_eq!(value, 0); // unreachable: the line above panics
1606+
}

crates/quiver_types/src/list.rs

Lines changed: 245 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,9 @@ use arrow::array::{Array, ArrayRef, OffsetSizeTrait};
2020
use arrow::datatypes::ArrowNativeType as _;
2121
use arrow::datatypes::DataType;
2222

23-
use crate::datatype::{ColumnError, InfallibleBuild, LogicalType, downcast_array};
23+
use crate::datatype::{
24+
ColumnError, InfallibleBuild, LogicalType, PrimitiveType, RefType, downcast_array,
25+
};
2426

2527
/// Marker for an arrow `List` column with items of logical type `L`.
2628
///
@@ -155,20 +157,189 @@ pub(crate) use impl_list_datatype;
155157

156158
impl_list_datatype!(List, TypedList, arrow::array::ListArray, List);
157159

158-
/// One list element of a `Column<List<L>>`: an iterator over the typed items.
160+
/// One list element of a list column (`List`, [`LargeList`](crate::LargeList),
161+
/// [`FixedSizeList`](crate::FixedSizeList), …): a zero-copy, random-access view
162+
/// of that row's typed items.
163+
///
164+
/// It mirrors [`Column`](crate::Column)'s read API — the items behave like a
165+
/// borrowed slice of a single-typed column:
166+
///
167+
/// - length: [`len`](ListValue::len), [`is_empty`](ListValue::is_empty)
168+
/// - by-item access: [`get`](ListValue::get) / [`value`](ListValue::value)
169+
/// (zero-copy views) and [`get_owned`](ListValue::get_owned) /
170+
/// [`value_owned`](ListValue::value_owned) (owned), plus `list[i]` where the
171+
/// item can be borrowed from the array (see the [`Index`](std::ops::Index) impl)
172+
/// - bulk: [`to_vec`](ListValue::to_vec), and [`as_slice`](ListValue::as_slice)
173+
/// for primitive items (one contiguous zero-copy slice)
174+
/// - iteration: [`iter`](ListValue::iter) / [`iter_owned`](ListValue::iter_owned)
175+
///
176+
/// `ListValue` is itself an [`Iterator`] over the items, so `.map(…)` /
177+
/// `.collect()` / `.sum()` work directly on it. It is [`Copy`]: consuming it as
178+
/// an iterator advances a cursor (the random-access methods then see the
179+
/// remaining items), while [`iter`](ListValue::iter) hands out a fresh cursor
180+
/// without consuming the original.
181+
///
182+
/// ```
183+
/// use quiver::{Column, List};
184+
///
185+
/// let column = Column::<List<i64>>::from_values([vec![10, 20, 30], vec![]]);
186+
/// let row = column.value(0);
187+
///
188+
/// assert_eq!(row.len(), 3);
189+
/// assert_eq!(row.value(1), 20); // by item index
190+
/// assert_eq!(row[2], 30); // borrowed (primitive items)
191+
/// assert_eq!(row.as_slice(), &[10, 20, 30]); // contiguous, zero-copy
192+
/// assert_eq!(row.to_vec(), vec![10, 20, 30]);
193+
///
194+
/// let sum: i64 = row.iter().sum(); // `iter` does not consume `row`
195+
/// assert_eq!(sum, 60);
196+
///
197+
/// assert!(column.value(1).is_empty());
198+
/// ```
159199
pub struct ListValue<'a, L: LogicalType> {
160200
values: &'a L::Typed,
161201
index: usize,
162202
end: usize,
163203
}
164204

165-
impl<'a, L: LogicalType> ListValue<'a, L> {
205+
impl<L: LogicalType> Clone for ListValue<'_, L> {
206+
fn clone(&self) -> Self {
207+
*self
208+
}
209+
}
210+
211+
impl<L: LogicalType> Copy for ListValue<'_, L> {}
212+
213+
impl<'a, L: LogicalType + 'a> ListValue<'a, L> {
166214
/// `index..end` into `values`.
167215
pub(crate) fn new(values: &'a L::Typed, index: usize, end: usize) -> Self {
168216
Self { values, index, end }
169217
}
218+
219+
/// The number of items in this list element.
220+
#[must_use]
221+
pub fn len(&self) -> usize {
222+
self.end - self.index
223+
}
224+
225+
/// Is this list element empty?
226+
#[must_use]
227+
pub fn is_empty(&self) -> bool {
228+
self.index == self.end
229+
}
230+
231+
/// The item at `index`, or `None` if out of bounds.
232+
///
233+
/// See [`ListValue::value`] for the returned view;
234+
/// [`ListValue::get_owned`] returns the owned value instead.
235+
#[must_use]
236+
pub fn get(&self, index: usize) -> Option<L::Value<'a>> {
237+
let item = self.index.checked_add(index)?;
238+
(item < self.end).then(|| L::value(self.values, item))
239+
}
240+
241+
/// The owned item at `index`, or `None` if out of bounds —
242+
/// e.g. `String` where [`ListValue::get`] returns `&str`.
243+
#[must_use]
244+
pub fn get_owned(&self, index: usize) -> Option<L::Owned> {
245+
self.get(index).map(L::to_owned_value)
246+
}
247+
248+
/// The item at `index`, returning the zero-copy view
249+
/// ([`LogicalType::Value`]); for the owned value see [`ListValue::value_owned`].
250+
///
251+
/// Where the item can be borrowed from the array, `list[index]` works too
252+
/// (see the [`Index`](std::ops::Index) impl).
253+
///
254+
/// Panics if out of bounds.
255+
#[must_use]
256+
pub fn value(&self, index: usize) -> L::Value<'a> {
257+
assert!(
258+
index < self.len(),
259+
"ListValue index {index} out of bounds for length {}",
260+
self.len()
261+
);
262+
L::value(self.values, self.index + index)
263+
}
264+
265+
/// The owned item at `index` — e.g. `String` where [`ListValue::value`]
266+
/// returns `&str`.
267+
///
268+
/// Panics if out of bounds.
269+
#[must_use]
270+
pub fn value_owned(&self, index: usize) -> L::Owned {
271+
L::to_owned_value(self.value(index))
272+
}
273+
274+
/// Iterates over the zero-copy views ([`LogicalType::Value`]) of the
275+
/// remaining items, without consuming `self`.
276+
///
277+
/// For owned values, see [`ListValue::iter_owned`].
278+
#[must_use]
279+
pub fn iter(&self) -> Self {
280+
*self
281+
}
282+
283+
/// Iterates over the owned values of the remaining items —
284+
/// e.g. `String` where [`ListValue::iter`] yields `&str`.
285+
pub fn iter_owned(&self) -> impl Iterator<Item = L::Owned> + 'a {
286+
self.iter().map(L::to_owned_value)
287+
}
288+
289+
/// Copies the items into a `Vec` of owned values,
290+
/// e.g. `Vec<String>` for a `List<Utf8>` element.
291+
#[must_use]
292+
pub fn to_vec(self) -> Vec<L::Owned> {
293+
self.iter_owned().collect()
294+
}
170295
}
171296

297+
/// `for item in &list` — iterates the items without consuming the view.
298+
impl<'a, L: LogicalType + 'a> IntoIterator for &ListValue<'a, L> {
299+
type Item = L::Value<'a>;
300+
type IntoIter = ListValue<'a, L>;
301+
302+
fn into_iter(self) -> Self::IntoIter {
303+
*self
304+
}
305+
}
306+
307+
/// `list[index]`: like [`ListValue::value`], but borrows from the array —
308+
/// `&list[i]` is `&str` for a `List<Utf8>` element, `&i64` for `List<i64>`.
309+
///
310+
/// Available for items that can be borrowed from the array: strings, binaries,
311+
/// and primitives — but not `bool`, `Option<…>`, or nested `List<…>` items.
312+
///
313+
/// Panics if out of bounds (like [`ListValue::value`]).
314+
impl<L: RefType> std::ops::Index<usize> for ListValue<'_, L> {
315+
type Output = L::Ref;
316+
317+
fn index(&self, index: usize) -> &Self::Output {
318+
assert!(
319+
index < self.len(),
320+
"ListValue index {index} out of bounds for length {}",
321+
self.len()
322+
);
323+
L::value_ref(self.values, self.index + index)
324+
}
325+
}
326+
327+
impl<'a, L: PrimitiveType> ListValue<'a, L> {
328+
/// The items as a contiguous zero-copy slice,
329+
/// e.g. `&[f32]` for a `List<f32>` element.
330+
///
331+
/// Only available for primitive and fixed-size binary items
332+
/// (`bool` is excluded: arrow bit-packs it).
333+
#[must_use]
334+
pub fn as_slice(&self) -> &'a [L::Native] {
335+
&L::values(self.values)[self.index..self.end]
336+
}
337+
}
338+
339+
// Iteration mirrors a slice's: the items live in `self.index..self.end`, all
340+
// in bounds, so the combinators are overridden to skip the per-element `Option`
341+
// plumbing of the default `next`-based implementations. (Primitive items have
342+
// an even faster path: [`ListValue::as_slice`].)
172343
impl<'a, L: LogicalType + 'a> Iterator for ListValue<'a, L> {
173344
type Item = L::Value<'a>;
174345

@@ -186,10 +357,81 @@ impl<'a, L: LogicalType + 'a> Iterator for ListValue<'a, L> {
186357
let remaining = self.end - self.index;
187358
(remaining, Some(remaining))
188359
}
360+
361+
fn count(self) -> usize {
362+
self.end - self.index
363+
}
364+
365+
fn last(self) -> Option<Self::Item> {
366+
(self.index < self.end).then(|| L::value(self.values, self.end - 1))
367+
}
368+
369+
fn nth(&mut self, n: usize) -> Option<Self::Item> {
370+
match self.index.checked_add(n) {
371+
Some(target) if target < self.end => {
372+
self.index = target + 1;
373+
Some(L::value(self.values, target))
374+
}
375+
_ => {
376+
self.index = self.end;
377+
None
378+
}
379+
}
380+
}
381+
382+
fn fold<B, F>(self, init: B, mut f: F) -> B
383+
where
384+
F: FnMut(B, Self::Item) -> B,
385+
{
386+
let Self { values, index, end } = self;
387+
let mut acc = init;
388+
for i in index..end {
389+
acc = f(acc, L::value(values, i));
390+
}
391+
acc
392+
}
393+
}
394+
395+
impl<'a, L: LogicalType + 'a> DoubleEndedIterator for ListValue<'a, L> {
396+
fn next_back(&mut self) -> Option<Self::Item> {
397+
if self.index < self.end {
398+
self.end -= 1;
399+
Some(L::value(self.values, self.end))
400+
} else {
401+
None
402+
}
403+
}
404+
405+
fn nth_back(&mut self, n: usize) -> Option<Self::Item> {
406+
match self.end.checked_sub(n + 1) {
407+
Some(target) if self.index <= target => {
408+
self.end = target;
409+
Some(L::value(self.values, target))
410+
}
411+
_ => {
412+
self.end = self.index;
413+
None
414+
}
415+
}
416+
}
417+
418+
fn rfold<B, F>(self, init: B, mut f: F) -> B
419+
where
420+
F: FnMut(B, Self::Item) -> B,
421+
{
422+
let Self { values, index, end } = self;
423+
let mut acc = init;
424+
for i in (index..end).rev() {
425+
acc = f(acc, L::value(values, i));
426+
}
427+
acc
428+
}
189429
}
190430

191431
impl<'a, L: LogicalType + 'a> ExactSizeIterator for ListValue<'a, L> {}
192432

433+
impl<'a, L: LogicalType + 'a> std::iter::FusedIterator for ListValue<'a, L> {}
434+
193435
/// Counts the nulls among the *reachable* items of a list array (`List` or
194436
/// `LargeList` — it is generic over the offset width):
195437
/// items inside the ranges of valid (non-null) rows.

0 commit comments

Comments
 (0)