diff --git a/src/include/firebird/impl/msg/jrd.h b/src/include/firebird/impl/msg/jrd.h index 0595ee9da2e..edbd046213c 100644 --- a/src/include/firebird/impl/msg/jrd.h +++ b/src/include/firebird/impl/msg/jrd.h @@ -1020,3 +1020,4 @@ FB_IMPL_MSG(JRD, 1017, dsql_agg_non_agg_context, -104, "42", "000", "Aggregate f FB_IMPL_MSG(JRD, 1018, dsql_agg_param_not_accum, -204, "42", "000", "Aggregate function input parameters may be referenced only in ON ACCUMULATE DO") FB_IMPL_MSG(JRD, 1019, dsql_agg_exit_group, -204, "42", "000", "EXIT is not allowed in ON GROUP DO section of aggregate function") FB_IMPL_MSG(JRD, 1020, dsql_agg_return, -204, "42", "000", "RETURN is not allowed in ON START DO, ON ACCUMULATE DO or ON FINISH DO sections of aggregate function; use EXIT instead") +FB_IMPL_MSG(JRD, 1021, blob_write_after_the_end, -204, "42", "000", "Cannot write to blob. Position @1 is out of blob length @2") diff --git a/src/include/gen/Firebird.pas b/src/include/gen/Firebird.pas index 93e2512ee4e..94e9c72bdb4 100644 --- a/src/include/gen/Firebird.pas +++ b/src/include/gen/Firebird.pas @@ -6062,6 +6062,7 @@ IPerformanceStatsImpl = class(IPerformanceStats) isc_dsql_agg_param_not_accum = 335545338; isc_dsql_agg_exit_group = 335545339; isc_dsql_agg_return = 335545340; + isc_blob_write_after_the_end = 335545341; isc_gfix_db_name = 335740929; isc_gfix_invalid_sw = 335740930; isc_gfix_incmp_sw = 335740932; diff --git a/src/jrd/blb.cpp b/src/jrd/blb.cpp index c4fdb295d13..1138b11897a 100644 --- a/src/jrd/blb.cpp +++ b/src/jrd/blb.cpp @@ -90,6 +90,97 @@ static void move_to_string(Jrd::thread_db*, dsc*, dsc*); static void slice_callback(array_slice*, ULONG, dsc*); static blb* store_array(thread_db*, jrd_tra*, bid*); +namespace { + +// A helper class to track positions input buffer writing process +class DataModifyHelper +{ +public: + DataModifyHelper(const offset_t position, const void* buffer, const ULONG length, + const USHORT dataPageSize, const USHORT maxDataPagesNumber) + : m_newData(buffer), m_newLength(length), + m_dataPageSize(dataPageSize) + { + m_byteOffsetInPage = position % m_dataPageSize; + } + + // Replace content on blob data page + inline void replaceInPage(blob_page* page) noexcept + { + fb_assert(needWrite()); + + UCHAR* data = reinterpret_cast(page->blp_page); + const ULONG dataLength = std::min(m_dataPageSize - m_byteOffsetInPage, m_newLength - m_written); + fb_assert(dataLength <= m_dataPageSize); + + memcpy(data + m_byteOffsetInPage, reinterpret_cast(m_newData) + m_written, dataLength); + m_written += dataLength; + m_byteOffsetInPage = 0; // Offset only in the first page + }; + + inline bool needWrite() const noexcept + { + return m_written < m_newLength; + } + + [[maybe_unused]] + inline ULONG getWrittenLength() const noexcept + { + return m_written; + } + +private: + const void* m_newData; + const ULONG m_newLength; + + // Where to replace + offset_t m_byteOffsetInPage = 0; + const USHORT m_dataPageSize; + + ULONG m_written = 0; +}; + +// A helper class to track positions pages +class PageIterator +{ +public: + PageIterator(const ULONG pageId, const ULONG* blbPages, const FB_SIZE_T pagesCount) + : m_pageId(pageId), + m_pagesId(blbPages), + m_numberOfPages(pagesCount) + { } + + PageIterator(const ULONG pageId, const vcl& blbPages) + : m_pageId(pageId), + m_pagesId(blbPages.begin()), + m_numberOfPages(blbPages.count()) + { } + + // Get blob pointer page ID (for blob level 2) or a blob data page (for blob level 1) + inline ULONG getNextPageId() noexcept + { + return m_pagesId[m_pageId++]; + } + + inline bool hasPages() const noexcept + { + return m_pageId < m_numberOfPages; + } + +private: + ULONG m_pageId = 0; + const ULONG* m_pagesId = nullptr; // Data pages (for BLOB level 1) or pointer pages (for BLOB level 2) + const ULONG m_numberOfPages = 0; +}; + +// Make sure blob is a temporary blob. If not, complain bitterly. +inline void verifyBlobModifiable(const USHORT blbFlags) +{ + if (!(blbFlags & BLB_temporary) || (blbFlags & BLB_closed)) + ERR_post(Arg::Gds(isc_cannot_update_old_blob)); // Cannot update existing blob +} + +} // namespace void blb::BLB_cancel(thread_db* tdbb) { @@ -1573,6 +1664,20 @@ void blb::BLB_put_data(thread_db* tdbb, const UCHAR* buffer, SLONG length) SET_TDBB(tdbb); const BLOB_PTR* p = buffer; + // BLB_put_segment will remove the flag after the first call so replace the data here + if (blb_flags & BLB_seek) + { + verifyBlobModifiable(blb_flags); + + blb_flags &= ~BLB_seek; + + // Modify part inside existing data + if (modifyBlobChunk(tdbb, blb_seek, p, length)) + return; + + // Continue and append the rest + } + while (length > 0) { // ASF: the comment below was copied from BLB_get_data @@ -1602,10 +1707,18 @@ void blb::BLB_put_segment(thread_db* tdbb, const void* seg, USHORT segment_lengt Database* dbb = tdbb->getDatabase(); const UCHAR* segment = static_cast(seg); - // Make sure blob is a temporary blob. If not, complain bitterly. + verifyBlobModifiable(blb_flags); - if (!(blb_flags & BLB_temporary) || (blb_flags & BLB_closed)) - ERR_post(Arg::Gds(isc_cannot_update_old_blob)); + if (blb_flags & BLB_seek) + { + blb_flags &= ~BLB_seek; + + // Modify part inside existing data + if (modifyBlobChunk(tdbb, blb_seek, segment, segment_length)) + return; + + // Continue and append the rest + } if (blb_filter) { @@ -1944,6 +2057,149 @@ void blb::scalar(thread_db* tdbb, blob->BLB_close(tdbb); } +void blb::modifyData(thread_db* tdbb, offset_t position, const void* buffer, const ULONG length) +{ + // All BLOB data is stored in the following format: + // + // contains unflushed data and is easy to modify. + // must be fetched, marked, modified, and released. + // + // Depending on the BLOB level, the algorithm works as follows: + // + // Level 0: All data is inside blb_buffer. + // This is the simplest case: just perform a memset, and we're done. + // + // Level 1: Flushed data is located on pages (blb_pages), unflushed data is in blb_buffer. + // To modify the data: + // 1. Find the first blob data page that needs modification, read, mark and release it. + // 2. If the remaining data to modify exceeds the current page size, proceed to the next data page. + // 3. If there are no more data pages but there is still data to modify, update the . + // + // Level 2: Flushed data is organized in a pages tree. + // - The blb_pages array contains BLOB pointer pages. + // - Each pointer page holds a list of BLOB data page IDs. + // + // To locate and modify the required page: + // 1. Calculate the pointer page offset: + // NUMBER_OF_USED_PAGES = position / + // PINTER_PAGE_ID = NUMBER_OF_USED_PAGES / . + // 2. Determine the target data page: + // DATA_PAGE_ID = NUMBER_OF_USED_PAGES % . + // 3. Compute the byte offset within the data page: + // BYTE_OFFSET = position % . + // 4. Modify the first relevant data page, then move to the next one. + // 5. If no more data pages are available, advance to the next pointer page, + // read its first data page, and continue modifying next data pages. + // 6. If all pages have been processed but there is still input data left, update the . + + + fb_assert ((blb_flags & BLB_temporary) && !(blb_flags & BLB_closed)); // Can update only new blob + fb_assert(position + length <= blb_length); // Update only existing data + + if (blb_level == 0) // No pages, just a buffer () + { + blob_page* page = (blob_page*) getBuffer(); + memcpy(reinterpret_cast(page->blp_page) + position, buffer, length); + return; + } + + // Use helper to simplify pages modification + const auto maxDataPagesNumber = blb_pointers; + const auto dataPageSize = tdbb->getDatabase()->dbb_page_size - BLP_SIZE; + + const auto numberOfUsedDataPages = position / dataPageSize; + + DataModifyHelper helper(position, buffer, length, dataPageSize, maxDataPagesNumber); + blob_page* page = nullptr; + + WIN window(blb_pg_space_id, -1); + if (blb_flags & BLB_large_scan) + { + window.win_flags = WIN_large_scan; + window.win_scans = 1; + } + + auto releasePage = [&tdbb, &window]() + { + if (window.win_flags & WIN_large_scan) + CCH_RELEASE_TAIL(tdbb, &window); + else + CCH_RELEASE(tdbb, &window); + }; + + // Level 1 blobs are much easier -- page number is in vector. + if (blb_level == 1) + { + // Work with data pages + fb_assert(blb_pages); + + const auto dataPageId = numberOfUsedDataPages; + fb_assert(dataPageId < maxDataPagesNumber); + + // Update data on pages one by one + PageIterator dataPagesIt(dataPageId, *blb_pages); + while (helper.needWrite()) + { + if (!dataPagesIt.hasPages()) // The last data chunk is in the blb_buffer + { + page = reinterpret_cast(getBuffer()); + helper.replaceInPage(page); + fb_assert(helper.getWrittenLength() == length); + return; + } + + // Work with data page + window.win_page = dataPagesIt.getNextPageId(); + page = reinterpret_cast(CCH_FETCH(tdbb, &window, LCK_write, pag_blob)); + + CCH_MARK(tdbb, &window); // Mark as dirty + helper.replaceInPage(page); + releasePage(); + } + } + else + { + fb_assert(blb_level == 2); + + // blb_pages constains pointer pages ID + // A pointer page constains a list of data page IDs + + const ULONG pointerPageId = numberOfUsedDataPages / maxDataPagesNumber; // Example: 100000 / 8000 = 12 + ULONG dataPageId = numberOfUsedDataPages % maxDataPagesNumber; // Example: 100000 % 8000 = 4000 + + PageIterator pointerPagesIt(pointerPageId, *blb_pages); + while (helper.needWrite()) + { + if (!pointerPagesIt.hasPages()) // The last data is in the blb_buffer + { + helper.replaceInPage(page); + fb_assert(helper.getWrittenLength() == length); + return; + } + + // Get pointer page + window.win_page = pointerPagesIt.getNextPageId(); + page = reinterpret_cast(CCH_FETCH(tdbb, &window, LCK_write, pag_blob)); + + // Get data pages one by one and update + const ULONG numberOfDataPages = page->blp_length / sizeof(page->blp_page); + PageIterator dataPagesIt(dataPageId, page->blp_page, numberOfDataPages); + while (dataPagesIt.hasPages() && helper.needWrite()) + { + auto dataPage = reinterpret_cast(CCH_HANDOFF(tdbb, &window, + dataPagesIt.getNextPageId(), + LCK_write, pag_blob)); + + CCH_MARK(tdbb, &window); // Mark as dirty + helper.replaceInPage(dataPage); + } + releasePage(); + + dataPageId = 0; // Offset only for the first pointer pages + } + } + fb_assert(helper.getWrittenLength() == length); +} static ArrayField* alloc_array(jrd_tra* transaction, Ods::InternalArrayDesc* proto_desc) { @@ -3033,3 +3289,21 @@ void blb::BLB_cancel() { BLB_cancel(JRD_get_thread_data()); } + +FB_SIZE_T blb::read(thread_db* tdbb, const offset_t position, void* buffer, const ULONG length) +{ + // Mode 0 - from start + BLB_lseek(0, position); + return BLB_get_data(tdbb, reinterpret_cast(buffer), length, false); +} + +void blb::write(thread_db* tdbb, const offset_t position, const void* buffer, ULONG length) +{ + verifyBlobModifiable(blb_flags); + + // Modify part inside existing data + if (modifyBlobChunk(tdbb, position, buffer, length)) + return; // Only modify, exit + + BLB_put_data(tdbb, reinterpret_cast(buffer), length); // Append +} diff --git a/src/jrd/blb.h b/src/jrd/blb.h index 1a3ded79023..382755fc8ac 100644 --- a/src/jrd/blb.h +++ b/src/jrd/blb.h @@ -40,6 +40,7 @@ #include "../common/classes/ImplementHelper.h" #include "../common/dsc.h" #include "../jrd/Resources.h" +#include "err_proto.h" namespace Ods { @@ -133,6 +134,14 @@ class blb : public pool_alloc return destination; } + // Read form specified position + FB_SIZE_T read(thread_db* tdbb, const offset_t position, void* buffer, const ULONG length); + + // Write data at any position in a temporally (new) blob + // The position of the new buffer must start inside the blob range, but its length may extend beyond it + // Existing data will be overwritten + void write(thread_db* tdbb, const offset_t position, const void* buffer, ULONG length); + private: static blb* allocate_blob(thread_db*, jrd_tra*); static blb* copy_blob(thread_db* tdbb, const bid* source, bid* destination, @@ -142,6 +151,41 @@ class blb : public pool_alloc void insert_page(thread_db*); void destroy(const bool purge_flag); + // Modify data. Throw error on valid length violation + void modifyData(thread_db* tdbb, offset_t position, const void* buffer, const ULONG length); + + // Modify existing data + // Output: + // true: the input range is only inside the blob data + // false: the input range is extends beyond existing data. Modify `buffer` and `length` to return only non-written data + template + requires((std::is_same_v || std::is_same_v) && std::is_integral_v) + bool modifyBlobChunk(thread_db* tdbb, const offset_t position, const BufferType*& buffer, SizeType& length) + { + if (position > blb_length) + { + ERR_post(Firebird::Arg::Gds(isc_blob_write_after_the_end) << + Firebird::Arg::Int64(position) << Firebird::Arg::Int64(blb_length)); + } + + const offset_t end = position + length; + if (end <= blb_length) + { + // Range is inside the current data, replace and report that no extra actions are requeued + modifyData(tdbb, position, buffer, length); + return true; + } + + // Part inside existing data + const offset_t middle = blb_length - position; + modifyData(tdbb, position, buffer, middle); + + // Return only part to append + buffer = reinterpret_cast(reinterpret_cast(buffer) + middle); // Move pointer + length -= middle; + return false; + } + FB_SIZE_T blb_temp_size = 0; // size stored in transaction temp space offset_t blb_temp_offset = 0; // offset in transaction temp space Attachment* blb_attachment = nullptr; // database attachment diff --git a/src/jrd/tests/BlobTest.cpp b/src/jrd/tests/BlobTest.cpp new file mode 100644 index 00000000000..8b30e4f3440 --- /dev/null +++ b/src/jrd/tests/BlobTest.cpp @@ -0,0 +1,396 @@ +#include "boost/test/unit_test.hpp" +#include "../../jrd/blb.h" + +#include "TestContext.h" + +#include + + +BOOST_AUTO_TEST_SUITE(EngineSuite) +BOOST_AUTO_TEST_SUITE(JrdClassesSuite) + + +BOOST_AUTO_TEST_SUITE(BlobTests) + + +static constexpr UCHAR STREAM_BLOB_BPB[] = { + isc_bpb_version1, + isc_bpb_type, 1, isc_bpb_type_stream, +}; + +std::string getDefaultString(std::string_view header = "", int pageNumber = 0, std::optional dum = std::nullopt) +{ + std::string output; + output += header; + + // Bigger page size - level 1 + // Bigger 2 pages - level 2 + auto tdbb = JRD_get_thread_data(); + auto size = tdbb->getDatabase()->dbb_page_size; + + for (int i = 0 ; i < pageNumber; i++) + { + std::string dummy; + dummy.resize(size, dum.value_or('0' + i)); + output += dummy; + } + + return output; +} + +Jrd::blb* makeBlob(Jrd::bid& id, std::string_view testData = "") +{ + auto tdbb = JRD_get_thread_data(); + + Jrd::blb* blob = Jrd::blb::create2(tdbb, tdbb->getTransaction(), &id, sizeof(STREAM_BLOB_BPB), STREAM_BLOB_BPB); + BOOST_REQUIRE(blob != nullptr); + + blob->BLB_put_data(tdbb, (const UCHAR*)testData.data(), testData.length()); + + return blob; +} + + +std::string readBlob(Jrd::bid id) +{ + auto tdbb = JRD_get_thread_data(); + + auto blob = Jrd::blb::open(tdbb, tdbb->getTransaction(), &id); + + std::string buffer; + buffer.resize(blob->blb_length, '\0'); + const ULONG readLength = blob->BLB_get_data(JRD_get_thread_data(), (UCHAR*)buffer.data(), blob->blb_length, true); + + return buffer; +} + +BOOST_AUTO_TEST_SUITE(BlobModificationTests) + +enum class ModifyFunction +{ + WRITE, + PUT_DATA, + PUT_SEGMENT, +}; + +void replaceInBlob(Jrd::thread_db* tdbb, Jrd::blb*& blob, const ULONG pos, const std::string_view replacement, + const ModifyFunction modifyFunction) +{ + switch (modifyFunction) + { + case ModifyFunction::WRITE: + blob->write(tdbb, pos, replacement.data(), replacement.length()); + break; + case ModifyFunction::PUT_SEGMENT: + if (replacement.length() < MAX_USHORT) + { + blob->BLB_lseek(0, pos); + blob->BLB_put_segment(tdbb, (const UCHAR*)replacement.data(), replacement.length()); + break; + } + [[fallthrough]]; + case ModifyFunction::PUT_DATA: + blob->BLB_lseek(0, pos); + blob->BLB_put_data(tdbb, (const UCHAR*)replacement.data(), replacement.length()); + break; + } + + blob->BLB_close(tdbb); + blob = nullptr; +} + +std::string replaceInContent(std::string sourceData, const ULONG posToInplace, const std::string_view contentToInplace) +{ + auto sourceLength = sourceData.length(); + + const auto replacementEnd = posToInplace + contentToInplace.length(); + if (replacementEnd < sourceLength) + { + for (ULONG i = 0; i < contentToInplace.length(); ++i) + { + sourceData[posToInplace + i] = contentToInplace[i]; + } + } + else + { + sourceData.resize(posToInplace); + sourceData += contentToInplace; + } + return sourceData; +} + +static constexpr std::array functions = {ModifyFunction::PUT_DATA, ModifyFunction::PUT_SEGMENT, ModifyFunction::WRITE}; +static void putFunctionInfo(const ModifyFunction func) +{ + switch (func) + { + case ModifyFunction::WRITE: + BOOST_TEST_INFO("Modify function WRITE"); + break; + case ModifyFunction::PUT_SEGMENT: + BOOST_TEST_INFO("Modify function PUT_SEGMENT"); + break; + case ModifyFunction::PUT_DATA: + BOOST_TEST_INFO("Modify function PUT_DATA"); + break; + } +} + +BOOST_FIXTURE_TEST_CASE(Level0WriteTest, EngineHolder) +{ + Jrd::bid id; + + for (auto type : functions) + { + const std::string testData = "Hello World, BLB_get_data, level=0"; + + // Full rewrite + auto blob = makeBlob(id, testData); + std::string buffer; + buffer.resize(blob->blb_length, '*'); + replaceInBlob(tdbb, blob, 0, buffer, type); + BOOST_TEST(readBlob(id) == buffer); + + // Middle write + blob = makeBlob(id, testData); + replaceInBlob(tdbb, blob, 12, " __write_,", type); + BOOST_TEST(readBlob(id) == replaceInContent(testData, 12, " __write_,")); + + // Ending is out of range - add to end + blob = makeBlob(id, testData); + replaceInBlob(tdbb, blob, 27, testData, type); + BOOST_TEST(readBlob(id) == replaceInContent(testData, 27, testData)); + + // Beginning is out of range + blob = makeBlob(id, testData); + BOOST_CHECK_THROW(blob->write(tdbb, 40, (const void*)testData.data(), testData.length()), Firebird::Exception); + } +} + +BOOST_FIXTURE_TEST_CASE(Level1WriteTest, EngineHolder) +{ + Jrd::bid id; + Jrd::blb* blob = nullptr; + + std::string result; + std::string expected; + + const std::string_view testData = "Hello World, BLB_get_data, level=1 | "; + std::string defaultData = getDefaultString(testData, 1); + for (auto type : functions) + { + putFunctionInfo(type); + + // Full rewrite + auto blob = makeBlob(id, defaultData); + + replaceInBlob(tdbb, blob, 0, "new data", type); + + result = readBlob(id); + expected = replaceInContent(defaultData, 0, "new data"); + BOOST_TEST(result == expected); + } + + std::string replacement; + replacement.resize(200, '*'); + + for (auto type : functions) + { + // Middle to end write + blob = makeBlob(id, defaultData); + replaceInBlob(tdbb, blob, blob->blb_length - 200, replacement, type); + + result = readBlob(id); + expected = replaceInContent(defaultData, defaultData.length() - 200, replacement); + BOOST_TEST(result.length() == expected.length()); + BOOST_REQUIRE(result.substr(0, 400) == expected.substr(0, 400)); + BOOST_REQUIRE(result.substr(result.length() - 300) == expected.substr(expected.length() - 300)); + BOOST_TEST(result == expected); + } + + for (auto type : functions) + { + putFunctionInfo(type); + + // Middle + replacement.resize(200, '*'); + blob = makeBlob(id, defaultData); + replaceInBlob(tdbb, blob, blob->blb_length - 4000, replacement, type); + + result = readBlob(id); + expected = replaceInContent(defaultData, defaultData.length() - 4000, replacement); + BOOST_REQUIRE(result.length() == expected.length()); + BOOST_REQUIRE(result.substr(result.length() - 4000, 300) == expected.substr(expected.length() - 4000, 300)); + BOOST_TEST(result == expected); + } + + replacement.clear(); + for (auto type : functions) + { + putFunctionInfo(type); + + // Ending is out of range - add to end + blob = makeBlob(id, defaultData); + replacement.resize(blob->blb_length, '@'); + + const auto insertPos = blob->blb_length - 1000; + replaceInBlob(tdbb, blob, insertPos, replacement, type); + + result = readBlob(id); + expected = replaceInContent(defaultData, insertPos, replacement); + BOOST_REQUIRE(result.length() == expected.length()); + BOOST_REQUIRE(result.substr(0, 400) == expected.substr(0, 400)); + BOOST_REQUIRE(result.substr(result.length() - 300) == expected.substr(expected.length() - 300)); + BOOST_TEST(result == expected); + } + + defaultData = getDefaultString(testData, 8); + replacement = getDefaultString(testData, 3, '*'); + for (auto type : functions) + { + putFunctionInfo(type); + + // Big + blob = makeBlob(id, defaultData); + + const auto insertPos = blob->blb_length / 2; + replaceInBlob(tdbb, blob, insertPos, replacement, type); + + result = readBlob(id); + expected = replaceInContent(defaultData, insertPos, replacement); + BOOST_REQUIRE(result.length() == expected.length()); + + std::string_view resultView(result); + std::string_view expectedView(expected); + for (FB_SIZE_T i = 0; i < expected.length(); i += 1000) + { + auto left = std::min(1000, expected.length() - i); + BOOST_TEST(resultView.substr(i, left) == expected.substr(i, left)); + } + } +} + +BOOST_FIXTURE_TEST_CASE(Level2WriteTest, EngineHolder) +{ + // Takes some time + + Jrd::bid id; + Jrd::blb* blob = nullptr; + + std::string result; + std::string expected; + + const std::string_view testData = "Hello World, BLB_get_data, level=2 | "; + std::string defaultData; + std::string replacement; + + for (auto type : functions) + { + putFunctionInfo(type); + + blob = makeBlob(id, defaultData); + + const auto insertPos = blob->blb_length / 2; + replaceInBlob(tdbb, blob, insertPos, replacement, type); + + result = readBlob(id); + expected = replaceInContent(defaultData, insertPos, replacement); + BOOST_REQUIRE(result.length() == expected.length()); + + std::string_view resultView(result); + std::string_view expectedView(expected); + for (FB_SIZE_T i = 0; i < expected.length(); i += 1000) + { + auto left = std::min(1000, expected.length() - i); + BOOST_TEST_INFO("Chunk position is " + std::to_string(i)); + BOOST_TEST(resultView.substr(i, left) == expected.substr(i, left)); + // if (resultView.substr(i, left) != expected.substr(i, left)) + // break; + } + } + + defaultData = getDefaultString(testData, 5050); + replacement = getDefaultString(testData, 150, '*'); + for (auto type : functions) + { + putFunctionInfo(type); + + blob = makeBlob(id, defaultData); + + const auto insertPos = 1998; + replaceInBlob(tdbb, blob, insertPos, replacement, type); + + result = readBlob(id); + expected = replaceInContent(defaultData, insertPos, replacement); + BOOST_REQUIRE(result.length() == expected.length()); + + std::string_view resultView(result); + std::string_view expectedView(expected); + for (FB_SIZE_T i = 0; i < expected.length(); i += 1000) + { + auto left = std::min(1000, expected.length() - i); + BOOST_TEST_INFO("Chunk position is " + std::to_string(i)); + BOOST_TEST(resultView.substr(i, left) == expected.substr(i, left)); + } + } +} + +BOOST_AUTO_TEST_SUITE_END() // BlobModificationTest + +BOOST_AUTO_TEST_SUITE(BlobModificationTests) + + +BOOST_FIXTURE_TEST_CASE(Level0ReadTest, EngineHolder) +{ + Jrd::bid id; + + const std::string_view testData = "Hello World, read, level=0"; + auto blob = makeBlob(id, testData); + blob->BLB_close(tdbb); + blob = Jrd::blb::open(tdbb, tdbb->getTransaction(), &id); + + std::string buffer; + buffer.resize(blob->blb_length, '\0'); + blob->read(tdbb, 0, buffer.data(), buffer.length()); + BOOST_TEST(buffer == testData); + + auto read = blob->read(tdbb, 5, buffer.data(), buffer.length()); + BOOST_TEST(read == buffer.length() - 5); + + BOOST_TEST(buffer.substr(0, read) == testData.substr(5)); + + blob->BLB_close(tdbb); +} + +BOOST_FIXTURE_TEST_CASE(Level1ReadTest, EngineHolder) +{ + Jrd::bid id; + + std::string result; + std::string expected; + + const std::string_view testData = "Hello World, read, level=1 | "; + std::string defaultData = getDefaultString(testData, 8); + + auto blob = makeBlob(id, defaultData); + blob->BLB_close(tdbb); + blob = Jrd::blb::open(tdbb, tdbb->getTransaction(), &id); + + std::string buffer; + buffer.resize(blob->blb_length, '\0'); + auto read = blob->read(tdbb, blob->blb_length / 2, buffer.data(), buffer.length()); + BOOST_TEST(buffer.substr(0, read) == defaultData.substr(blob->blb_length / 2, read)); + + read = blob->read(tdbb, 0, buffer.data(), buffer.length()); + BOOST_TEST(read == buffer.length()); + BOOST_TEST(buffer == defaultData); + + blob->BLB_close(tdbb); +} + +BOOST_AUTO_TEST_SUITE_END() // BlobModificationTests + +BOOST_AUTO_TEST_SUITE_END() // BlobTests + +BOOST_AUTO_TEST_SUITE_END() // JrdClassesSuite +BOOST_AUTO_TEST_SUITE_END() // EngineSuite diff --git a/src/jrd/tests/TestContext.cpp b/src/jrd/tests/TestContext.cpp new file mode 100644 index 00000000000..1d3525d12c6 --- /dev/null +++ b/src/jrd/tests/TestContext.cpp @@ -0,0 +1,81 @@ +#include "TestContext.h" +#include "../jrd/req.h" + +#include "../jrd/trace/TraceManager.h" + +#include "boost/test/unit_test.hpp" + +#include + +// It is to slow to create a new DB for each test, so create it once and keep in cache +class CachedAttach +{ +public: + CachedAttach(Firebird::MemoryPool& pool) : + status(pool) + { + dbPath = std::filesystem::temp_directory_path() / "engine_utests.fdb"; + removeDb(); + } + + Jrd::JAttachment* getAttachment() + { + // Delay init + if (att != nullptr) + return att; + + att = prov.createDatabase(&status, dbPath.string().data(), 0, nullptr); + BOOST_REQUIRE(att); + + tra = att->startTransaction(&status, 0, nullptr); + BOOST_REQUIRE(tra); + + statement = att->prepare(&status, tra, 0, "select 1 from rdb$database", 3, 0); + + return att; + } + + ~CachedAttach() + { + att->release(); + Jrd::TraceManager::getStorage()->shutdown(); + removeDb(); + } + + void removeDb() + { + if (std::filesystem::exists(dbPath)) + std::filesystem::remove(dbPath); + } + + Jrd::JProvider prov{nullptr}; + Firebird::FbLocalStatus status; + + + std::filesystem::path dbPath; + Jrd::JAttachment* att = nullptr; + + Jrd::JTransaction* tra = nullptr; + Jrd::JStatement* statement = nullptr; +}; +static Firebird::GlobalPtr storage; + + +TestContextHolder::TestContextHolder() : + m_tdbb(&storage->status, storage->getAttachment(), FB_FUNCTION), + pool(*getDefaultMemoryPool()) +{ + m_tdbb->setRequest(storage->statement->getHandle()->getRequest()); + m_tdbb->setTransaction(storage->tra->getHandle()); + + ISC_TIMESTAMP ts = {}; + m_tdbb->getRequest()->setGmtTimeStamp(ts); + + tdbb = JRD_get_thread_data(); +} + +TestContextHolder::~TestContextHolder() +{ + m_tdbb->setRequest(nullptr); + m_tdbb->setTransaction(nullptr); +} diff --git a/src/jrd/tests/TestContext.h b/src/jrd/tests/TestContext.h new file mode 100644 index 00000000000..b2c5050208a --- /dev/null +++ b/src/jrd/tests/TestContext.h @@ -0,0 +1,23 @@ +#ifndef TEST_CONTEXT_H +#define TEST_CONTEXT_H + +#include "firebird.h" +#include "../jrd/jrd.h" + +class TestContextHolder +{ +public: + TestContextHolder(); + ~TestContextHolder(); + +private: + Jrd::EngineContextHolder m_tdbb; // must be at stack + +public: // Available in tests + Firebird::MemoryPool& pool; + Jrd::thread_db* tdbb{}; +}; + +using EngineHolder = TestContextHolder; + +#endif // TEST_CONTEXT_H