Skip to content

Conversation

@grtlr
Copy link
Member

@grtlr grtlr commented Nov 12, 2025

What

This PR revises the Lenses API and specification by introducing different types of lenses that are centered around the time columns:

  • OneToOne lenses have the same amount of output rows as their input column.
  • Static lenses don't have any time columns attached to them.
  • OneToMany lenses produce multiple rows for any given row in their input column.

These kind of lenses are accessed in the LensBuilder via columns, static_columns, and scatter_columns methods, which create scopes that can be used to bundle columns together.

Although the concept of chunks is not exposed in the Lenses API, under the hood all columns (time or component) specified in a scope (LensKind) will end up in the same chunk.

Context

The new scatter columns operating mode is required for "plural" MCAP messages that have multiple timestamps defined in a single message. An example for this is foxglove.FrameTransforms.

@grtlr grtlr added the exclude from changelog PRs with this won't show up in CHANGELOG.md label Nov 12, 2025
@grtlr grtlr requested a review from Copilot November 12, 2025 10:19
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR refactors the Lenses API to support three distinct lens types based on row transformation patterns: OneToOne (1:1 mapping), Static (timeless data), and ToMany (1:N scatter). The new builder-based API uses output_columns, output_static_columns, and output_scatter_columns methods to create scoped column groups, replacing the previous single-method approach.

Key Changes:

  • Introduced LensKind enum to distinguish between lens transformation types
  • Added builder pattern with scoped output methods for different lens modes
  • Implemented Explode transformation for flattening nested lists in scatter operations

Reviewed Changes

Copilot reviewed 15 out of 15 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
examples/rust/lenses/src/main.rs Updated lens construction to use new builder API with scoped column methods
crates/utils/re_arrow_combinators/tests/util.rs Added helper utility for displaying arrays as record batches in tests
crates/utils/re_arrow_combinators/tests/transform.rs Refactored to use shared utility module
crates/utils/re_arrow_combinators/tests/snapshots/*.snap Added snapshot test files for explode functionality
crates/utils/re_arrow_combinators/tests/explode.rs New test file for explode transformation
crates/utils/re_arrow_combinators/src/reshape.rs Implemented Explode transformation for scattering list elements
crates/top/re_sdk/src/lenses/snapshots/*.snap Added snapshots for scatter column tests
crates/top/re_sdk/src/lenses/op.rs Minor import restructuring
crates/top/re_sdk/src/lenses/mod.rs Updated exports to include new builder types
crates/top/re_sdk/src/lenses/builder.rs New builder API with ColumnsBuilder, StaticColumnsBuilder, and ScatterColumnsBuilder
crates/top/re_sdk/src/lenses/ast.rs Major refactoring to support new lens types and transformation logic

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@grtlr grtlr force-pushed the grtlr/lenses-v2 branch 2 times, most recently from 23173e5 to 4abf140 Compare November 12, 2025 10:28
@github-actions
Copy link

github-actions bot commented Nov 12, 2025

Web viewer built successfully.

Result Commit Link Manifest
a8e1695 https://rerun.io/viewer/pr/11871 +nightly +main

View image diff on kitdiff.

Note: This comment is updated whenever you push a commit.

@oxkitsune oxkitsune self-requested a review November 12, 2025 13:09
Copy link
Member

@emilk emilk left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I only have a superficial understanding of lenses, so I mostly reviewed the code.

It mostly looks good except the pervasive logging of errors instead of returning them, which is very unidiomatic. If we're going that route I want a good motivation for it, and tests that include checking the log output for any errors!

Comment on lines 78 to 81
ToMany {
target_entity: TargetEntity,
components: Vec<ComponentOutput>,
timelines: Vec<TimeOutput>,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this mean timelines is never empty? Should we maybe use vec1 for it?

Copy link
Member Author

@grtlr grtlr Nov 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry the doc comment was confusing here. Time columns defined via Lenses are optional.

Regarding vec1: Might make sense for component columns, but these structs would be backing a potential future config language so that would make them weird to use? A just saw that vec1 has a serde feature.

Comment on lines +254 to +257
re_log::error!(
"Lens operations failed for component columns '{}': {err}",
output.component_descr
);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are we sure we should not abort the operation if this happens?

} else {
re_log::error_once!(
"Output for timeline '{timeline_name}' must produce data type {}",
DataType::List(Arc::new(Field::new_list_field(DataType::Int64, false))),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeeshus arrow-rs can be verbose sometimes…

let inner_offsets = inner_list.value_offsets();

for j in start..end {
new_validity.push(!inner_list.is_null(j));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
new_validity.push(!inner_list.is_null(j));
new_validity.push(inner_list.is_valid(j));

type Source = ListArray;
type Target = ListArray;

fn transform(&self, source: &Self::Source) -> Result<Self::Target, Error> {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
fn transform(&self, source: &Self::Source) -> Result<Self::Target, Error> {
fn transform(&self, source: &Self::Source) -> Result<Self::Target, Error> {
re_tracing::profile_function!();

@@ -0,0 +1,21 @@
---
source: crates/utils/re_arrow_combinators/tests/explode.rs
expression: "format!(\"{}\", DisplayRB(result))"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be a lot easier to check these tests if these snapshot tests included the input.

Alternatively, put the snapshot output into the test files with inline snapshots

let explode = Explode;
let result = explode.transform(&input).unwrap();

insta::assert_snapshot!("primitives", format!("{}", DisplayRB(result)));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment on lines 434 to 439
Err(err) => {
re_log::error_once!(
"Failed to replicate timeline '{}' for entity '{entity_path}': {err}",
timeline_name
);
return None;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In some places you log errors and continue, but here you log an error and abort. Why not just return an Err instead?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

exclude from changelog PRs with this won't show up in CHANGELOG.md

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants