Guidance for Claude, Codex and other coding agents working in the Vortex repository.
- When asked to review a PR, especially via
/pr-review, use the.agents/skills/pr-reviewskill. - When asked a question about the PR or codebase, especially via
/query, use the.agents/skills/queryskill. - When asked to investigate a CI failure, especially via
/ci-failure-analysis, use the.agents/skills/ci-failure-analysisskill.
Vortex is a Rust monorepo for columnar array processing, compression encodings, and file IO.
The workspace also contains Java bindings in java/, Python bindings in vortex-python/,
documentation in docs/, and benchmark tooling in vortex-bench/ and benchmarks/.
vortex-bufferdefines zero-copy alignedBuffer<T>andBufferMut<T>, guaranteed to be aligned toTor to a requested runtime alignment.vortex-array/src/dtypecontains theDTypelogical type system used throughout Vortex.vortex-arraycontains the coreArraytrait and the base encodings, including most Apache Arrow-style encodings.encodings/*contains more specialized compressed encodings.vortex-fileimplements file IO. It usesLayoutReaderfromvortex-layout.vortex-scan,vortex-session,vortex-datafusion, andvortex-duckdbcontain scan and execution integrations.vortex-pythoncontains Python bindings. RST-flavored project docs live indocs/.
Prefer narrow crate builds while iterating:
cargo build -p <crate-name>Use workspace-wide builds only when the change spans crate boundaries or before handing off a broad refactor:
cargo build --workspaceRun tests for the crate or binding you touched before broader checks:
cargo nextest run -p <crate-name>if cargo-nextest is not available you can install it with
cargo install --locked cargo-nextestExamples:
cargo nextest run -p vortex-array
make -C docs doctest
uv run --all-packages pytest vortex-python/test
cargo test --docRun docs doctests from the docs directory with make -C docs doctest so the correct Sphinx
Makefile target is used.
If you touch documentation run doc tests via cargo test --doc.
For Python binding changes under vortex-python/, run the narrow Python checks that match the
files touched before broader test suites. Useful checks include:
python -m py_compile <changed-python-files>
uv run --all-packages --reinstall-package vortex-data pytest <changed-python-tests>If Python docstrings, docs/api/python/, or Sphinx configuration change, also run the docs checks
from a clean Sphinx environment:
uv run --all-packages make -C docs clean html
uv run --all-packages make -C docs clean doctestRun verification that matches the files changed. Do not run expensive Rust checks for changes that
only touch Markdown, agent configuration, comments outside Rust code, symlinks, or other metadata
with no Rust/API behavior impact. For docs/config-only changes, validate formatting by inspection
or with a targeted doc/config command, and verify symlink or path changes with ls, find, and
git status.
For Python binding changes under vortex-python/, run the relevant Python lint and type checks:
uv run basedpyright vortex-python
uv run ruff check <changed-python-files>If PyO3 Rust files in vortex-python/src/ change, include cargo +nightly fmt --check -p vortex-python. Always finish Python binding work with git diff --check.
For Rust code, public API, feature flag, or generated-file changes, run these before stopping:
cargo +nightly fmt --all
cargo clippy --all-targets --all-featuresNotes:
- For
.github/changes, follow.github/AGENTS.mdand runyamllint --strict -c .yamllint.yamlon changed workflow files. - You can try
cargo fix --lib --allow-dirty --allow-staged && cargo clippy --fix --lib --allow-dirty --allow-stagedto fix minor Rust diagnostics automatically when working on Rust code. - If cargo fails with exactly
sccache: error: Operation not permitted, rerun that command withRUSTC_WRAPPER=so rustc runs directly. Only do this for that exact error.
- When iterating on CI failures, fetch only failed job logs first:
gh run view <run-id> --job <job-id> --log-failed. - Run narrow local repro commands for the affected crate, test, docs target, or binding before running workspace-wide checks.
- If a
ghcommand fails witherror connecting to api.github.comin the sandbox, immediately rerun it with escalated network permissions instead of retrying in the sandbox. - Verify causation from logs, diffs, and local repros before attributing a failure to a PR.
- Prefer
impl AsRef<T>to&Tfor public interfaces where practical, for exampleimpl AsRef<Path>. - Avoid
unsafeunless it is necessary. Prefer zero-cost safe abstractions, or cheap non-zero-cost safe abstractions, over hand-written unsafe code. - Every new public API definition must have a doc comment. Examples are useful but not required.
- Use
vortex_err!to create aVortexErrorwith a format string. - Use
vortex_bail!to create and immediately return aVortexErroras aVortexResult<T>. - Keep imports at the top of the module. The only exception is a
#[cfg(test)]test module, where imports should be at the top of that module. - Prefer imports over qualified identifiers when the name is used repeatedly.
- Avoid function-scoped imports unless required or unless fully qualifying both sides would be exceptionally verbose.
- Only write comments that explain non-obvious logic or important context. Do not comment self-explanatory code.
- Keep public APIs small and consistent with neighboring crates.
Performance: avoid hidden-cost accessors in hot loops
The most common performance trap in this codebase is calling a per-element accessor that
hides non-trivial work inside an O(n) loop, instead of doing the work once over the whole
chunk. The call site looks like a cheap getter, but each call re-pays a cost that is constant
(or amortizable) across the loop, making the loop O(n · k).
Watch for these accessors used inside for i in 0..n { ... }:
| Per-element accessor (in a loop) | Hidden cost | Bulk replacement |
|---|---|---|
Validity::is_valid(i) / is_null(i) |
for array-backed validity, allocates an ExecutionCtx and runs a scalar lookup per call |
validity.execute_mask(len, ctx)? once, then Mask::value(i) |
array.scalar_at(i) / array.execute_scalar(i, ctx) |
per-element execution through the compute stack | canonicalize once (execute::<PrimitiveArray> / as_slice) then index |
BitBuffer::value(i) / Mask::value(i) accumulated into a count |
recomputes the byte address each call; defeats popcount | true_count(), BitBuffer::count_range(start, end), set_indices() |
BitIterator::next() to accumulate a running rank/prefix count |
bit-at-a-time | count_range over each gap (SIMD popcount) |
re-deriving a value inside the loop (e.g. self.validity()? each iteration) |
re-runs the derivation n times |
hoist it above the loop |
Decide per site — bulk is not always the answer:
- Sequential / contiguous access over an accessor that hides amortizable work → hoist and go bulk (materialize once, then index or iterate the chunk).
- Gather over arbitrary indices → you cannot amortize a per-element decode, but you can
still materialize the backing buffer once (e.g.
execute_mask) and then do cheapO(1)random reads, avoiding per-call context/allocation. - The accessor is already genuinely
O(1)(e.g. reading an already-materializedMask/ slice, or a native bitmap) → leave it; bulk would not help.
Even after materializing into a Mask/BitBuffer, do not loop with value(i) per
element to act on each set bit — the per-element branch dominates. Iterate a u64 word
at a time with all-set / all-unset fast paths: use [BitBuffer::for_each_set_index]. It
beats for i in 0..len { if buf.value(i) {..} } by 2-45x (more at low density) and beats
collecting set_indices() by ~2x at mid/high density, while self-adapting from sparse to
dense — see vortex-mask/benches/mask_iteration.rs. Reach for the cached indices() /
slices() representations when you need them more than once; otherwise for_each_set_index
needs no materialization.
When you touch such a loop, back the change with a benchmark (see
vortex-array/benches/validity_is_valid.rs for the is_valid case,
vortex-mask/benches/valid_counts.rs for the popcount case, and
vortex-mask/benches/mask_iteration.rs for the per-element-vs-word-vs-sparse comparison).
- Strongly consider
rstestcases when parameterizing repetitive test logic. - Prefer test functions that return
VortexResult<()>and use?instead ofunwrap. - Prefer test module names
tests, nottest. - Use
assert_arrays_eq!for array comparisons instead of element-by-element assertions. - Keep tests concise and focused on behavior, edge cases, and regressions.
- If a bug fix is requested, add or identify a failing test first when practical. A test that passes before and after the fix does not prove the fix.
- If clippy lints in tests prohibit patterns that are acceptable only in test code, consider allowing the lint at the test module level.
- If an existing
foo.rsmodule needs many tests, promote it to a directory module:foo/mod.rsplusfoo/tests.rs, included fromfoo/mod.rsbehind the appropriate test configuration.
Check new and modified lines against this list before finishing:
- Running broad CI-style commands before trying a narrow local repro.
- Using
unwrap,expect, or panic-oriented assertions in tests whereVortexResult<()>and?would be clearer. - Comparing arrays element by element instead of using
assert_arrays_eq!. - Adding imports inside functions when module-level imports would work.
- Introducing
unsafewithout proving that safe Rust cannot express the same operation. - Updating expected test output to match buggy behavior without independently verifying the intended semantics.
- Silently reducing the scope of an approved plan when implementation is harder than expected.
- Calling a hidden-cost per-element accessor (
Validity::is_valid,scalar_at,BitBuffer:: valueaccumulation) inside a hot loop instead of materializing once — see "Performance: avoid hidden-cost accessors in hot loops".
When summarizing work, write valid Markdown that can be copied into GitHub. Include the checks you ran and call out any checks you could not run.
All commits must be signed off by the committers in this form:
Signed-off-by: "COMMITTER" <COMMITTER_EMAIL>