diff --git a/+file/+internal/isSchemaDefinedTableCategory.m b/+file/+internal/isSchemaDefinedTableCategory.m new file mode 100644 index 000000000..86c81210b --- /dev/null +++ b/+file/+internal/isSchemaDefinedTableCategory.m @@ -0,0 +1,9 @@ +function tf = isSchemaDefinedTableCategory(propertyInfo, namespace) +% isSchemaDefinedTableCategory - Determine whether a property stores a +% schema-defined AlignedDynamicTable category. + + tf = isa(propertyInfo, 'file.Group') ... + && ~propertyInfo.isConstrainedSet ... + && ~isempty(propertyInfo.type) ... + && file.internal.isDescendantOf(propertyInfo.type, namespace, 'DynamicTable'); +end diff --git a/+file/+internal/isSchemaDefinedTableColumn.m b/+file/+internal/isSchemaDefinedTableColumn.m new file mode 100644 index 000000000..f51763d36 --- /dev/null +++ b/+file/+internal/isSchemaDefinedTableColumn.m @@ -0,0 +1,11 @@ +function tf = isSchemaDefinedTableColumn(propertyInfo, namespace) +% isSchemaDefinedTableColumn - Determine whether a property stores a +% schema-defined DynamicTable column. + + tf = isscalar(propertyInfo) ... + && isa(propertyInfo, 'file.Dataset') ... + && ~propertyInfo.isConstrainedSet ... + && ~isempty(propertyInfo.type) ... + && file.internal.isDescendantOf( ... + propertyInfo.type, namespace, 'VectorData'); +end diff --git a/+file/fillClass.m b/+file/fillClass.m index 0ac289b74..bd46ff12e 100644 --- a/+file/fillClass.m +++ b/+file/fillClass.m @@ -175,6 +175,15 @@ {fullPropertyDefinition, schemaCategoryPropertyBlock}, newline); end + if file.internal.isDescendantOf(name, namespace, 'DynamicTable') + columnNames = collectSchemaDefinedTableColumns( ... + classprops, nonInherited, namespace); + schemaColumnPropertyBlock = file.fillPrivateConstantProperty( ... + 'DeclaredSchemaColumns', columnNames); + fullPropertyDefinition = strjoin(... + {fullPropertyDefinition, schemaColumnPropertyBlock}, newline); + end + constructorBody = file.fillConstructor(... name,... superclassNames{1},... @@ -265,17 +274,22 @@ for iProperty = 1:length(propertyNames) propertyName = propertyNames{iProperty}; propertyInfo = classProps(propertyName); - if isSchemaDefinedAlignedDynamicTableCategory(propertyInfo, namespace) + if file.internal.isSchemaDefinedTableCategory(propertyInfo, namespace) categoryNames(end+1) = string(propertyName); %#ok end end end -function tf = isSchemaDefinedAlignedDynamicTableCategory(propertyInfo, namespace) - tf = isa(propertyInfo, 'file.Group') ... - && ~propertyInfo.isConstrainedSet ... - && ~isempty(propertyInfo.type) ... - && file.internal.isDescendantOf(propertyInfo.type, namespace, 'DynamicTable'); +function columnNames = collectSchemaDefinedTableColumns( ... + classProps, propertyNames, namespace) + columnNames = string.empty(1, 0); + for iProperty = 1:length(propertyNames) + propertyName = propertyNames{iProperty}; + propertyInfo = classProps(propertyName); + if file.internal.isSchemaDefinedTableColumn(propertyInfo, namespace) + columnNames(end+1) = string(propertyName); %#ok + end + end end function propertyBlockStr = createSchemaNameMappingBlock(schemaNames) diff --git a/+file/getPropertyHooks.m b/+file/getPropertyHooks.m index 0ca72a8d8..e6c89942d 100644 --- a/+file/getPropertyHooks.m +++ b/+file/getPropertyHooks.m @@ -7,8 +7,7 @@ fullClassName = namespace.getFullClassName(typeName); - if strcmp(fullClassName, 'types.hdmf_common.DynamicTable') ... - && strcmp(propName, 'colnames') + if strcmp(typeName, 'DynamicTable') && strcmp(propName, 'colnames') hooks.ValidatorLines = { ... 'val = types.util.dynamictable.validateColnames(val);' }; end @@ -19,29 +18,18 @@ 'val = obj.validateCategoryNames(val);' }; end - if file.internal.isDescendantOf(typeName, namespace, 'DynamicTable') ... - && isSchemaDefinedDynamicTableColumn(propName, prop) + isNamedTableColumn = file.internal.isDescendantOf(typeName, namespace, 'DynamicTable') ... + && file.internal.isSchemaDefinedTableColumn(prop, namespace) ... + && ~endsWith(propName, '_index'); + if isNamedTableColumn hooks.PostsetStatements = { ... sprintf('types.util.dynamictable.syncNamedColumn(obj, ''%s'');', propName) }; end - if file.internal.isDescendantOf(typeName, namespace, 'AlignedDynamicTable') ... - && isSchemaDefinedAlignedDynamicTableCategory(prop, namespace) + isNamedTableCategory = file.internal.isDescendantOf(typeName, namespace, 'AlignedDynamicTable') ... + && file.internal.isSchemaDefinedTableCategory(prop, namespace); + if isNamedTableCategory hooks.PostsetStatements = [hooks.PostsetStatements, { ... sprintf('obj.ensureCategoryNameRegistered(''%s'');', propName) }]; end end - -function tf = isSchemaDefinedDynamicTableColumn(propName, prop) - tf = isa(prop, 'file.Dataset') ... - && ~prop.isConstrainedSet ... - && ~strcmp(propName, 'id') ... - && ~endsWith(propName, '_index'); -end - -function tf = isSchemaDefinedAlignedDynamicTableCategory(prop, namespace) - tf = isa(prop, 'file.Group') ... - && ~prop.isConstrainedSet ... - && ~isempty(prop.type) ... - && file.internal.isDescendantOf(prop.type, namespace, 'DynamicTable'); -end diff --git a/+matnwb/+neurodata/DynamicTableBase.m b/+matnwb/+neurodata/DynamicTableBase.m index 1f46d1621..c1964c9a1 100644 --- a/+matnwb/+neurodata/DynamicTableBase.m +++ b/+matnwb/+neurodata/DynamicTableBase.m @@ -193,6 +193,19 @@ function clear(obj) end methods (Hidden) + function columnNames = getSchemaDefinedColumns(obj) + % getSchemaDefinedColumns - Return schema-defined column names. + % + % Generated DynamicTable classes declare their local schema column + % names as private constants. Aggregate them across the generated + % neurodata type hierarchy so inherited columns are included. + + import matnwb.neurodata.internal.collectConstantPropertiesAcrossHierarchy + + columnNames = collectConstantPropertiesAcrossHierarchy( ... + class(obj), 'DeclaredSchemaColumns'); + end + function ensureDynamicTableConsistency(obj) % ensureDynamicTableConsistency - Ensure DynamicTable column consistency. % diff --git a/+tests/+unit/+schema/DynamicTableColumnTest.m b/+tests/+unit/+schema/DynamicTableColumnTest.m new file mode 100644 index 000000000..f74ad3035 --- /dev/null +++ b/+tests/+unit/+schema/DynamicTableColumnTest.m @@ -0,0 +1,37 @@ +classdef DynamicTableColumnTest < tests.unit.abstract.SchemaTest +% DynamicTableColumnTest - Test schema-defined DynamicTable column detection. + + properties (Constant) + SchemaFolder = "dynamicTableColumnSchema" + SchemaNamespaceFileName = "dtc.namespace.yaml" + end + + methods (Test) + function testVectorDataDatasetUsesGeneratedProperty(testCase) + dynamicTable = types.dtc.MixedDatasetTable( ... + 'description', 'test table'); + schemaColumn = types.hdmf_common.VectorData( ... + 'description', 'schema column', ... + 'data', single((1:3)')); + + dynamicTable.addColumn('schema_column', schemaColumn); + + testCase.verifyEqual(dynamicTable.schema_column, schemaColumn) + testCase.verifyFalse( ... + dynamicTable.vectordata.isKey('schema_column')) + end + + function testNonColumnDatasetRemainsPropertyCollision(testCase) + dynamicTable = types.dtc.MixedDatasetTable( ... + 'description', 'test table'); + invalidColumn = types.hdmf_common.VectorData( ... + 'description', 'invalid column', ... + 'data', single((1:3)')); + + testCase.verifyError( ... + @() dynamicTable.addColumn( ... + 'table_metadata', invalidColumn), ... + 'NWB:DynamicTable:AddColumn:InvalidPropertyCollision') + end + end +end diff --git a/+tests/+unit/dynamicTableTest.m b/+tests/+unit/dynamicTableTest.m index a2f6f4fc1..9cbedd46c 100644 --- a/+tests/+unit/dynamicTableTest.m +++ b/+tests/+unit/dynamicTableTest.m @@ -391,6 +391,29 @@ function testAddColumnUsesSchemaPropertyForTimeSeriesReferenceColumn(testCase) testCase.verifyFalse(timeIntervals.vectordata.isKey('timeseries')); end + function testAddColumnUsesSchemaPropertiesForRaggedSchemaColumn(testCase) + timeIntervals = types.core.TimeIntervals( ... + 'description', 'test time intervals'); + + tags = types.hdmf_common.VectorData( ... + 'description', 'tags', ... + 'data', {'a'; 'b'; 'c'}); + tagsIndex = types.hdmf_common.VectorIndex( ... + 'description', 'tag indices', ... + 'data', uint64([2; 3]), ... + 'target', types.untyped.ObjectView(tags)); + + timeIntervals.addColumn('tags', tags, 'tags_index', tagsIndex); + + % Both halves of a ragged schema column are stored on their + % generated property, but only the data column is a colname. + testCase.verifyEqual(timeIntervals.tags, tags); + testCase.verifyEqual(timeIntervals.tags_index, tagsIndex); + testCase.verifyFalse(timeIntervals.vectordata.isKey('tags')); + testCase.verifyFalse(timeIntervals.vectordata.isKey('tags_index')); + testCase.verifyEqual(timeIntervals.colnames, {'tags'}); + end + function testAddColumnWrongTypeForSchemaPropertyKeepsPropertyRouting(testCase) timeIntervals = types.core.TimeIntervals( ... 'description', 'test time intervals'); diff --git a/+tests/test-schema/dynamicTableColumnSchema/dtc.namespace.yaml b/+tests/test-schema/dynamicTableColumnSchema/dtc.namespace.yaml new file mode 100644 index 000000000..80e70674d --- /dev/null +++ b/+tests/test-schema/dynamicTableColumnSchema/dtc.namespace.yaml @@ -0,0 +1,7 @@ +namespaces: +- full_name: DynamicTable Column Schema Test + name: dtc + schema: + - namespace: core + - source: dtc.tables.yaml + version: 1.0.0 diff --git a/+tests/test-schema/dynamicTableColumnSchema/dtc.tables.yaml b/+tests/test-schema/dynamicTableColumnSchema/dtc.tables.yaml new file mode 100644 index 000000000..68877db09 --- /dev/null +++ b/+tests/test-schema/dynamicTableColumnSchema/dtc.tables.yaml @@ -0,0 +1,14 @@ +groups: +- neurodata_type_def: MixedDatasetTable + neurodata_type_inc: DynamicTable + doc: DynamicTable containing a column dataset and a non-column dataset. + datasets: + - name: schema_column + neurodata_type_inc: VectorData + doc: A schema-defined table column. + quantity: '?' + - name: table_metadata + neurodata_type_inc: Data + dtype: text + doc: Dataset metadata that is not a table column. + quantity: '?' diff --git a/+types/+core/ElectrodesTable.m b/+types/+core/ElectrodesTable.m index dea0d360c..1030fbf2f 100644 --- a/+types/+core/ElectrodesTable.m +++ b/+types/+core/ElectrodesTable.m @@ -23,6 +23,9 @@ y; % (VectorData) y coordinate of the channel location in the brain (+y is inferior). Units should be specified in microns. z; % (VectorData) z coordinate of the channel location in the brain (+z is right). Units should be specified in microns. end +properties (Constant, Access = private) + DeclaredSchemaColumns = ["filtering", "group", "group_name", "imp", "location", "reference", "rel_x", "rel_y", "rel_z", "x", "y", "z"]; +end methods function obj = ElectrodesTable(varargin) diff --git a/+types/+core/EventsTable.m b/+types/+core/EventsTable.m index 7073239a3..6c2a04fd8 100644 --- a/+types/+core/EventsTable.m +++ b/+types/+core/EventsTable.m @@ -15,6 +15,9 @@ duration; % (DurationVectorData) Optional column containing the duration of each event, in seconds. A value of NaN can be used for events without a duration or with a duration that is not yet specified. source_description; % (char) Optional short text description of where the events came from, applying to every row in the table. For example, "Acquisition system" for events emitted directly by the acquisition system (e.g., TTL edges or hardware event channels); "Thresholding of analog signal ANALOG1 at 3 V" for events produced by a detection algorithm run on acquired data; or "Manual video review" for events added by a human annotator. This is a free-text label of origin only; use `description` for the longer narrative of how the event times were computed (channels used, encoding scheme, algorithm parameters, etc.). end +properties (Constant, Access = private) + DeclaredSchemaColumns = ["annotation", "duration", "timestamp"]; +end methods function obj = EventsTable(varargin) diff --git a/+types/+core/ExperimentalConditionsTable.m b/+types/+core/ExperimentalConditionsTable.m index a3ae0815d..a4c4b22d3 100644 --- a/+types/+core/ExperimentalConditionsTable.m +++ b/+types/+core/ExperimentalConditionsTable.m @@ -10,6 +10,9 @@ repetitions; % REQUIRED (DynamicTableRegion) A reference to one or more rows in the RepetitionsTable table. repetitions_index; % REQUIRED (VectorIndex) Index dataset for the repetitions column. end +properties (Constant, Access = private) + DeclaredSchemaColumns = ["repetitions", "repetitions_index"]; +end methods function obj = ExperimentalConditionsTable(varargin) diff --git a/+types/+core/FrequencyBandsTable.m b/+types/+core/FrequencyBandsTable.m index ceba7804d..491baafba 100644 --- a/+types/+core/FrequencyBandsTable.m +++ b/+types/+core/FrequencyBandsTable.m @@ -15,6 +15,9 @@ band_mean; % (VectorData) The mean Gaussian filters, in Hz. band_stdev; % (VectorData) The standard deviation of Gaussian filters, in Hz. end +properties (Constant, Access = private) + DeclaredSchemaColumns = ["band_limits", "band_mean", "band_name", "band_stdev"]; +end methods function obj = FrequencyBandsTable(varargin) diff --git a/+types/+core/IntracellularElectrodesTable.m b/+types/+core/IntracellularElectrodesTable.m index 122d57322..b1b552ae7 100644 --- a/+types/+core/IntracellularElectrodesTable.m +++ b/+types/+core/IntracellularElectrodesTable.m @@ -9,6 +9,9 @@ properties electrode; % REQUIRED (VectorData) Column for storing the reference to the intracellular electrode. end +properties (Constant, Access = private) + DeclaredSchemaColumns = ["electrode"]; +end methods function obj = IntracellularElectrodesTable(varargin) diff --git a/+types/+core/IntracellularRecordingsTable.m b/+types/+core/IntracellularRecordingsTable.m index 109fd846e..64eb43d22 100644 --- a/+types/+core/IntracellularRecordingsTable.m +++ b/+types/+core/IntracellularRecordingsTable.m @@ -14,6 +14,9 @@ properties (Constant, Access = private) DeclaredSchemaCategories = ["electrodes", "responses", "stimuli"]; end +properties (Constant, Access = private) + DeclaredSchemaColumns = string.empty(1, 0); +end methods function obj = IntracellularRecordingsTable(varargin) diff --git a/+types/+core/IntracellularResponsesTable.m b/+types/+core/IntracellularResponsesTable.m index 734a2c843..d17ba6d42 100644 --- a/+types/+core/IntracellularResponsesTable.m +++ b/+types/+core/IntracellularResponsesTable.m @@ -9,6 +9,9 @@ properties response; % REQUIRED (TimeSeriesReferenceVectorData) Column storing the reference to the recorded response for the recording (rows) end +properties (Constant, Access = private) + DeclaredSchemaColumns = ["response"]; +end methods function obj = IntracellularResponsesTable(varargin) diff --git a/+types/+core/IntracellularStimuliTable.m b/+types/+core/IntracellularStimuliTable.m index 8762d56e5..ec0baa159 100644 --- a/+types/+core/IntracellularStimuliTable.m +++ b/+types/+core/IntracellularStimuliTable.m @@ -13,6 +13,9 @@ properties stimulus_template; % (TimeSeriesReferenceVectorData) Column storing the reference to the stimulus template for the recording (rows). end +properties (Constant, Access = private) + DeclaredSchemaColumns = ["stimulus", "stimulus_template"]; +end methods function obj = IntracellularStimuliTable(varargin) diff --git a/+types/+core/PlaneSegmentation.m b/+types/+core/PlaneSegmentation.m index 4c8d71101..a9f24b306 100644 --- a/+types/+core/PlaneSegmentation.m +++ b/+types/+core/PlaneSegmentation.m @@ -18,6 +18,9 @@ voxel_mask; % (VectorData) Voxel masks for each ROI: a list of indices and weights for the ROI. Voxel masks are concatenated and parsing of this dataset is maintained by the PlaneSegmentation. At least one of `image_mask`, `pixel_mask`, or `voxel_mask` is required. voxel_mask_index; % (VectorIndex) Index into voxel_mask. end +properties (Constant, Access = private) + DeclaredSchemaColumns = ["image_mask", "pixel_mask", "pixel_mask_index", "voxel_mask", "voxel_mask_index"]; +end methods function obj = PlaneSegmentation(varargin) diff --git a/+types/+core/RepetitionsTable.m b/+types/+core/RepetitionsTable.m index 26b0ece50..8f2bafee4 100644 --- a/+types/+core/RepetitionsTable.m +++ b/+types/+core/RepetitionsTable.m @@ -10,6 +10,9 @@ sequential_recordings; % REQUIRED (DynamicTableRegion) A reference to one or more rows in the SequentialRecordingsTable table. sequential_recordings_index; % REQUIRED (VectorIndex) Index dataset for the sequential_recordings column. end +properties (Constant, Access = private) + DeclaredSchemaColumns = ["sequential_recordings", "sequential_recordings_index"]; +end methods function obj = RepetitionsTable(varargin) diff --git a/+types/+core/SequentialRecordingsTable.m b/+types/+core/SequentialRecordingsTable.m index d663a98c6..324fae3c4 100644 --- a/+types/+core/SequentialRecordingsTable.m +++ b/+types/+core/SequentialRecordingsTable.m @@ -11,6 +11,9 @@ simultaneous_recordings_index; % REQUIRED (VectorIndex) Index dataset for the simultaneous_recordings column. stimulus_type; % REQUIRED (VectorData) The type of stimulus used for the sequential recording. end +properties (Constant, Access = private) + DeclaredSchemaColumns = ["simultaneous_recordings", "simultaneous_recordings_index", "stimulus_type"]; +end methods function obj = SequentialRecordingsTable(varargin) diff --git a/+types/+core/SimultaneousRecordingsTable.m b/+types/+core/SimultaneousRecordingsTable.m index e6e7e5689..a6853c66a 100644 --- a/+types/+core/SimultaneousRecordingsTable.m +++ b/+types/+core/SimultaneousRecordingsTable.m @@ -10,6 +10,9 @@ recordings; % REQUIRED (DynamicTableRegion) A reference to one or more rows in the IntracellularRecordingsTable table. recordings_index; % REQUIRED (VectorIndex) Index dataset for the recordings column. end +properties (Constant, Access = private) + DeclaredSchemaColumns = ["recordings", "recordings_index"]; +end methods function obj = SimultaneousRecordingsTable(varargin) diff --git a/+types/+core/SweepTable.m b/+types/+core/SweepTable.m index f6c5acbca..8ac6ebb58 100644 --- a/+types/+core/SweepTable.m +++ b/+types/+core/SweepTable.m @@ -11,6 +11,9 @@ series_index; % REQUIRED (VectorIndex) Index for series. sweep_number; % REQUIRED (VectorData) Sweep number of the PatchClampSeries in that row. end +properties (Constant, Access = private) + DeclaredSchemaColumns = ["series", "series_index", "sweep_number"]; +end methods function obj = SweepTable(varargin) diff --git a/+types/+core/TimeIntervals.m b/+types/+core/TimeIntervals.m index e7c6d0a37..428e6c7be 100644 --- a/+types/+core/TimeIntervals.m +++ b/+types/+core/TimeIntervals.m @@ -17,6 +17,9 @@ timeseries; % (TimeSeriesReferenceVectorData) An index into a TimeSeries object. timeseries_index; % (VectorIndex) Index for timeseries. end +properties (Constant, Access = private) + DeclaredSchemaColumns = ["start_time", "stop_time", "tags", "tags_index", "timeseries", "timeseries_index"]; +end methods function obj = TimeIntervals(varargin) diff --git a/+types/+core/Units.m b/+types/+core/Units.m index 9ea742d20..2c4721b14 100644 --- a/+types/+core/Units.m +++ b/+types/+core/Units.m @@ -30,6 +30,9 @@ waveforms_index_index; % (VectorIndex) Index into the 'waveforms_index' dataset. One value for every unit (row in the table). See 'waveforms' for more detail. waveforms_sampling_rate; % (single) Sampling rate, in hertz. end +properties (Constant, Access = private) + DeclaredSchemaColumns = ["electrode_group", "electrodes", "electrodes_index", "obs_intervals", "obs_intervals_index", "spike_times", "spike_times_index", "waveform_mean", "waveform_sd", "waveforms", "waveforms_index", "waveforms_index_index"]; +end methods function obj = Units(varargin) diff --git a/+types/+hdmf_common/AlignedDynamicTable.m b/+types/+hdmf_common/AlignedDynamicTable.m index 7788be205..33fd3b623 100644 --- a/+types/+hdmf_common/AlignedDynamicTable.m +++ b/+types/+hdmf_common/AlignedDynamicTable.m @@ -19,6 +19,9 @@ properties (Constant, Access = private) DeclaredSchemaCategories = string.empty(1, 0); end +properties (Constant, Access = private) + DeclaredSchemaColumns = string.empty(1, 0); +end methods function obj = AlignedDynamicTable(varargin) diff --git a/+types/+hdmf_common/DynamicTable.m b/+types/+hdmf_common/DynamicTable.m index 59656a149..2e447d4f6 100644 --- a/+types/+hdmf_common/DynamicTable.m +++ b/+types/+hdmf_common/DynamicTable.m @@ -19,6 +19,9 @@ properties (Constant, Access = private) GroupPropertyNames = ["vectordata"]; end +properties (Constant, Access = private) + DeclaredSchemaColumns = string.empty(1, 0); +end methods function obj = DynamicTable(varargin) diff --git a/+types/+hdmf_common/MeaningsTable.m b/+types/+hdmf_common/MeaningsTable.m index a69037929..8d16501ac 100644 --- a/+types/+hdmf_common/MeaningsTable.m +++ b/+types/+hdmf_common/MeaningsTable.m @@ -11,6 +11,9 @@ target; % REQUIRED (VectorData) Link to the VectorData object for which this table provides meanings. value; % REQUIRED (VectorData) The value of a row in the linked VectorData object. end +properties (Constant, Access = private) + DeclaredSchemaColumns = ["meaning", "value"]; +end methods function obj = MeaningsTable(varargin) diff --git a/+types/+util/+dynamictable/addVarargColumn.m b/+types/+util/+dynamictable/addVarargColumn.m index 7d3baccd4..a7a92bc6b 100644 --- a/+types/+util/+dynamictable/addVarargColumn.m +++ b/+types/+util/+dynamictable/addVarargColumn.m @@ -7,10 +7,11 @@ function addVarargColumn(DynamicTable, varargin) parse(p, varargin{:}); newColNames = DynamicTable.validate_colnames(fieldnames(p.Unmatched)); newVectorData = p.Unmatched; -storageTargets = resolveStorageTargets(DynamicTable, newColNames, struct2cell(newVectorData)); +[storageTargets, storageNames] = resolveStorageTargets(DynamicTable, newColNames); % Check if any of the new columns already exist in the table -existingCols = getExistingColumns(DynamicTable, newColNames, storageTargets); +existingCols = getExistingColumns( ... + DynamicTable, newColNames, storageTargets, storageNames); assert(isempty(existingCols), ... 'NWB:DynamicTable:AddColumn:ColumnExists', ... 'Column(s) { %s } already exist in the table', strjoin(existingCols, ', ')); @@ -56,20 +57,24 @@ function addVarargColumn(DynamicTable, varargin) validateColumnHeight(new_cn, currentColumnHeight, tableHeight) end - assignColumn(DynamicTable, new_cn, new_cv, storageTargets{i}); + assignColumn( ... + DynamicTable, new_cn, new_cv, storageTargets{i}, storageNames{i}); updateColnames(DynamicTable, new_cn, new_cv) end end -function storageTargets = resolveStorageTargets(dynamicTable, columnNames, columnData) +function [storageTargets, storageNames] = resolveStorageTargets(dynamicTable, columnNames) storageTargets = cell(size(columnNames)); + storageNames = cell(size(columnNames)); for i = 1:length(columnNames) - storageTargets{i} = types.util.dynamictable.resolveColumnStorage( ... - dynamicTable, columnNames{i}, columnData{i}); + [storageTargets{i}, storageNames{i}] = ... + types.util.dynamictable.resolveColumnStorage( ... + dynamicTable, columnNames{i}); end end -function existingCols = getExistingColumns(dynamicTable, newColNames, storageTargets) +function existingCols = getExistingColumns( ... + dynamicTable, newColNames, storageTargets, storageNames) existingCols = {}; if ~isempty(dynamicTable.colnames) @@ -84,7 +89,7 @@ function addVarargColumn(DynamicTable, varargin) switch storageTargets{i} case 'property' - if ~isempty(dynamicTable.(newColumnName)) + if ~isempty(dynamicTable.(storageNames{i})) existingCols{end+1} = newColumnName; %#ok end case 'vectordata' @@ -96,7 +101,8 @@ function addVarargColumn(DynamicTable, varargin) end end -function assignColumn(DynamicTable, columnName, columnValue, storageTarget) +function assignColumn( ... + DynamicTable, columnName, columnValue, storageTarget, storageName) assert(any(strcmp(storageTarget, {'property', 'vectordata'})), ... 'NWB:DynamicTable:AddColumn:InternalError', ... 'Unrecognized storage target `%s` for column `%s`.', ... @@ -104,9 +110,9 @@ function assignColumn(DynamicTable, columnName, columnValue, storageTarget) switch storageTarget case 'property' - DynamicTable.(columnName) = columnValue; + DynamicTable.(storageName) = columnValue; case 'vectordata' - DynamicTable.vectordata.set(columnName, columnValue); + DynamicTable.vectordata.set(storageName, columnValue); end end diff --git a/+types/+util/+dynamictable/resolveColumnStorage.m b/+types/+util/+dynamictable/resolveColumnStorage.m index b10954681..935b5128f 100644 --- a/+types/+util/+dynamictable/resolveColumnStorage.m +++ b/+types/+util/+dynamictable/resolveColumnStorage.m @@ -1,4 +1,4 @@ -function storageTarget = resolveColumnStorage(dynamicTable, columnName, columnData) +function [storageTarget, storageName] = resolveColumnStorage(dynamicTable, columnName) %resolveColumnStorage - Determine where an added column should be stored. % Schema-backed table columns are stored on the object property itself. % All other columns are stored in the generic vectordata set. @@ -6,25 +6,26 @@ arguments dynamicTable {matnwb.common.validation.mustBeDynamicTable} columnName (1,1) string - columnData {matnwb.common.validation.mustBeVectorData} end - + + storageName = char(columnName); + schemaNameMapping = io.internal.getSchemaPropertyNameMapping(dynamicTable); + propertyName = io.internal.getPropertyNameForSchemaName( ... + schemaNameMapping, storageName); + if dynamicTable.isDynamicProperty(columnName) storageTarget = 'vectordata'; return end - if ~isprop(dynamicTable, columnName) - storageTarget = 'vectordata'; - return; - end - - if canAssignToProperty(dynamicTable, columnName, columnData) + schemaColumnNames = dynamicTable.getSchemaDefinedColumns(); + if any(schemaColumnNames == columnName) + storageName = propertyName; storageTarget = 'property'; - return; + return end - if ~isSchemaColumnProperty(dynamicTable, columnName) + if isprop(dynamicTable, propertyName) newException = MException('NWB:DynamicTable:AddColumn:InvalidPropertyCollision', ... ['Cannot add column `%s` because it collides with non-column property ' ... '`%s` on `%s`.'], ... @@ -32,46 +33,5 @@ throwAsCaller(newException) end - storageTarget = 'property'; -end - -function tf = canAssignToProperty(dynamicTable, columnName, columnData) - dummyTable = feval(class(dynamicTable)); - dummyColumn = feval(class(columnData)); - tf = tryAssignToProperty(dummyTable, columnName, dummyColumn); -end - -function tf = isSchemaColumnProperty(dynamicTable, columnName) - dummyTable = feval(class(dynamicTable)); - dummyColumns = getDummyColumnObjects(); - - tf = false; - for i = 1:length(dummyColumns) - if tryAssignToProperty(dummyTable, columnName, dummyColumns{i}) - tf = true; - return; - end - end -end - -function tf = tryAssignToProperty(dynamicTable, columnName, columnData) - tf = false; - try - dynamicTable.(columnName) = columnData; - tf = true; - catch - % Assignment failed, so this value is not accepted for the property. - end -end - -function dummyColumns = getDummyColumnObjects() - dummyColumns = { - types.hdmf_common.VectorData() - types.hdmf_common.VectorIndex() - types.hdmf_common.DynamicTableRegion() - }; - - if exist('types.core.TimeSeriesReferenceVectorData', 'class') - dummyColumns{end+1} = types.core.TimeSeriesReferenceVectorData(); - end + storageTarget = 'vectordata'; end