Skip to content

[codex] Improve dynamic table validation and colnames sync#818

Merged
ehennestad merged 10 commits intomainfrom
improve-dynamic-table-validation
Apr 21, 2026
Merged

[codex] Improve dynamic table validation and colnames sync#818
ehennestad merged 10 commits intomainfrom
improve-dynamic-table-validation

Conversation

@ehennestad
Copy link
Copy Markdown
Collaborator

@ehennestad ehennestad commented Apr 17, 2026

Motivation

This PR fixes gaps in DynamicTable validation and synchronization around colnames.

Changes:

  • Ensure the colnames property contains unique elements
  • Validates that materialized table columns are listed in colnames
  • Updates generated setters for schema-defined DynamicTable columns so direct assignments keep colnames in sync.
  • Adds re-check of DynamicTable configuration during nwbExport to ensure dynamic tables are still valid when writing.

How to test the behavior?

nwbtest('Name','tests.unit.dynamicTableTest')
nwbtest('Name','tests.system.DynamicTableTest')

Example 1: Adding duplicate colnames

vectorDataA = types.hdmf_common.VectorData('description', 'column a', 'data', 1);

dynamicTable = types.hdmf_common.DynamicTable( ...
    'description', 'example', ...
    'colnames', {'columnA', 'columnA'}, ...
    'columnA', vectorDataA );

Before:

colnames: {columnA, columnA}

main accepted duplicate colnames.

After:

Error using types.util.dynamictable.validateColnames
Column names in `colnames` must be unique.

The feature branch rejects duplicate colnames.

Example 2: Adding wrong colname

vectorDataA = types.hdmf_common.VectorData('description', 'column a', 'data', 1);

dynamicTable = types.hdmf_common.DynamicTable( ...
    'description', 'example', ...
    'colnames', {'columnB'}, ...
    'columnA', vectorDataA );

Before:

colnames: {columnB}

main allowed a materialized column to be missing from colnames.

After:

Error using types.util.dynamictable.checkConfig
All materialized DynamicTable columns must be listed in `colnames`.
Missing from `colnames`: columnA

The feature branch detects the mismatch immediately.

Example 3: Adding column w/o colnames automatically updates colnames

timeIntervals = types.core.TimeIntervals( ...
    'description', 'example');

timeIntervals.start_time = types.hdmf_common.VectorData('description', 'start time', 'data', 1);

timeIntervals

Before:

colnames: {}

On main, direct assignment does not sync colnames.

After:

colnames: {start_time}

On the feature branch, direct assignment updates colnames automatically.

Example 4: nwbExport fails for invalid table

vectorDataA = types.hdmf_common.VectorData('description', 'column a', 'data', 1);

dynamicTable = types.hdmf_common.DynamicTable( ...
    'description', 'example', ...
    'colnames', {'columnA'}, ...
    'columnA', vectorDataA );
dynamicTable.colnames = {'columnB', 'columnC'};

nwb = tests.factory.NWBFile();
nwb.acquisition.set('DynamicTable', dynamicTable);
nwbExport(nwb, 'temp_dynamic_table_check_export')

Before:

Export succeeded.

main allowed export to proceed even after colnames drifted out of sync.

After:

Error using nwbExport
The following error was caught when exporting type "types.hdmf_common.DynamicTable" at file location "/acquisition/DynamicTable":
All materialized DynamicTable columns must be listed in `colnames`.
Missing from `colnames`: columnA

The feature branch re-validates DynamicTable configuration during export.

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 Codex

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR tightens DynamicTable invariants around colnames by adding normalization/validation, syncing colnames when schema-defined column properties are assigned, and re-checking table configuration during export to catch drift.

Changes:

  • Added validateColnames (uniqueness + normalization) and syncNamedColumn utilities under types.util.dynamictable.
  • Updated checkConfig to validate colnames and detect materialized columns missing from colnames.
  • Updated class generation to inject validator/setter hooks (and added/updated tests) so direct assignments keep colnames consistent and export re-validates constraints.

Reviewed changes

