diff --git a/CHANGELOG.md b/CHANGELOG.md index 8a27ea841..261043161 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 -## Upcoming [0.4.0] (~August 2026) +## [0.4.0] - 2026-09-03 ### Added * **Added support for streaming data read of remote NWB files:** @@ -74,6 +74,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * Updated unit tests to create `Subject` for all NWB files generated in the test suite to comply with NWB best practices. `nwbinspector` tests require subject by default now. (@oruebel, [#289](https://github.com/NeurodataWithoutBorders/aqnwb/pull/289) removed this requirements by specifying `--ignore=check_subject_exists` and [#320](https://github.com/NeurodataWithoutBorders/aqnwb/pull/320) removed the ignore to ensure that subject is always present in the test files) ### Fixed +* Fixed overly restrictive ISO 8601 datetime validation in `isISO8601Date()` that incorrectly rejected valid timestamps with optional fractional seconds and UTC (`Z`), basic, or extended numeric timezone offsets. (@copilot, @oruebel, [#340](https://github.com/NeurodataWithoutBorders/aqnwb/pull/340)) * Fixed `get_utc_offset_seconds` to correctly account for daylight saving time using platform-specific APIs (`tm_gmtoff` on Unix/macOS; `_get_timezone` + `_get_dstbias` on Windows), preventing `session_start_time` from being written ~1 hour ahead of UTC during DST (@cboulay, [#295](https://github.com/NeurodataWithoutBorders/aqnwb/pull/295)) * Fixed `resources/utils/schematype_to_aqnwb.py` to facilitate generation of new types: * Emit forward declarations rather than full header includes for referenced generated types, avoiding circular header dependencies. This issue surfaced due to the update to the latest HDMF Common schema types where `DynamicTable` references `MeaningsTable`, which is itself a `DynamicTable` (@copilot, @oruebel, [#300](https://github.com/NeurodataWithoutBorders/aqnwb/pull/300)) diff --git a/CMakeLists.txt b/CMakeLists.txt index 6e7160d36..3d48d451d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -8,7 +8,7 @@ include(cmake/prelude.cmake) project( aqnwb - VERSION 0.3.0 + VERSION 0.4.0 DESCRIPTION "An API to acquire neurophysiology data in the NWB format" LANGUAGES CXX ) diff --git a/src/Utils.hpp b/src/Utils.hpp index 13afb1729..b410b35bb 100644 --- a/src/Utils.hpp +++ b/src/Utils.hpp @@ -191,19 +191,20 @@ static inline std::string getCurrentTime() } /** - * @brief Check that a string is formatted in ISO8601 format + * @brief Check that a string is formatted as an ISO 8601 datetime. * - * This function only validates the regex pattern but does not check that - * the time values specified are indeed valid. + * This function only validates the datetime syntax and requires a timezone + * designator. It does not check that the specified date and time values are + * valid. * * @return bool indicating whether the string is in ISO8601 form */ static inline bool isISO8601Date(const std::string& dateStr) { - // Define the regex pattern for ISO 8601 extended format with timezone offset - // Allow one or more fractional seconds digits + // Require extended date/time fields and a UTC or numeric timezone designator. + // Fractional seconds and the colon in numeric offsets are optional. const std::string iso8601Pattern = - R"(^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d+[+-]\d{2}:\d{2}$)"; + R"(^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?(Z|[+-]\d{2}:?\d{2})$)"; std::regex pattern(iso8601Pattern); // Check if the date string matches the regex pattern diff --git a/src/nwb/hdmf/table/DynamicTable.cpp b/src/nwb/hdmf/table/DynamicTable.cpp index 71d1b4d5c..40a0b875b 100644 --- a/src/nwb/hdmf/table/DynamicTable.cpp +++ b/src/nwb/hdmf/table/DynamicTable.cpp @@ -427,6 +427,23 @@ Status DynamicTable::addRows(const std::vector& rows, // 2. Validate that all rows have the required columns for (const auto& row : rows) { + // 2.1 Validate the keys in the row, e.g., no unknown columns + for (const auto& [key, value] : row) { + if (key == "id") { + std::cerr << "DynamicTable::addRows: Row IDs must be provided via the " + "rowIds parameter, not as an 'id' key in the row data." + << std::endl; + return Status::Failure; + } + if (m_configuredColumnIndices.find(key) == m_configuredColumnIndices.end() + && targetColumns.find(key) == targetColumns.end()) + { + std::cerr << "DynamicTable::addRows: Unknown column '" << key + << "' provided." << std::endl; + return Status::Failure; + } + } + // 2.2 Validate against the configured columns for (const auto& col : m_configuredColumns) { if (targetColumns.find(col.name) != targetColumns.end()) { // Target columns must be present in the row @@ -458,11 +475,20 @@ Status DynamicTable::addRows(const std::vector& rows, } } - // 3. Buffer values for each column + // 3. Buffer values for each column. + // We accumulate all the incoming row data into memory buffers before + // performing any disk I/O. This batching strategy significantly reduces I/O + // overhead and helps ensure consistency: if a type mismatch or validation + // error is found in the incoming data, we can fail early without leaving the + // file partially modified. std::unordered_map columnBuffers; - std::unordered_map> indexBuffers; + std::unordered_map + indexBuffers; + // Initialize empty variants with the correct underlying vector type (e.g. + // std::vector) based on the column's data type, so that std::visit can + // append to them later. for (const auto& col : m_configuredColumns) { auto vectorIndex = std::dynamic_pointer_cast(col.column); if (vectorIndex) { @@ -472,13 +498,16 @@ Status DynamicTable::addRows(const std::vector& rows, std::string targetName = target->getName(); columnBuffers[targetName] = IO::BaseDataType::createEmptyVectorVariant( target->readData()->getDataType()); - indexBuffers[col.name] = std::vector(); + indexBuffers[col.name] = IO::BaseDataType::createEmptyVectorVariant( + vectorIndex->readData()->getDataType()); } else if (targetColumns.find(col.name) == targetColumns.end()) { columnBuffers[col.name] = IO::BaseDataType::createEmptyVectorVariant(col.dataType); } } + // Iterate over each user-provided row and serialize their cell values into + // our memory buffers for (const auto& row : rows) { for (const auto& col : m_configuredColumns) { auto vectorIndex = std::dynamic_pointer_cast(col.column); @@ -489,7 +518,9 @@ Status DynamicTable::addRows(const std::vector& rows, std::string targetName = target->getName(); auto it = row.find(targetName); if (it != row.end()) { - // Also need to account for existing elements in the target column + // For ragged columns, we need to track how many elements exist in the + // target dataset so we know what the boundary index is for the + // current ragged element. size_t existingTargetElements = 0; auto targetData = target->readData(); if (targetData) { @@ -519,9 +550,40 @@ Status DynamicTable::addRows(const std::vector& rows, }, columnBuffers[targetName]); - uint32_t newIndex = - static_cast(existingTargetElements + newTargetSize); - indexBuffers[col.name].push_back(newIndex); + size_t newIndex = existingTargetElements + newTargetSize; + Status indexAppendStatus = std::visit( + [newIndex, &col](auto& vec) -> Status + { + using VecType = std::decay_t; + if constexpr (std::is_same_v) { + return Status::Failure; + } else { + using ValType = typename VecType::value_type; + // std::visit forces the compiler to generate code for ALL + // possible types in BaseDataVectorVariant (including + // std::vector). We use if constexpr to prevent + // compilation errors when casting size_t to a string. + // VectorIndex must be an integer, so we reject other types. + // Note: HDMF formally requires VectorIndex to be unsigned. + // However, we use std::is_integral_v to be lenient and permit + // signed integers as well, in case we are appending to an + // existing non-strict dataset. + if constexpr (std::is_integral_v) { + vec.push_back(static_cast(newIndex)); + return Status::Success; + } else { + std::cerr + << "DynamicTable::addRows: VectorIndex '" << col.name + << "' has invalid non-integral data type." << std::endl; + return Status::Failure; + } + } + }, + indexBuffers[col.name]); + + if (indexAppendStatus != Status::Success) { + return Status::Failure; + } } } else if (targetColumns.find(col.name) == targetColumns.end()) { // Regular column (not a target of a VectorIndex) @@ -549,6 +611,17 @@ Status DynamicTable::addRows(const std::vector& rows, return Status::Failure; std::string targetName = target->getName(); + // Write index buffer + IO::BaseDataType::BaseDataVectorVariant indexVariant = + indexBuffers[col.name]; + Status writeIndexStatus = vectorIndex->appendBuffer(indexVariant); + if (writeIndexStatus != Status::Success) { + std::cerr << "DynamicTable::addRows: Failed to write buffer for index " + "column '" + << col.name << "'." << std::endl; + return Status::Failure; + } + // Write target buffer if (columnBuffers.find(targetName) != columnBuffers.end()) { Status writeStatus = target->appendBuffer(columnBuffers[targetName]); @@ -562,17 +635,6 @@ Status DynamicTable::addRows(const std::vector& rows, // it columnBuffers.erase(targetName); } - - // Write index buffer - IO::BaseDataType::BaseDataVectorVariant indexVariant = - indexBuffers[col.name]; - Status writeIndexStatus = vectorIndex->appendBuffer(indexVariant); - if (writeIndexStatus != Status::Success) { - std::cerr << "DynamicTable::addRows: Failed to write buffer for index " - "column '" - << col.name << "'." << std::endl; - return Status::Failure; - } } else if (targetColumns.find(col.name) == targetColumns.end()) { // Write regular column buffer Status writeStatus = col.column->appendBuffer(columnBuffers[col.name]); @@ -891,6 +953,20 @@ Status DynamicTable::loadConfiguredColumnsFromFile() auto col = readColumn(colName); if (col) { registerColumn(col); + + // Also check for associated index columns because m_colNames only lists + // target columns. A ragged array of ragged arrays would have multiple + // "_index" suffixes. + std::string currentIndexName = colName + "_index"; + while (true) { + auto indexCol = readColumn(currentIndexName); + if (indexCol) { + registerColumn(indexCol); + currentIndexName += "_index"; + } else { + break; + } + } } } diff --git a/src/nwb/hdmf/table/VectorIndex.cpp b/src/nwb/hdmf/table/VectorIndex.cpp index 932550012..dc0cc60fb 100644 --- a/src/nwb/hdmf/table/VectorIndex.cpp +++ b/src/nwb/hdmf/table/VectorIndex.cpp @@ -197,19 +197,11 @@ AQNWB::Types::CellValue VectorIndex::combineCellsToVector( } std::vector VectorIndex::readIndexedCellValues( - SizeType start, SizeType count, SizeType stride, SizeType block) + SizeType start, SizeType count) { if (count == 0) { return {}; } - if (stride == 0) { - throw std::invalid_argument( - "VectorIndex::readIndexedCellValues: stride must be greater than 0"); - } - if (block == 0) { - throw std::invalid_argument( - "VectorIndex::readIndexedCellValues: block must be greater than 0"); - } auto ioPtr = getIO(); if (!ioPtr || !ioPtr->isOpen()) { @@ -229,11 +221,9 @@ std::vector VectorIndex::readIndexedCellValues( SizeArray startArray = {start}; SizeArray countArray = count != AQNWB::Types::SizeTypeNotSet ? SizeArray {count} : SizeArray {}; - SizeArray strideArray = {stride}; - SizeArray blockArray = {block}; - std::vector indices = VectorData::readCellValues( - startArray, countArray, strideArray, blockArray); + std::vector indices = + VectorData::readCellValues(startArray, countArray); if (indices.empty()) { return {}; diff --git a/src/nwb/hdmf/table/VectorIndex.hpp b/src/nwb/hdmf/table/VectorIndex.hpp index d60129dd1..189f57b6e 100644 --- a/src/nwb/hdmf/table/VectorIndex.hpp +++ b/src/nwb/hdmf/table/VectorIndex.hpp @@ -136,8 +136,6 @@ class VectorIndex : public AQNWB::NWB::VectorData * * @param start The starting index for the slice (optional). * @param count The number of elements to read (optional). - * @param stride The stride (optional). - * @param block The block size (optional). * @return A vector of CellValue, where each CellValue contains the data for a * cell. * @throws std::out_of_range if the requested range is invalid (e.g., start + @@ -149,10 +147,7 @@ class VectorIndex : public AQNWB::NWB::VectorData * invalid (e.g., not monotonically increasing). */ std::vector readIndexedCellValues( - SizeType start = 0, - SizeType count = AQNWB::Types::SizeTypeNotSet, - SizeType stride = 1, - SizeType block = 1); + SizeType start = 0, SizeType count = AQNWB::Types::SizeTypeNotSet); // Define read methods DEFINE_REFERENCED_REGISTERED_FIELD( diff --git a/tests/testDynamicTable.cpp b/tests/testDynamicTable.cpp index e7d036400..f2ead7c17 100644 --- a/tests/testDynamicTable.cpp +++ b/tests/testDynamicTable.cpp @@ -929,20 +929,33 @@ TEST_CASE("DynamicTable", "[table]") auto readIds = readTable->readIdColumn()->readData()->values().data; REQUIRE(readIds == std::vector({0, 1, 2})); + // Test Issue #330: Append a row to a ragged column after reopening + std::vector rowsToAppend = { + {{"col_str", std::string("row4")}, + {"ragged_data", std::vector {10, 11}}}}; + status = readTable->addRows(rowsToAppend); + REQUIRE(status == Status::Success); + + // Read back again to check appended row + auto readIdsNew = readTable->readIdColumn()->readData()->values().data; + REQUIRE(readIdsNew == std::vector({0, 1, 2, 3})); + auto colStr = readTable->readColumn("col_str"); auto colStrDataGeneric = colStr->readData()->valuesGeneric(); auto colStrDataTyped = DataBlock::fromGeneric(colStrDataGeneric); auto colStrData = colStrDataTyped.data; - REQUIRE(colStrData.size() == 3); - REQUIRE(colStrData == std::vector({"row1", "row2", "row3"})); + REQUIRE(colStrData.size() == 4); + REQUIRE(colStrData + == std::vector({"row1", "row2", "row3", "row4"})); auto raggedDataCol = readTable->readColumn("ragged_data"); auto raggedDataGeneric = raggedDataCol->readData()->valuesGeneric(); auto raggedDataTyped = DataBlock::fromGeneric(raggedDataGeneric); auto raggedData = raggedDataTyped.data; - REQUIRE(raggedData.size() == 9); - REQUIRE(raggedData == std::vector({1, 2, 3, 4, 5, 6, 7, 8, 9})); + REQUIRE(raggedData.size() == 11); + REQUIRE(raggedData + == std::vector({1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11})); auto raggedIndexCol = readTable->readColumn("ragged_data_index"); @@ -950,12 +963,112 @@ TEST_CASE("DynamicTable", "[table]") auto raggedIndexTyped = DataBlock::fromGeneric(raggedIndexGeneric); auto raggedIndexData = raggedIndexTyped.data; - REQUIRE(raggedIndexData.size() == 3); - REQUIRE(raggedIndexData == std::vector({3, 5, 9})); + REQUIRE(raggedIndexData.size() == 4); + REQUIRE(raggedIndexData == std::vector({3, 5, 9, 11})); io->close(); } + SECTION("test ragged array columns with different data types") + { + auto testRaggedType = [&tablePath](IO::BaseDataType indexType, + const std::string& typeName, + bool shouldSucceed) + { + std::string path = + getTestFilePath("testDynamicTableRaggedArray_" + typeName + ".h5"); + std::shared_ptr io = createIO("HDF5", path); + io->open(); + + auto table = NWB::DynamicTable::create(tablePath, io); + + // Configure schema + std::vector specs; + specs.push_back(NWB::ElementIdentifiers::createDataSpec( + "id", IO::ArrayDataSetConfig(IO::BaseDataType::I32, {0}, {10}))); + specs.push_back(NWB::VectorData::createDataSpec( + "col_str", + IO::ArrayDataSetConfig(IO::BaseDataType::V_STR, {0}, {10}), + "String column")); + + Status status = table->initialize("Table with ragged array", specs); + REQUIRE(status == Status::Success); + + // Add target column for ragged array + auto targetCol = + NWB::VectorData::create(mergePaths(tablePath, "ragged_data"), io); + targetCol->initialize( + IO::ArrayDataSetConfig(IO::BaseDataType::I32, {0}, {10}), + "Target data for ragged array"); + status = table->addColumn(targetCol); + REQUIRE(status == Status::Success); + + // Add VectorIndex column + auto indexCol = NWB::VectorIndex::create( + mergePaths(tablePath, "ragged_data_index"), io); + indexCol->initialize(IO::ArrayDataSetConfig(indexType, {0}, {10}), + "Index for ragged array", + targetCol->getPath()); + status = table->addColumn(indexCol); + REQUIRE(status == Status::Success); + + // Add rows + std::vector rows = { + {{"col_str", std::string("row1")}, + {"ragged_data", std::vector {1, 2, 3}}}}; + + status = table->addRows(rows); + + if (!shouldSucceed) { + REQUIRE(status == Status::Failure); + } else { + REQUIRE(status == Status::Success); + } + + io->close(); + }; + + // Test the 4 unsigned widths. (Issue #332) + testRaggedType(IO::BaseDataType::U8, "U8", true); + testRaggedType(IO::BaseDataType::U16, "U16", true); + testRaggedType(IO::BaseDataType::U32, "U32", true); + testRaggedType(IO::BaseDataType::U64, "U64", true); + + // Note: Signed integers (e.g. I32) and non-integral types (F32, V_STR) + // are rejected by VectorIndex::initialize and thus cannot be created here. + // However, DynamicTable::addRows gracefully tolerates pre-existing signed + // integer indices and explicitly fails on non-integral types. + + // Explicitly test that non-integral index types fail (by verifying that + // VectorIndex::initialize rejects them, fulfilling our contract). + auto testBadIndexInit = + [&tablePath](IO::BaseDataType badIndexType, const std::string& typeName) + { + std::string path = getTestFilePath("testDynamicTableRaggedArray_bad_" + + typeName + ".h5"); + std::shared_ptr io = createIO("HDF5", path); + io->open(); + + auto targetCol = + NWB::VectorData::create(mergePaths(tablePath, "ragged_data"), io); + targetCol->initialize( + IO::ArrayDataSetConfig(IO::BaseDataType::I32, {0}, {10}), + "Target data"); + + auto indexCol = NWB::VectorIndex::create( + mergePaths(tablePath, "ragged_data_index"), io); + REQUIRE_THROWS_AS( + indexCol->initialize(IO::ArrayDataSetConfig(badIndexType, {0}, {10}), + "Index", + targetCol->getPath()), + std::invalid_argument); + io->close(); + }; + + testBadIndexInit(IO::BaseDataType::F32, "F32"); + testBadIndexInit(IO::BaseDataType::V_STR, "V_STR"); + } + SECTION("test readRows") { std::string path = getTestFilePath("testDynamicTableReadRows.h5"); @@ -1213,8 +1326,16 @@ TEST_CASE("DynamicTable", "[table]") REQUIRE(nonExistentCol == nullptr); // Test addRow with missing required column + size_t initialRowCount = table->getNumberOfRows(); AQNWB::Types::RowData missingColRow = {{"wrong_col", std::string("val")}}; REQUIRE(table->addRow(missingColRow) == Status::Failure); + REQUIRE(table->getNumberOfRows() == initialRowCount); + + // Test addRow with an unknown extra column (should fail) + AQNWB::Types::RowData extraColRow = {{"col_str", std::string("val")}, + {"unknown_col", std::string("bad")}}; + REQUIRE(table->addRow(extraColRow) == Status::Failure); + REQUIRE(table->getNumberOfRows() == initialRowCount); // Add a valid row so we can test readRows AQNWB::Types::RowData validRow = {{"col_str", std::string("val")}}; diff --git a/tests/testNWBFile.cpp b/tests/testNWBFile.cpp index e66d816c1..10bca6be5 100644 --- a/tests/testNWBFile.cpp +++ b/tests/testNWBFile.cpp @@ -161,7 +161,7 @@ TEST_CASE("createTimeIntervalsTables", "[nwb]") REQUIRE(io->objectExists("/intervals") == false); // create the Epochs Table - auto epochsTable = nwbfile->createEpochs(false, 50); + auto epochsTable = nwbfile->createEpochs(true, 50); REQUIRE(epochsTable != nullptr); REQUIRE(epochsTable->getName() == "epochs"); @@ -170,21 +170,21 @@ TEST_CASE("createTimeIntervalsTables", "[nwb]") REQUIRE(epochsTable->readDescription()->values().data[0] == "Time intervals marking coarse-grained experimental phases or subdivisions of a recording session, such as baseline, task, rest, or sleep stage"); // create the Trials Table - auto trialsTable = nwbfile->createTrials(false, 50); + auto trialsTable = nwbfile->createTrials(true, 50); REQUIRE(trialsTable != nullptr); REQUIRE(trialsTable->getName() == "trials"); REQUIRE(trialsTable->readDescription()->values().data[0] == "Time intervals corresponding to repeated experimental units with consistent structure, such as individual stimulus-response-reward cycles."); // create the Invalid Times Table - auto invalidTimesTable = nwbfile->createInvalidTimes(false, 50); + auto invalidTimesTable = nwbfile->createInvalidTimes(true, 50); REQUIRE(invalidTimesTable != nullptr); REQUIRE(invalidTimesTable->getName() == "invalid_times"); REQUIRE(invalidTimesTable->readDescription()->values().data[0] == "Time intervals that should be removed from analysis"); // create a custom TimeIntervals Table - auto customTable = nwbfile->createTimeIntervals( - "custom_intervals", "test custom", false, 50); + auto customTable = + nwbfile->createTimeIntervals("custom_intervals", "test custom", true, 50); REQUIRE(customTable != nullptr); REQUIRE(customTable->getName() == "custom_intervals"); REQUIRE(customTable->readDescription()->values().data[0] == "test custom"); diff --git a/tests/testUtilsFunctions.cpp b/tests/testUtilsFunctions.cpp index 16ddcf12c..2515cf426 100644 --- a/tests/testUtilsFunctions.cpp +++ b/tests/testUtilsFunctions.cpp @@ -16,22 +16,20 @@ TEST_CASE("isISO8601Date function tests", "[utils]") REQUIRE(AQNWB::isISO8601Date( "2018-09-28T14:43:54.12345+02:00")); // Allow for too many fractional // seconds + REQUIRE(AQNWB::isISO8601Date("2018-09-28T14:43:54+02:00")); + REQUIRE(AQNWB::isISO8601Date("2018-09-28T14:43:54.123+0200")); + REQUIRE(AQNWB::isISO8601Date("2018-09-28T14:43:54.123Z")); + REQUIRE(AQNWB::isISO8601Date("2018-09-28T14:43:54.123-0800")); } SECTION("Invalid ISO 8601 date strings") { REQUIRE_FALSE(AQNWB::isISO8601Date( "2018-09-28 14:43:54.123+02:00")); // Space instead of 'T' - REQUIRE_FALSE(AQNWB::isISO8601Date( - "2018-09-28T14:43:54+02:00")); // Missing fractional seconds - REQUIRE_FALSE(AQNWB::isISO8601Date( - "2018-09-28T14:43:54.123+0200")); // Missing colon in timezone - REQUIRE_FALSE(AQNWB::isISO8601Date( - "2018-09-28T14:43:54.123Z")); // Missing timezone offset - REQUIRE_FALSE(AQNWB::isISO8601Date( - "2018-09-28T14:43:54.123-0800")); // Incorrect timezone format REQUIRE_FALSE( AQNWB::isISO8601Date("2018-09-28T14:43:54.123")); // Missing timezone + REQUIRE_FALSE(AQNWB::isISO8601Date("2018-09-28T14:43:54+02")); + REQUIRE_FALSE(AQNWB::isISO8601Date("2018-09-28")); REQUIRE_FALSE(AQNWB::isISO8601Date("Random text 1213")); } } @@ -68,6 +66,7 @@ TEST_CASE("Test current time format", "[utils]") SECTION("Time format is valid ISO 8601") { REQUIRE(std::regex_match(time, timeRegex)); + REQUIRE(AQNWB::isISO8601Date(time)); } } diff --git a/tests/testVectorIndex.cpp b/tests/testVectorIndex.cpp index cf829942c..c31ba80c9 100644 --- a/tests/testVectorIndex.cpp +++ b/tests/testVectorIndex.cpp @@ -410,14 +410,10 @@ TEST_CASE("VectorIndex", "[table]") REQUIRE(cells[0].get>() == vec1); REQUIRE(cells[1].get>() == vec2); - // Test readIndexedCellValues with start/count/stride/block + // Test readIndexedCellValues with start/count auto cells2 = vectorIndex->readIndexedCellValues( 0, 0); // count 0 should return empty REQUIRE(cells2.empty()); - // Stride of 0 should throw - REQUIRE_THROWS(vectorIndex->readIndexedCellValues(0, 2, 0)); - // Block of 0 should throw - REQUIRE_THROWS(vectorIndex->readIndexedCellValues(0, 2, 1, 0)); io->close(); }