duckdb-mcap
June 10, 2026 · View on GitHub
A high-performance DuckDB extension that enables SQL-native querying of MCAP files.
Built against DuckDB v1.4.4 (pinned via submodules).
Current surface
-
mcap_scan(path)streams MCAP messages.pathmay be a single file, a glob pattern (runs/*.mcap), or any path DuckDB's filesystems understand (includinghttpfs/S3 once that extension is loaded). Columns:column type timestampTIMESTAMP_NSmessage log time, full nanosecond precision topicVARCHARchannel topic payload_blobBLOBraw message bytes schema_nameVARCHARschema name of the channel payload_jsonJSONdecoded payload (see below), NULL if undecodable publish_timeTIMESTAMP_NSmessage publish time sequenceUINTEGERper-channel sequence number channel_idUSMALLINTMCAP channel id filenameVARCHARsource file (useful with globs) -
FROM 'run.mcap'works directly (replacement scan). -
Scans run in parallel: each file's chunk index is split into time-range partitions consumed by DuckDB's worker threads.
-
mcap_topics(path)— per-topic type, message count, and exact per-topicstart/endtime ranges (from message indexes; NULL if the file has none). -
mcap_channels(path),mcap_schemas(path)— channel/schema registry. -
mcap_info(path)— header + statistics (profile, library, counts, time range). -
mcap_attachments(path),mcap_metadata(path)— attachment and metadata records. -
rosout(path)— ROS-aware view over/rosoutlogs. Handles both JSON and CDR (rcl_interfaces/msg/Log) encodings; numeric levels map to DEBUG/INFO/WARN/ERROR/FATAL. -
Pushdown: topic and timestamp filters are translated into MCAP
ReadMessageOptions(channel/chunk pruning); projection pushdown skips JSON decoding whenpayload_jsonis not selected. Cardinality estimates come from MCAP statistics.
Payload decoding (payload_json)
| Encoding | Support | How |
|---|---|---|
json / jsonschema | ✅ | Passthrough |
protobuf | ✅ | Dynamic decode from the embedded FileDescriptorSet (no precompiled .proto) via libprotobuf, emitted as JSON |
cdr (ROS2) | ✅ | ros2msg/ros2idl schema parser + CDR reader → JSON (nested types, arrays, strings; LE and BE) |
cdr (XCDR2 / PL_CDR) | ❌ | Rejected (NULL) rather than misparsed |
ros1 | ❌ | Out of scope (deprecated) |
Unsupported encodings (and wstring fields, whose CDR layout is
vendor-ambiguous) yield NULL for payload_json instead of garbage.
Example
LOAD 'mcap';
LOAD 'json';
-- metadata
SELECT * FROM mcap_info('run.mcap');
SELECT * FROM mcap_topics('run.mcap');
-- decoded protobuf / ros2 fields, across a directory of recordings
SELECT timestamp, filename, payload_json->>'$.header.frame_id' AS frame
FROM mcap_scan('runs/*.mcap')
WHERE topic = '/vectornav/IMU_restamped'
AND timestamp BETWEEN TIMESTAMP '2024-01-01' AND TIMESTAMP '2024-01-02';
-- or just
SELECT count(*) FROM 'run.mcap';
Build dependencies
- DuckDB + extension-ci-tools submodules (pinned to v1.4.4)
lz4,zstd(MCAP chunk compression) — e.g.brew install lz4 zstdprotobuf(protobuf payload decoding) — e.g.brew install protobuf
Bootstrap
git submodule update --init --recursive
python3 -m venv .venv
. .venv/bin/activate
pip install -r test/requirements.txt
python test/generate_sample_mcap.py
python test/generate_protobuf_mcap.py
python test/generate_ros2_mcap.py
make
make test
Fast dev loop
./run_tests.sh # incremental build + run all test/sql/*.test, then the C++ unit tests
./run_tests.sh --no-build # just run tests
./run_tests.sh protobuf # build + run a single test
Tests
- SQL (
test/sql/*.test): end-to-end sqllogictests over generated MCAP fixtures, run via DuckDB'sunittestbinary (make test). Fixture generators are pinned viatest/requirements.txt; CI regenerates the fixtures and fails on drift. - C++ (
test/cpp/*.cpp): Catch2 unit tests for the pushdown filter logic (AND/OR/IN, range hull, overflow saturation) and the JSON field extractor (escapes, unicode, nesting). Build + run with./run_cpp_tests.sh(links thelibduckdbfrom a prior build); also run as part of a full./run_tests.sh.