Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
3682ffb
Add .worktrees to .gitignore
antiguru Apr 16, 2026
8a59751
Revert "Add .worktrees to .gitignore"
antiguru Apr 16, 2026
cdcb228
index: add Cursor GAT and cursor() method to Index trait
antiguru Apr 16, 2026
64d7bc5
index: add default cursors to all container Index impls
antiguru Apr 16, 2026
cc24669
index: add cursor composition for tuples
antiguru Apr 16, 2026
2b1ac51
index: specialized rank-free cursors for Repeats and Lookbacks
antiguru Apr 16, 2026
532d7b4
derive: add DefaultCursor to generated Index impls
antiguru Apr 16, 2026
dfe5a67
test: cursor integration tests for Repeats, Lookbacks, tuples
antiguru Apr 16, 2026
02abf83
cursor: cache bitvector word in RepeatsCursor and LookbacksCursor
antiguru Apr 16, 2026
80b9eae
derive: composed cursor for struct containers
antiguru Apr 22, 2026
005744f
index: unify index_iter with cursor
antiguru Apr 22, 2026
aa2cd7d
cursor: address review findings
antiguru Apr 22, 2026
bc6fe6f
test: .borrow().index_iter() now compiles
antiguru Apr 22, 2026
ba7dfa0
index: remove into_index_iter from Index trait
antiguru Apr 22, 2026
152f49a
CHANGELOG: add cursor/index_iter entry for unreleased
antiguru Apr 22, 2026
39665f1
bench: add direct Repeats/Lookbacks cursor vs get comparison
antiguru Apr 22, 2026
05930f7
docs: document Index trait design — Ref/Cursor GAT distinction
antiguru Apr 22, 2026
ccb368b
docs: fix two cargo doc warnings
antiguru Apr 22, 2026
55a835e
index: add CursorOf<'a, T> type alias
antiguru Apr 22, 2026
2c9a6e0
derive: interpolate field names in generated doc comments
antiguru Apr 22, 2026
b93a939
index: generalize CursorOf over container C rather than columnar T
antiguru Apr 22, 2026
ed717f0
index: restore into_index_iter as slow-path escape hatch
antiguru Apr 22, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- `Index::Cursor` GAT and `Index::cursor(range)` method for specialized sequential iteration; `Repeats` and `Lookbacks` skip per-element `rank()` via bit-caching cursors, yielding ~10x faster iteration through columnar composites containing these types ([#105](https://github.com/frankmcsherry/columnar/pull/105))
- Composed cursors for tuples and derived struct containers zip field cursors, propagating specialized iteration through `#[derive(Columnar)]` types
- `DefaultCursor` fallback for types without specialized iteration

### Changed

- `Index::index_iter` returns `Self::Cursor<'_>` instead of `IterOwn<&Self>`, so existing callers pick up specialized cursors automatically; `container.borrow().index_iter()` now compiles, which previously required `into_index_iter` to sidestep a missing `&&[T]: Index` impl. `into_index_iter` remains as a slow-path escape hatch for consuming iteration.

## [0.12.1](https://github.com/frankmcsherry/columnar/compare/columnar-v0.12.0...columnar-v0.12.1) - 2026-03-29

### Other
Expand Down
12 changes: 12 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -61,3 +61,15 @@ harness = false
[[bench]]
name = "simd"
harness = false

[[bench]]
name = "derived_cursor"
harness = false

[[bench]]
name = "options_cursor"
harness = false

[[bench]]
name = "repeats_cursor"
harness = false
80 changes: 80 additions & 0 deletions benches/derived_cursor.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
//! Bench composed-cursor vs default-cursor on derived struct containers.
//!
//! Focus: derived struct with a Repeats field. Composed cursor would let the
//! Repeats field use its rank-free cursor; DefaultCursor falls back to get()
//! which calls rank() per element.

use bencher::{benchmark_group, benchmark_main, Bencher};
use bencher as _;
use columnar::{Columnar, Borrow, Index, Len, Repeats};

#[derive(Columnar, Debug, PartialEq, Eq, Clone)]
struct Row {
key: u64,
val: u64,
}

const N: u64 = 100_000;
const KEYS: u64 = 100;

fn populate() -> RowContainer<Repeats<Vec<u64>>, Vec<u64>> {
use columnar::common::Push;
let mut keys: Repeats<Vec<u64>> = Default::default();
let mut vals: Vec<u64> = Vec::with_capacity(N as usize);
for i in 0..N {
Push::push(&mut keys, &(i / (N / KEYS)));
vals.push(i);
}
RowContainer { key: keys, val: vals }
}

fn derived_get(bencher: &mut Bencher) {
let container = populate();
let borrowed = container.borrow();
bencher.bytes = (N * 16) as u64;
bencher.iter(|| {
let mut sum = 0u64;
for i in 0..borrowed.len() {
let row = borrowed.get(i);
sum = sum.wrapping_add(*row.key).wrapping_add(*row.val);
}
bencher::black_box(sum);
});
}

fn derived_cursor(bencher: &mut Bencher) {
let container = populate();
let borrowed = container.borrow();
bencher.bytes = (N * 16) as u64;
bencher.iter(|| {
let mut sum = 0u64;
for row in borrowed.index_iter() {
sum = sum.wrapping_add(*row.key).wrapping_add(*row.val);
}
bencher::black_box(sum);
});
}

// Hand-written tuple for comparison — already gets composed cursor.
fn tuple_cursor(bencher: &mut Bencher) {
use columnar::common::Push;
let mut keys: Repeats<Vec<u64>> = Default::default();
let mut vals: Vec<u64> = Vec::with_capacity(N as usize);
for i in 0..N {
Push::push(&mut keys, &(i / (N / KEYS)));
vals.push(i);
}
let container = (keys, vals);
let borrowed = container.borrow();
bencher.bytes = (N * 16) as u64;
bencher.iter(|| {
let mut sum = 0u64;
for (k, v) in borrowed.index_iter() {
sum = sum.wrapping_add(*k).wrapping_add(*v);
}
bencher::black_box(sum);
});
}

benchmark_group!(benches, derived_get, derived_cursor, tuple_cursor);
benchmark_main!(benches);
94 changes: 94 additions & 0 deletions benches/options_cursor.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
//! Bench Options/Results cursor: does cursor speed up iteration vs get()?
//!
//! Default impl uses DefaultCursor which delegates to get() → rank() per element.
//! Same rank() overhead as Repeats before specialization.

use bencher::{benchmark_group, benchmark_main, Bencher};
use columnar::{Borrow, Index, Len, Options, Results};

const N: u64 = 100_000;

fn options_populate() -> Options<Vec<u64>> {
use columnar::common::Push;
let mut opts: Options<Vec<u64>> = Default::default();
for i in 0..N {
if i % 3 == 0 { Push::push(&mut opts, None::<u64>); }
else { Push::push(&mut opts, Some(i)); }
}
opts
}

fn options_get(bencher: &mut Bencher) {
let opts = options_populate();
let borrowed = opts.borrow();
bencher.bytes = N * 8;
bencher.iter(|| {
let mut sum = 0u64;
for i in 0..borrowed.len() {
if let Some(v) = borrowed.get(i) {
sum = sum.wrapping_add(*v);
}
}
bencher::black_box(sum);
});
}

fn options_cursor(bencher: &mut Bencher) {
let opts = options_populate();
let borrowed = opts.borrow();
bencher.bytes = N * 8;
bencher.iter(|| {
let mut sum = 0u64;
for v in borrowed.index_iter() {
if let Some(v) = v {
sum = sum.wrapping_add(*v);
}
}
bencher::black_box(sum);
});
}

fn results_populate() -> Results<Vec<u64>, Vec<u32>> {
use columnar::common::Push;
let mut r: Results<Vec<u64>, Vec<u32>> = Default::default();
for i in 0..N {
if i % 5 == 0 { Push::push(&mut r, Err::<u64, u32>(i as u32)); }
else { Push::push(&mut r, Ok::<u64, u32>(i)); }
}
r
}

fn results_get(bencher: &mut Bencher) {
let r = results_populate();
let borrowed = r.borrow();
bencher.bytes = N * 8;
bencher.iter(|| {
let mut sum = 0u64;
for i in 0..borrowed.len() {
match borrowed.get(i) {
Ok(v) => sum = sum.wrapping_add(*v),
Err(e) => sum = sum.wrapping_add(*e as u64),
}
}
bencher::black_box(sum);
});
}

fn results_cursor(bencher: &mut Bencher) {
let r = results_populate();
let borrowed = r.borrow();
bencher.bytes = N * 8;
bencher.iter(|| {
let mut sum = 0u64;
for v in borrowed.index_iter() {
match v {
Ok(v) => sum = sum.wrapping_add(*v),
Err(e) => sum = sum.wrapping_add(*e as u64),
}
}
bencher::black_box(sum);
});
}

benchmark_group!(benches, options_get, options_cursor, results_get, results_cursor);
benchmark_main!(benches);
79 changes: 79 additions & 0 deletions benches/repeats_cursor.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
//! Direct Repeats / Lookbacks cursor bench: measure rank-free iteration win.

use bencher::{benchmark_group, benchmark_main, Bencher};
use columnar::{Borrow, Index, Len, Repeats, Lookbacks};

const N: u64 = 100_000;

fn repeats_populate(run: u64) -> Repeats<Vec<u64>> {
use columnar::common::Push;
let mut r: Repeats<Vec<u64>> = Default::default();
for i in 0..N {
Push::push(&mut r, &(i / run));
}
r
}

fn repeats_get(bencher: &mut Bencher) {
let r = repeats_populate(100);
let borrowed = r.borrow();
bencher.bytes = N * 8;
bencher.iter(|| {
let mut sum = 0u64;
for i in 0..borrowed.len() {
sum = sum.wrapping_add(*borrowed.get(i));
}
bencher::black_box(sum);
});
}

fn repeats_cursor(bencher: &mut Bencher) {
let r = repeats_populate(100);
let borrowed = r.borrow();
bencher.bytes = N * 8;
bencher.iter(|| {
let mut sum = 0u64;
for v in borrowed.index_iter() {
sum = sum.wrapping_add(*v);
}
bencher::black_box(sum);
});
}

fn lookbacks_populate() -> Lookbacks<Vec<u64>> {
use columnar::common::Push;
let mut l: Lookbacks<Vec<u64>> = Default::default();
for i in 0..N {
Push::push(&mut l, &(i % 17));
}
l
}

fn lookbacks_get(bencher: &mut Bencher) {
let l = lookbacks_populate();
let borrowed = l.borrow();
bencher.bytes = N * 8;
bencher.iter(|| {
let mut sum = 0u64;
for i in 0..borrowed.len() {
sum = sum.wrapping_add(*borrowed.get(i));
}
bencher::black_box(sum);
});
}

fn lookbacks_cursor(bencher: &mut Bencher) {
let l = lookbacks_populate();
let borrowed = l.borrow();
bencher.bytes = N * 8;
bencher.iter(|| {
let mut sum = 0u64;
for v in borrowed.index_iter() {
sum = sum.wrapping_add(*v);
}
bencher::black_box(sum);
});
}

benchmark_group!(benches, repeats_get, repeats_cursor, lookbacks_get, lookbacks_cursor);
benchmark_main!(benches);
Loading