Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/include/firebird/impl/msg/jrd.h
Original file line number Diff line number Diff line change
Expand Up @@ -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")
1 change: 1 addition & 0 deletions src/include/gen/Firebird.pas
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
280 changes: 277 additions & 3 deletions src/jrd/blb.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<UCHAR*>(page->blp_page);
const ULONG dataLength = std::min<ULONG>(m_dataPageSize - m_byteOffsetInPage, m_newLength - m_written);
fb_assert(dataLength <= m_dataPageSize);

memcpy(data + m_byteOffsetInPage, reinterpret_cast<const UCHAR*>(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)
{
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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<const UCHAR*>(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)
{
Expand Down Expand Up @@ -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: <pages> <record>
//
// <record> contains unflushed data and is easy to modify.
// <pages> 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 <record>.
//
// 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 / <page size>
// PINTER_PAGE_ID = NUMBER_OF_USED_PAGES / <number of IDs per pointer page> .
// 2. Determine the target data page:
// DATA_PAGE_ID = NUMBER_OF_USED_PAGES % <number of IDs per pointer page>.
// 3. Compute the byte offset within the data page:
// BYTE_OFFSET = position % <page size>.
// 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 <record>.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thanks, but.. where you get the terminology you used ? I'm sorry, but it is very, very hard to read - I need constantly translate into familiar terms.

There is no level of blob pages. There is level of blob.

There are blob pointer pages and blob data pages, and blob record contains blob data (for level-0 blobs) or array of pointers (for non-0 level blobs).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Sorry for the inaccuracy; I have virtually no experience working with pages, so I used the existing code as an example. It has a blob_page class for both the BLOB data page and the BLOB pointer page, so I misnamed it.
Thanks to your clarification, all the terminology is now correct (I hope).


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 (<record>)
{
blob_page* page = (blob_page*) getBuffer();
memcpy(reinterpret_cast<char*>(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<blob_page*>(getBuffer());
helper.replaceInPage(page);
fb_assert(helper.getWrittenLength() == length);
return;
}

// Work with data page
window.win_page = dataPagesIt.getNextPageId();
page = reinterpret_cast<blob_page*>(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<blob_page*>(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<blob_page*>(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)
{
Expand Down Expand Up @@ -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<UCHAR*>(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<const UCHAR*>(buffer), length); // Append
}
44 changes: 44 additions & 0 deletions src/jrd/blb.h
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
#include "../common/classes/ImplementHelper.h"
#include "../common/dsc.h"
#include "../jrd/Resources.h"
#include "err_proto.h"

namespace Ods
{
Expand Down Expand Up @@ -133,6 +134,14 @@ class blb : public pool_alloc<type_blb>
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,
Expand All @@ -142,6 +151,41 @@ class blb : public pool_alloc<type_blb>
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<class BufferType, class SizeType>
requires((std::is_same_v<BufferType, void> || std::is_same_v<BufferType, UCHAR>) && std::is_integral_v<SizeType>)
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<const BufferType*>(reinterpret_cast<const UCHAR*>(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
Expand Down
Loading
Loading