Copilot reviewed 25 out of 25 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
+types/+util/+dynamictable/validateColnames.m New helper to normalize and enforce uniqueness of colnames.
+types/+util/+dynamictable/syncNamedColumn.m New helper to ensure schema-defined columns are registered in colnames on assignment.
+types/+util/+dynamictable/checkConfig.m Extends DynamicTable config validation (now checks for missing detected columns and normalizes colnames).
+types/+hdmf_common/DynamicTable.m Uses validateColnames in validate_colnames and adds checkCustomConstraint to re-run checkConfig (e.g., on export).
+types/+core/Units.m Adds post-set hooks to sync schema-defined columns into colnames on direct assignment.
+types/+core/TimeIntervals.m Adds post-set hooks to sync schema-defined columns into colnames on direct assignment.
+types/+core/SweepTable.m Adds post-set hooks to sync schema-defined columns into colnames on direct assignment.
+types/+core/SimultaneousRecordingsTable.m Adds post-set hooks to sync schema-defined columns into colnames on direct assignment.
+types/+core/SequentialRecordingsTable.m Adds post-set hooks to sync schema-defined columns into colnames on direct assignment.
+types/+core/RepetitionsTable.m Adds post-set hooks to sync schema-defined columns into colnames on direct assignment.
+types/+core/PlaneSegmentation.m Adds post-set hooks to sync schema-defined columns into colnames on direct assignment.
+types/+core/IntracellularStimuliTable.m Adds post-set hooks to sync schema-defined columns into colnames on direct assignment.
+types/+core/IntracellularResponsesTable.m Adds post-set hooks to sync schema-defined columns into colnames on direct assignment.
+types/+core/IntracellularElectrodesTable.m Adds post-set hooks to sync schema-defined columns into colnames on direct assignment.
+types/+core/FrequencyBandsTable.m Adds post-set hooks to sync schema-defined columns into colnames on direct assignment.
+types/+core/ExperimentalConditionsTable.m Adds post-set hooks to sync schema-defined columns into colnames on direct assignment.
+types/+core/ElectrodesTable.m Adds post-set hooks to sync schema-defined columns into colnames on direct assignment.
+tests/+unit/dynamicTableTest.m Updates/extends tests for duplicate colnames, direct-assignment syncing, and export-time drift detection.
+file/isDynamicTableDescendant.m New generator helper to detect DynamicTable inheritance.
+file/getPropertyHooks.m New generator hook registry for custom validator/setter behavior.
+file/fillValidators.m Injects additional validator lines via generator hooks.
+file/fillSetters.m Adds optional post-set hook generation via generator hooks.
+file/fillCustomConstraint.m Adds custom constraint generation for DynamicTable to re-check config.
+file/fillConstructor.m Uses new file.isDynamicTableDescendant helper when injecting DynamicTable validation into constructors.
+file/fillClass.m Passes type/namespace into fillSetters for hook-aware setter generation.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +8 to +16
colnames = cleanColumnNames(colnames);
if ischar(colnames)
colnames = {colnames};
end

assert(iscellstr(colnames), ...
'NWB:DynamicTable:InvalidColumnNames', ...
'Column names must be a cell array of character vectors.');

Copy link

Copilot AI Apr 20, 2026

Choose a reason for hiding this comment

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

validate_colnames accepts MATLAB string arrays because types.util.checkDtype(...,'char',val) allows isstring(val), but validateColnames later asserts iscellstr(colnames)/ischar(colnames) and will therefore reject string input. Consider normalizing string (and cell arrays of strings) to cellstr early (e.g., colnames = cellstr(colnames) when isstring(colnames)), so the validator is consistent with checkDtype and common MATLAB calling patterns.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

@copilot apply changes based on this feedback

Comment on lines +9 to +15
if ischar(colnames)
colnames = {colnames};
end

assert(iscellstr(colnames), ...
'NWB:DynamicTable:InvalidColumnNames', ...
'Column names must be a cell array of character vectors.');
Copy link

Copilot AI Apr 20, 2026

Choose a reason for hiding this comment

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

validateColnames will also fail for an N-by-M char array (multiple column names stored as padded rows), because it wraps any char input into a 1-element cell and then requires iscellstr. If colnames can be a char matrix (e.g., from some HDF5 string array reads or user input), consider converting multi-row char arrays via cellstr(colnames) before validating uniqueness.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

I don't think char matrix should be supported

Comment thread +types/+util/+dynamictable/checkConfig.m
|| isa(value, 'types.core.VectorData');
isVectorIndex = isa(value, 'types.hdmf_common.VectorIndex') ...
|| isa(value, 'types.core.VectorIndex');
tf = ~isempty(value) && isVectorData && ~isVectorIndex;
Copy link

Copilot AI Apr 20, 2026

Choose a reason for hiding this comment

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

isMaterializedColumn currently treats any non-empty VectorData object as “materialized”, regardless of whether its .data is empty / zero-length (the previous implementation in getDetectedColumnNames explicitly skipped empty Vector.data for DataStub, DataPipe, and in-memory arrays). If the intent is to validate only columns with actual row data, reintroduce the data-emptiness check (or rename the helper/message to reflect the new semantics).

Suggested change
tf = ~isempty(value) && isVectorData && ~isVectorIndex;
hasData = false;
if ~isempty(value) && isVectorData && ~isVectorIndex
hasData = ~isempty(value.data);
end
tf = ~isempty(value) && isVectorData && ~isVectorIndex && hasData;

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

I think the current implementation is correct. Presence of VectorData should be treated as a materialised column. If data is empty, an error should be thrown later in checkConfig if other columns in the same table has a non-zero length. If the property is dropped here because data is empty, the column height check would not flag this empty column as a problem later.

Comment thread +tests/+unit/dynamicTableTest.m
@ehennestad ehennestad marked this pull request as ready for review April 21, 2026 11:57
@ehennestad ehennestad enabled auto-merge April 21, 2026 11:57
@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 21, 2026

Codecov Report

❌ Patch coverage is 97.41379% with 3 lines in your changes missing coverage. Please review.
✅ Project coverage is 95.25%. Comparing base (f6a4dbc) to head (effd91b).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
+types/+util/+dynamictable/validateColnames.m 88.88% 2 Missing ⚠️
+types/+util/+dynamictable/checkConfig.m 95.45% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #818      +/-   ##
==========================================
- Coverage   95.35%   95.25%   -0.10%     
==========================================
  Files         205      209       +4     
  Lines        7430     7508      +78     
==========================================
+ Hits         7085     7152      +67     
- Misses        345      356      +11     

☔ View full report in Codecov by Sentry.
📢 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.

@bendichter bendichter self-requested a review April 21, 2026 14:28
@ehennestad ehennestad added this pull request to the merge queue Apr 21, 2026
Merged via the queue into main with commit faf3801 Apr 21, 2026
18 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.

4 participants