fix(io): write compound datasets that have no rows - #876
Open
ehennestad wants to merge 4 commits into
Open
Conversation
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #876 +/- ##
==========================================
+ Coverage 95.31% 95.33% +0.01%
==========================================
Files 234 234
Lines 8329 8352 +23
==========================================
+ Hits 7939 7962 +23
Misses 390 390 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
3 tasks
ehennestad
force-pushed
the
fix-empty-compound-dataset-write
branch
from
August 27, 2026 09:02
0d9ca4c to
0682abd
Compare
ehennestad
force-pushed
the
fix-empty-compound-dataset-write
branch
2 times, most recently
from
August 31, 2026 14:25
0956f02 to
5ce3703
Compare
A compound dataset with no rows could not be written correctly. Every column was written as a string on disk regardless of what it held, so an integer column read back as text, and the same column changed member type depending on whether the table happened to be empty. A compound dataset holding a table with no rows also counted as a missing required property, so a file containing one could not be exported at all. Take the row count from the input, which knows it, rather than reading it back off the normalized struct, where a scalar struct means either a single row of values or one column per field. Read the columns of a zero-row table directly so numeric and logical columns keep their class, and carry every other column as an empty cellstr so it keeps the variable length string member type a text column already gets. Treat a zero-row table as a value rather than an omission when checking required properties. Raise NWB:WriteCompound:NoFields when the data carries no fields at all. A compound type needs at least one member, so HDF5 refused with "H5Tcreate size must be positive" from several call layers down. All three input kinds are normalized before the check, so a field-less struct, a column-less table and an empty containers.Map report the same error. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Assert that a zero-row table gives each column its own member type, and that every column class writes without erroring. A member type can only be derived from an empty column for some classes, and each column is placed first because that is the field the scalar-struct row count is read from. Cover the no-fields error for all three input kinds. Verified to fail without the fix: a uint32 column comes back as a cell array, i.e. written as a string. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Use camelCase variable name
ehennestad
force-pushed
the
fix-empty-compound-dataset-write
branch
from
September 1, 2026 11:40
5ce3703 to
46cf771
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Motivation
Background — This surfaced while adding a user-facing API for HERD, the type that records which external entities the terms in a file refer to. A HERD holds six compound datasets that the schema all marks required, so a file whose HERD has no external references yet holds six empty ones. That is a state a file legitimately reaches: asking a file for its external resources attaches a HERD before anything has been added to it, and PyNWB writes an empty HERD for the same sequence. The cross-reference columns of those tables are
uint32, which is how the mistyping below surfaced. Writing and reading such a dataset were both affected; this PR is the write half, and #880 is the read half.Problem — A compound dataset with no rows could not be written correctly. Every column was written as a string on disk regardless of what it held, so an integer column read back as text, and the same column changed member type depending on whether the table happened to be empty. A compound dataset holding a table with no rows also counted as a missing required property, so a file containing one could not be exported at all.
Separately, data carrying no fields at all reported an HDF5 error from several call layers down rather than saying what was wrong.
Solution — Take the row count from the input, and read the columns of a zero-row table directly so each keeps its own type. Treat a zero-row table as a value rather than an omission when checking required properties, and name the no-fields case in its own error.
What changed
NWB:WriteCompound:NoFieldsinstead ofMATLAB:imagesci:hdf5lib:libraryError/ "H5Tcreate size must be positive". A field-less struct, a column-less table and an emptycontainers.Mapall report it.Text columns are unaffected: a text column is written as a variable length string whether or not the table has rows. This covers the write path only. Reading a zero-row compound dataset back returns an empty array without member names, which #880 fixes.
Implementation notes
The row count used to be read back off the normalized struct, which cannot express it: a scalar struct is written either as a single row of values or as one column per field, and the two were told apart by checking whether the first field held text. Normalizing a zero-row table into that same shape gave a scalar struct a third meaning. It is now taken from the input, and each input kind is normalized in its own branch, so the text check applies only to a scalar struct or
containers.Map— the one input that really is ambiguous.table2structreturns a 0x1 struct array for a zero-row table, and the per-column classes cannot be recovered from it, so the columns are read off the table directly. Numeric and logical columns keep their class; every other column is carried as an empty cellstr, which is the member type a text column already gets. Classes that cannot be typed from an empty value, such as object references, therefore also write as strings.types.untyped.MetaClass/checkRequiredPropsused a bareisemptytest, which is true for a table with no rows. It now excludes tables, so an explicitly typed empty table counts as a value.Examples
Member types of a zero-row compound dataset
Before
After
The same column with rows already wrote as
H5T_INTEGER, so the member type no longer depends on whether the table is empty.Exporting a type whose compound datasets hold no rows
Building a
types.hdmf_common.HERDwhose six tables are all empty, assigning it tonwb.general_external_resources, then callingnwbExport(nwb, 'empty_herd.nwb'):Before
After
Writing data that has no fields
Before
After
How to test
Unit test:
Checklist
fix #XXwhereXXis the issue number?🤖 Generated with Claude Code