HDF5 and ROSBag Ingestion
Transform HDF5 datasets or ROS messages into rows in an existing multimodal table. Start with a conn from Multimodal Tables, then install the extra for your input format. Source paths and transforms depend on your dataset.
Load HDF5
MultimodalConnection.load_from_hdf5 streams one or more local or remote HDF5
files into an existing multimodal table. HDF5 loading requires Python 3.8 or
newer. Install the optional dependency first:
pip install 'pypaimon[hdf5]'
The transform receives an open h5py.File and an Hdf5File. Its path is the
resolved local file:// URI or remote URI; name and stem provide convenient
path components. For local sources, local_path returns a decoded Path that
can be used to read sibling files, including paths containing spaces or Unicode;
it is None for remote sources. The transform can return one Arrow table or
record batch, or an iterable that yields multiple tables or batches:
import pyarrow as pa
EMBEDDING_VECTOR_TYPE = pa.list_(pa.float32(), 3)
IMAGE_BLOB_TYPE = pa.large_binary()
schema = pa.schema([
pa.field("episode_id", pa.string(), nullable=False),
pa.field("frame_index", pa.int32(), nullable=False),
# Arrow fixed-size lists map to Paimon VECTOR columns.
pa.field("embedding", EMBEDDING_VECTOR_TYPE, nullable=False),
# Arrow binary and large-binary values map to Paimon BLOB columns.
pa.field("image", IMAGE_BLOB_TYPE),
])
frames = conn.create_table("frames", schema=schema)
def transform(h5, source):
episode_id = source.stem
for begin in range(0, len(h5["embedding"]), 128):
end = min(begin + 128, len(h5["embedding"]))
yield pa.RecordBatch.from_pydict({
"episode_id": [episode_id] * (end - begin),
"frame_index": list(range(begin, end)),
"embedding": h5["embedding"][begin:end].tolist(),
"image": [bytes(value) for value in h5["image"][begin:end]],
}, schema=schema)
result = conn.load_from_hdf5(
"frames",
"/data/episodes",
transform=transform,
)
print(result.file_count)
print(result.batch_count)
print(result.row_count)
print(result.snapshot_id)
A path can be one .h5 or .hdf5 file, an iterable of paths, or a directory.
Directories are searched recursively. Before opening files, load_from_hdf5
resolves every discovered path, removes duplicate files within that call, and
sorts by the resolved path. Overlapping directory and file arguments therefore
read a physical file once per call. Local paths and file://, hdfs://,
viewfs://, oss://, s3://, and gs:// URIs use PyPaimon's existing
FileIO implementations.
Remote credentials and endpoints belong to the HDF5 source, not necessarily
the target warehouse. Pass standard FileIO settings through source_options;
they are not inherited from the target table, written to table options, or
retained after the call. This also applies to HDFS client settings: provide
source-specific settings explicitly, while configuration discoverable by the
underlying filesystem client can still come from its normal environment:
remote_result = conn.load_from_hdf5(
"frames",
"oss://source-bucket/episodes",
transform=transform,
source_options={
"fs.oss.endpoint": "oss-cn-hangzhou.aliyuncs.com",
"fs.oss.accessKeyId": "SOURCE_ACCESS_KEY_ID",
"fs.oss.accessKeySecret": "SOURCE_ACCESS_KEY_SECRET",
},
)
Because native HDFS Kerberos credentials use process-global ticket state, HDF5
sources cannot run an explicit-keytab login in a shared process. Acquire the
ticket in a process-isolated worker, omit the source principal/keytab options,
and then call load_from_hdf5 in that worker.
HDF5 requires random access. load_from_hdf5 passes the seekable stream returned
by FileIO.new_input_stream directly to h5py and never downloads it to a local
temporary file. A non-seekable stream fails before commit. Recursive directory
discovery requires backend listing support; legacy OSS on PyArrow before 16
must use explicit file paths, Jindo, or a newer PyArrow version. HDF5 performs
many small seeks, so benchmark remote-object latency for large production
inputs even though no full-file download is required.
An empty path iterable or existing directory with no HDF5 files is a no-op. It
returns zero file, batch, and row counts with snapshot_id=None, without
creating a writer or snapshot. A nonexistent path or unsupported file suffix
is still an error, as is a discovered file whose transform produces no rows.
The target schema is strict. Each batch must contain exactly the target columns in target order and must be safely convertible to the target Arrow schema. Missing columns are not filled with null, extra columns are not discarded, and invalid nullability, incompatible types, or fixed-size vector lengths fail the whole call.
One call uses one writer and one commit for all discovered files, and a
successful non-empty call creates exactly one snapshot. Any failure before the
commit aborts the writer and leaves no partial snapshot. An exception after the
commit starts has Paimon's unknown-commit-result semantics: load_from_hdf5 does
not retry and does not abort files that a snapshot may already reference.
For a future Ray integration, split HDF5 file descriptions across worker tasks,
apply the same transform and strict batch contract in each task, and collect
writer commit messages for one coordinator commit. Do not call load_from_hdf5
independently in every task, because that would create one commit per task. The
HDF5 core itself has no Ray dependency.
load_from_hdf5 is append-only and is not retry-safe. It does not add source
provenance columns, maintain a source ledger, skip prior inputs, or detect
source drift. Calling it again with the same input appends the rows again.
Load ROSBag
MultimodalConnection.load_from_rosbag imports ROS1 .bag, ROS2 SQLite3 or
MCAP recording directories, and standalone ROS2 .mcap files through a user
Arrow transform. It requires Python 3.10 or newer:
pip install 'pypaimon[rosbag]'
import pyarrow as pa
schema = pa.schema([
pa.field("source", pa.string(), nullable=False),
pa.field("timestamp", pa.int64(), nullable=False),
pa.field("value", pa.string(), nullable=False),
])
conn.create_table("messages", schema=schema)
def transform(reader, source):
rows = []
for connection, timestamp, rawdata in reader.messages():
message = reader.deserialize(rawdata, connection.msgtype)
rows.append({
"source": source.name,
"timestamp": timestamp,
"value": message.data,
})
return pa.Table.from_pylist(rows)
result = conn.load_from_rosbag(
"messages",
"/data/recordings",
transform=transform,
)
print(result.source_count, result.row_count, result.snapshot_id)
A transform receives an open rosbags.highlevel.AnyReader and a
RosbagSource. The source exposes the original normalized uri, the temporary
or original local_path, format, name, stem, and is_remote. Reader and
local staging paths are valid only while the transform is running. A transform
returns an Arrow table, record batch, or an iterable of either.
Directories are searched recursively, but a directory containing
metadata.yaml is one ROS2 logical source and is not searched below that
point. Standalone .db3 files are rejected because they may be one split of a
larger recording. Set allow_storage_fragment=True only when importing exactly
that SQLite storage fragment is intentional; the loader performs a SQLite
integrity check but cannot prove that other recording splits are absent.
Local paths and file://, oss://, s3://, hdfs://, viewfs://, and
gs:// URIs use PyPaimon FileIO. Pass source credentials through
source_options; target warehouse options are not inherited. Unlike HDF5,
rosbags requires local paths, so remote files are copied in bounded chunks to
an attempt-scoped temporary directory. ROS2 metadata member paths are checked
for traversal and normalization collisions before copying.
Before Paimon creates a writer, the loader:
- validates every source manifest and checks for active or recovery sidecars;
- scans every recording to EOF and compares declared and readable messages;
- executes each transform once and applies strict target Arrow schema checks;
- stores all validated output in a temporary Arrow IPC file.
This front-loaded validation normally reads each recording twice and needs temporary disk capacity, but a source, transform, or schema failure does not create Paimon data files. Source size, modification time, and ROS2 directory members are compared during the call. These checks detect observable changes; without a versioned URI they are not a transactional snapshot of an object store directory.
Use RosbagStagingConfig to choose the local temporary directory, reserve free
space with min_free_bytes, cap serial temporary usage with max_bytes, and
set the remote copy chunk size with copy_buffer_bytes. In Ray mode,
max_bytes applies to each worker's raw ROSBag staging; Arrow output capacity
is controlled by Ray object-store and spill settings.
For Ray, install pypaimon[ray,rosbag] and call
pypaimon.ray.load_from_rosbag. The driver discovers manifests, each worker
materializes and validates complete sources locally, and the transformed Ray
Dataset is explicitly materialize()d before write_paimon. Success uses one
coordinator commit. Ray may retry a transform, so transforms must be
deterministic and free of non-idempotent external side effects.
Both APIs are append-only and not retry-safe after a commit exception. A successful call creates one snapshot; write-stage failures may leave uncommitted orphan files for normal Paimon cleanup.