LibWeb: Seek IndexedDB record ranges

Add a helper for finding key-range slices in sorted record vectors. Use
it for object-store and index range operations.
This commit is contained in:
Andreas Kling 2026-05-11 09:02:49 +02:00 committed by Andreas Kling
parent 41c3989433
commit 2fda5ffc9b
3 changed files with 109 additions and 77 deletions

View file

@ -8,6 +8,7 @@
#include <LibWeb/IndexedDB/Internal/Index.h>
#include <LibWeb/IndexedDB/Internal/MutationLog.h>
#include <LibWeb/IndexedDB/Internal/ObjectStore.h>
#include <LibWeb/IndexedDB/Internal/RecordRange.h>
namespace Web::IndexedDB {
@ -87,17 +88,18 @@ void Index::clear_records()
Optional<IndexRecord&> Index::first_in_range(GC::Ref<IDBKeyRange> range)
{
return m_records.first_matching([&](auto const& record) {
return range->is_in_range(record.key);
});
auto record_range = record_range_for_key_range(m_records, range);
if (record_range.start == record_range.end)
return {};
return m_records[record_range.start];
}
GC::ConservativeVector<IndexRecord> Index::first_n_in_range(GC::Ref<IDBKeyRange> range, Optional<WebIDL::UnsignedLong> count)
{
GC::ConservativeVector<IndexRecord> records(range->heap());
for (auto const& record : m_records) {
if (range->is_in_range(record.key))
records.append(record);
auto record_range = record_range_for_key_range(m_records, range);
for (size_t i = record_range.start; i < record_range.end; ++i) {
records.append(m_records[i]);
if (count.has_value() && records.size() >= *count)
break;
@ -109,9 +111,10 @@ GC::ConservativeVector<IndexRecord> Index::first_n_in_range(GC::Ref<IDBKeyRange>
GC::ConservativeVector<IndexRecord> Index::last_n_in_range(GC::Ref<IDBKeyRange> range, Optional<WebIDL::UnsignedLong> count)
{
GC::ConservativeVector<IndexRecord> records(range->heap());
for (auto const& record : m_records.in_reverse()) {
if (range->is_in_range(record.key))
records.append(record);
auto record_range = record_range_for_key_range(m_records, range);
for (size_t i = record_range.end; i > record_range.start;) {
--i;
records.append(m_records[i]);
if (count.has_value() && records.size() >= *count)
break;
@ -122,12 +125,8 @@ GC::ConservativeVector<IndexRecord> Index::last_n_in_range(GC::Ref<IDBKeyRange>
u64 Index::count_records_in_range(GC::Ref<IDBKeyRange> range)
{
u64 count = 0;
for (auto const& record : m_records) {
if (range->is_in_range(record.key))
++count;
}
return count;
auto record_range = record_range_for_key_range(m_records, range);
return record_range.end - record_range.start;
}
void Index::store_a_record(IndexRecord const& record)

View file

@ -9,6 +9,7 @@
#include <LibWeb/IndexedDB/IDBKeyRange.h>
#include <LibWeb/IndexedDB/Internal/MutationLog.h>
#include <LibWeb/IndexedDB/Internal/ObjectStore.h>
#include <LibWeb/IndexedDB/Internal/RecordRange.h>
namespace Web::IndexedDB {
@ -63,49 +64,17 @@ void ObjectStore::remove_records_in_range(GC::Ref<IDBKeyRange> range)
return;
// Since records are sorted by key, records in range form a contiguous block.
// Binary search for the first record in range.
size_t lo = 0;
size_t hi = m_records.size();
auto record_range = record_range_for_key_range(m_records, range);
auto lower = range->lower_key();
if (lower) {
// Find the first record with key >= lower (or > lower if lower_open).
size_t l = 0, h = m_records.size();
while (l < h) {
size_t mid = l + (h - l) / 2;
auto cmp = Key::compare_two_keys(m_records[mid].key, *lower);
if (cmp < 0 || (cmp == 0 && range->lower_open()))
l = mid + 1;
else
h = mid;
}
lo = l;
}
auto upper = range->upper_key();
if (upper) {
// Find the first record with key > upper (or >= upper if upper_open).
size_t l = lo, h = m_records.size();
while (l < h) {
size_t mid = l + (h - l) / 2;
auto cmp = Key::compare_two_keys(m_records[mid].key, *upper);
if (cmp < 0 || (cmp == 0 && !range->upper_open()))
l = mid + 1;
else
h = mid;
}
hi = l;
}
if (lo < hi) {
if (record_range.start < record_range.end) {
if (m_mutation_log) {
Vector<ObjectStoreRecord> deleted;
deleted.ensure_capacity(hi - lo);
for (size_t i = lo; i < hi; ++i)
deleted.ensure_capacity(record_range.end - record_range.start);
for (size_t i = record_range.start; i < record_range.end; ++i)
deleted.append(move(m_records[i]));
m_mutation_log->note_records_deleted(move(deleted));
}
m_records.remove(lo, hi - lo);
m_records.remove(record_range.start, record_range.end - record_range.start);
}
}
@ -132,34 +101,26 @@ void ObjectStore::store_a_record(ObjectStoreRecord record)
m_mutation_log->note_record_stored(record.key);
// NOTE: The record is stored in the object stores list of records such that the list is sorted according to the key of the records in ascending order.
// We use binary search to find the correct insertion position.
size_t lo = 0;
size_t hi = m_records.size();
while (lo < hi) {
size_t mid = lo + (hi - lo) / 2;
if (Key::compare_two_keys(m_records[mid].key, record.key) < 0)
lo = mid + 1;
else
hi = mid;
if (m_records.is_empty() || Key::compare_two_keys(m_records.last().key, record.key) <= 0) {
m_records.append(move(record));
return;
}
m_records.insert(lo, move(record));
m_records.insert(first_record_index_with_key_at_or_after(m_records, record.key, false), move(record));
}
u64 ObjectStore::count_records_in_range(GC::Ref<IDBKeyRange> range)
{
u64 count = 0;
for (auto const& record : m_records) {
if (range->is_in_range(record.key))
++count;
}
return count;
auto record_range = record_range_for_key_range(m_records, range);
return record_range.end - record_range.start;
}
Optional<ObjectStoreRecord&> ObjectStore::first_in_range(GC::Ref<IDBKeyRange> range)
{
return m_records.first_matching([&](auto const& record) {
return range->is_in_range(record.key);
});
auto record_range = record_range_for_key_range(m_records, range);
if (record_range.start == record_range.end)
return {};
return m_records[record_range.start];
}
void ObjectStore::clear_records()
@ -221,9 +182,9 @@ void ObjectStore::possibly_update_the_key_generator(GC::Ref<Key> key)
GC::ConservativeVector<ObjectStoreRecord> ObjectStore::first_n_in_range(GC::Ref<IDBKeyRange> range, Optional<WebIDL::UnsignedLong> count)
{
GC::ConservativeVector<ObjectStoreRecord> records(range->heap());
for (auto const& record : m_records) {
if (range->is_in_range(record.key))
records.append(record);
auto record_range = record_range_for_key_range(m_records, range);
for (size_t i = record_range.start; i < record_range.end; ++i) {
records.append(m_records[i]);
if (count.has_value() && records.size() >= *count)
break;
@ -235,9 +196,10 @@ GC::ConservativeVector<ObjectStoreRecord> ObjectStore::first_n_in_range(GC::Ref<
GC::ConservativeVector<ObjectStoreRecord> ObjectStore::last_n_in_range(GC::Ref<IDBKeyRange> range, Optional<WebIDL::UnsignedLong> count)
{
GC::ConservativeVector<ObjectStoreRecord> records(range->heap());
for (auto const& record : m_records.in_reverse()) {
if (range->is_in_range(record.key))
records.append(record);
auto record_range = record_range_for_key_range(m_records, range);
for (size_t i = record_range.end; i > record_range.start;) {
--i;
records.append(m_records[i]);
if (count.has_value() && records.size() >= *count)
break;

View file

@ -0,0 +1,71 @@
/*
* Copyright (c) 2026-present, the Ladybird developers.
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/Types.h>
#include <LibGC/Ptr.h>
#include <LibWeb/IndexedDB/IDBKeyRange.h>
namespace Web::IndexedDB {
struct RecordRange {
size_t start { 0 };
size_t end { 0 };
};
template<typename Records>
static size_t first_record_index_with_key_at_or_after(Records const& records, GC::Ref<Key> key, bool open)
{
size_t low = 0;
size_t high = records.size();
while (low < high) {
size_t middle = low + (high - low) / 2;
auto comparison = Key::compare_two_keys(records[middle].key, key);
if (comparison < 0 || (comparison == 0 && open))
low = middle + 1;
else
high = middle;
}
return low;
}
template<typename Records>
static size_t first_record_index_with_key_after(Records const& records, GC::Ref<Key> key, bool open, size_t start = 0)
{
size_t low = start;
size_t high = records.size();
while (low < high) {
size_t middle = low + (high - low) / 2;
auto comparison = Key::compare_two_keys(records[middle].key, key);
if (comparison < 0 || (comparison == 0 && !open))
low = middle + 1;
else
high = middle;
}
return low;
}
template<typename Records>
static RecordRange record_range_for_key_range(Records const& records, GC::Ref<IDBKeyRange> range)
{
size_t start = 0;
size_t end = records.size();
if (auto lower = range->lower_key())
start = first_record_index_with_key_at_or_after(records, *lower, range->lower_open());
if (auto upper = range->upper_key())
end = first_record_index_with_key_after(records, *upper, range->upper_open(), start);
return { start, end };
}
}