Skip to content

[Data] Surface MCAP channel metadata as a map column - #65916

Open
marwan116 wants to merge 1 commit into
masterfrom
marwan/mcap-chan-metadata
Open

[Data] Surface MCAP channel metadata as a map column#65916
marwan116 wants to merge 1 commit into
masterfrom
marwan/mcap-chan-metadata

Conversation

@marwan116

Copy link
Copy Markdown
Contributor

Description

A Channel record carries a metadata field of type Map<string, string>MCAP specification, op=0x04, "Metadata about this channel" — holding whatever the recorder chose to record about the topic. ROS 2 writes offered_qos_profiles there, and rigs commonly add sensor serials or calibration identifiers.

No message record reproduces it, so today it is not reachable from the dataset at all. include_metadata=True adds channel_id, message_encoding, schema_name, schema_encoding and schema_data — the schema and the channel's id, but not the channel's own metadata map. A caller who needs it has to re-open the file with the mcap library alongside the Ray Data read.

This adds a channel_metadata column under the existing include_metadata flag.

The column is typed, not inferred

Arrow reads a Python dict as a struct whose fields are the union of the keys it happens to see. Two consequences, both verified:

file A: struct<serial: string>
file B: struct<offered_qos_profiles: string>
pa.concat_tables([A, B]) -> ArrowInvalid: Schema at index 1 was different
  • Blocks from two files recording different keys carry different types and fail to concatenate (only promote_options="permissive" gets past it).
  • A channel that never recorded a key becomes indistinguishable from one whose value is null, because the union fills the missing field with None.

A map<string, string> matches the specification's own type, keeps the block schema stable whatever the keys are, and concatenates across files without promotion. It is built explicitly in _add_channel_metadata rather than added in _message_to_dict, since the builder would otherwise infer the struct.

Verified on a real recording

A FAST-LIVO ROS 2 recording (Livox IMU + lidar + two compressed camera streams):

dtype: map<string, string>
  /left_camera/image/compressed    {'offered_qos_profiles': ''}
  /livox/imu                       {'offered_qos_profiles': ''}
  /livox/lidar                     {'offered_qos_profiles': ''}
  /right_camera/image/compressed   {'offered_qos_profiles': ''}

concat across files: 1457 rows from 40 blocks

Every rosbag2-produced MCAP populates this field, so this is the common case rather than a corner one.

Why this is not a duplicate

I checked issues and PRs for mcap, channel_metadata and metadata in all states.

Compatibility

include_metadata=True is the default, so this adds a column to the default output. It is additive — existing code selecting named columns is unaffected — but code asserting an exact schema will see the new column. read_mcap is @PublicAPI(stability="alpha"). Happy to move it behind a separate flag if maintainers prefer.

Tests

PYTHONPATH=python/ray/data/tests python -u -m pytest \
    python/ray/data/tests/datasource/test_mcap.py -v

21 passed, 1 failed. The failure is test_read_mcap_invalid_time_range, which fails identically on clean master — its regex omits the values the error message interpolates, so it can never match. #65912 fixes it; it is untouched here. (It is not caught in CI because mcap is in no requirements file, which is what #65914 addresses.)

Four new tests: the column carries the channel's metadata and is typed map; include_metadata=False omits it; blocks from files with different metadata keys still concatenate (the reason for map over struct); and a channel recording no metadata yields an empty map rather than failing.

Lint: black, ruff clean on all three files.

AI assistance

AI assistance (Claude) was used for this change. I reviewed every changed line and ran the tests locally.

A Channel record carries a `metadata` field of type `Map<string, string>`
(MCAP specification, op=0x04) holding whatever the recorder chose to
record about the topic. ROS 2 writes `offered_qos_profiles` there, and
rigs commonly add sensor serials or calibration identifiers. No message
record reproduces it, so it was not reachable from the dataset at all:
`include_metadata` added the schema and channel *ids*, but not the
channel's own metadata map.

Add it as a `channel_metadata` column under `include_metadata`.

The column is typed explicitly as `map<string, string>` rather than left
to inference. Arrow reads a Python dict as a `struct` whose fields are
the union of the keys it happens to see, which has two consequences:
blocks from two files recording different keys carry different types and
raise ArrowInvalid on concat_tables, and a channel that never recorded a
key becomes indistinguishable from one whose value is null. A map matches
the specification's own type, keeps the block schema stable whatever the
keys are, and concatenates across files.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Marwan Sarieddine <sarieddine.marwan@gmail.com>
@marwan116
marwan116 requested a review from a team as a code owner September 4, 2026 04:31

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review

This pull request adds support for extracting and appending MCAP channel metadata as a map<string, string> column (named channel_metadata) when reading MCAP files via ray.data.read_mcap. This ensures that channel-level metadata, such as ROS 2 QoS profiles or sensor serials, is preserved and accessible across concatenated blocks. The review feedback suggests two key improvements: handling fallback pandas DataFrame blocks to prevent the metadata column from being silently omitted, and defensively handling cases where channel.metadata might be None or missing.

Comment on lines +306 to +309
if values is None or not isinstance(block, pa.Table):
# `include_metadata=False`, or a pandas block, which
# `DelegatingBlockBuilder` produces for row values Arrow cannot hold.
return block

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

When DelegatingBlockBuilder falls back to producing a pandas.DataFrame (e.g., due to row values that Arrow cannot represent), the channel_metadata column is currently silently omitted because the function returns early for non-pa.Table blocks.

To ensure schema consistency and prevent the column from being dropped when falling back to Pandas, we should also handle pandas.DataFrame blocks by appending the metadata as a column of dictionaries.

Suggested change
if values is None or not isinstance(block, pa.Table):
# `include_metadata=False`, or a pandas block, which
# `DelegatingBlockBuilder` produces for row values Arrow cannot hold.
return block
if values is None:
return block
import pyarrow as pa
if isinstance(block, pa.Table):
column = pa.array(values, type=pa.map_(pa.string(), pa.string()))
return block.append_column(CHANNEL_METADATA_COLUMN, column)
import pandas as pd
if isinstance(block, pd.DataFrame):
block[CHANNEL_METADATA_COLUMN] = [dict(v) for v in values]
return block
return block

Comment on lines +185 to +186
if channel_metadata is not None:
channel_metadata.append(list(channel.metadata.items()))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

To be defensive against malformed MCAP files or custom channel implementations where channel.metadata might be None or missing, we should safely handle the case where channel.metadata is not populated.

Suggested change
if channel_metadata is not None:
channel_metadata.append(list(channel.metadata.items()))
if channel_metadata is not None:
metadata = channel.metadata or {}
channel_metadata.append(list(metadata.items()))

@ray-gardener ray-gardener Bot added the data Ray Data-related issues label Sep 4, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

data Ray Data-related issues

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant