Skip to content

fix(io): keep member names and types when reading a compound dataset with no rows - #880

Merged
ehennestad merged 1 commit into
mainfrom
fix-empty-compound-dataset-read
Aug 31, 2026
Merged

fix(io): keep member names and types when reading a compound dataset with no rows#880
ehennestad merged 1 commit into
mainfrom
fix-empty-compound-dataset-read

Conversation

@ehennestad

Copy link
Copy Markdown
Collaborator

Motivation

Background#876 fixed the write path for compound datasets that hold no rows, and noted that reading one back still returns an empty array without member names. This is that follow-up.

Problem — A compound dataset that holds no rows is read as []. Its member names and types are lost, along with any sign that the dataset exists. A file containing one cannot be exported again: the read leaves the required data property empty, so export stops with a required-property error naming a property the user never set. PyNWB writes six such datasets for the HERD of any file whose external resources hold no references, so this reaches MatNWB through files it did not write.

Solution — Read a compound dataset the same way whether or not it holds rows. It becomes a DataStub that carries the member names and types, and exporting it copies the dataset, so the file round-trips unchanged.

What changed

  • A compound dataset with no rows is read as a types.untyped.DataStub rather than []. Its dims is 0 and its dataType lists every member with the MATLAB type it maps to.
  • load() on that stub returns a scalar struct holding one empty, correctly typed column per member.
  • Indexing the stub returns the zero-row form of the table a populated compound dataset is read as.
  • A file holding such datasets can be read and exported again with its member names and HDF5 member types unchanged.
Implementation notes

H5D.read returns a 0x0 struct without fields for a dataset that holds no rows, so the member names never reached the point where io.parseCompound builds its columns. It now rebuilds them from the compound type instead, giving each member an empty value of the MATLAB type it maps to, including types.untyped.ObjectView for reference members and logical for boolean enums.

io.backend.hdf5.HDF5Reader exempts compound datasets from the rule that collapses a zero-size dataset to []. A compound dataset is stubbed regardless of its row count, so export copies it and its HDF5 member types are preserved exactly rather than rederived from MATLAB classes.

types.untyped.DataStub/export takes the plain object copy for a compound dataset with no rows. The manual read-and-write path exists to work around an HDF5 library bug that corrupts reference values, and a dataset without rows holds none.

Examples

The file below is what PyNWB writes for a session whose external resources were requested but never populated. Its six HERD datasets are compound datasets with no rows.

from datetime import datetime
from uuid import uuid4
from dateutil.tz import tzlocal
from pynwb import NWBHDF5IO, NWBFile

f = NWBFile(session_description='d', identifier=str(uuid4()),
            session_start_time=datetime(2018, 4, 25, 2, 30, 3, tzinfo=tzlocal()))
f.get_external_resources()
with NWBHDF5IO('empty_herd.nwb', 'w') as io:
    io.write(f)

Member names and types of a compound dataset that holds no rows

nwb = nwbRead('empty_herd.nwb', 'ignorecache');
objects = nwb.general_external_resources.objects.data
objects.dataType

Before

objects =

     []

>> objects.dataType
Error: Dot indexing is not supported for variables of this type.

After

objects =

  DataStub with dims 0 and members:

        files_idx: 'uint32'
        object_id: 'char'
      object_type: 'char'
    relative_path: 'char'
            field: 'char'

Exporting a file that holds them

nwb = nwbRead('empty_herd.nwb', 'ignorecache');
class(nwb.general_external_resources.keys.data)
nwbExport(nwb, 'roundtrip.nwb')

Before

ans =

    'double'

>> nwbExport(nwb, 'roundtrip.nwb')
Error using nwbExport
The following required properties are missing for instance for type "types.hdmf_common.Data" at file location "/general/external_resources/entities":
    data

After

ans =

    'types.untyped.DataStub'

>> nwbExport(nwb, 'roundtrip.nwb')
>> h5info('roundtrip.nwb', '/general/external_resources/objects').Dataspace.Size

ans =

     0

>> {info.Datatype.Type.Member.Name}

ans =

  1x5 cell array

    {'files_idx'}    {'object_id'}    {'object_type'}    {'relative_path'}    {'field'}

How to test

Write the file with PyNWB using the snippet above, then run:

nwb = nwbRead('empty_herd.nwb', 'ignorecache');
disp(class(nwb.general_external_resources.keys.data))   % types.untyped.DataStub
disp(nwb.general_external_resources.objects.data.dataType)

nwbExport(nwb, 'roundtrip.nwb');
info = h5info('roundtrip.nwb', '/general/external_resources/objects');
disp(info.Dataspace.Size)                                % 0
disp({info.Datatype.Type.Member.Name})

The new tests cover the round trip and a compound dataset whose members include a boolean and an object reference:

runtests('tests.unit.io.EmptyCompoundTest')

Checklist

  • Have you ensured the PR description clearly describes the problem and solutions?
  • Have you checked to ensure that there aren't other open or previously closed Pull Requests for the same change?
  • If this PR fixes an issue, is the first line of the PR description fix #XX where XX is the issue number?

🤖 Generated with Claude Code

@codecov

codecov Bot commented Aug 26, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 95.31%. Comparing base (2880afc) to head (866bf55).

Additional details and impacted files
@@            Coverage Diff             @@
##             main     #880      +/-   ##
==========================================
+ Coverage   95.27%   95.31%   +0.04%     
==========================================
  Files         234      234              
  Lines        8311     8329      +18     
==========================================
+ Hits         7918     7939      +21     
+ Misses        393      390       -3     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@ehennestad
ehennestad marked this pull request as draft August 27, 2026 07:45
@ehennestad
ehennestad requested a review from bendichter August 27, 2026 15:23
@ehennestad
ehennestad marked this pull request as ready for review August 27, 2026 15:23
@ehennestad
ehennestad force-pushed the fix-empty-compound-dataset-read branch from 5d18905 to 391a967 Compare August 27, 2026 15:23
@ehennestad
ehennestad enabled auto-merge August 27, 2026 15:25
@ehennestad
ehennestad force-pushed the fix-empty-compound-dataset-read branch from 391a967 to 3c08ee7 Compare August 27, 2026 15:29
… dataset

A compound dataset holding no rows was read as [], dropping its member
names and types along with the evidence that the dataset exists.
Re-exporting such a file failed with NWB:RequiredPropertyMissing, because
the required `data` property of the neurodata type read as empty. PyNWB
writes six such datasets for the HERD of a file that has no external
resource references.

H5D.read returns a 0x0 struct without fields for a dataset that holds no
rows, so io.parseCompound rebuilds the columns from the compound type,
giving each member an empty value of the MATLAB type it maps to. The HDF5
reader now stubs a compound dataset regardless of its row count, so
reading one yields a DataStub whose dims and dataType carry the structure
and whose export copies the dataset unchanged.

DataStub/export takes the plain object copy for a row-less compound. The
manual read-and-write path exists to work around an HDF5 library bug that
corrupts reference values, and a dataset without rows has none.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@ehennestad
ehennestad force-pushed the fix-empty-compound-dataset-read branch from eb7a97e to 866bf55 Compare August 31, 2026 10:44
@ehennestad
ehennestad added this pull request to the merge queue Aug 31, 2026
Merged via the queue into main with commit 52edb08 Aug 31, 2026
20 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants