blob: e19aa13eb01e55bb0cf8081f743baa723a632555 [file] [log] [blame]
/*
* Copyright (C) 2014, 2016 Apple Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
* THE POSSIBILITY OF SUCH DAMAGE.
*/
#include "config.h"
#include "SQLiteIDBCursor.h"
#if ENABLE(INDEXED_DATABASE)
#include "IDBCursorInfo.h"
#include "IDBGetResult.h"
#include "IDBSerialization.h"
#include "Logging.h"
#include "SQLiteIDBBackingStore.h"
#include "SQLiteIDBTransaction.h"
#include "SQLiteStatement.h"
#include "SQLiteTransaction.h"
#include <sqlite3.h>
#include <wtf/text/StringBuilder.h>
namespace WebCore {
namespace IDBServer {
static const size_t prefetchLimit = 256;
static const size_t prefetchSizeLimit = 1 * MB;
std::unique_ptr<SQLiteIDBCursor> SQLiteIDBCursor::maybeCreate(SQLiteIDBTransaction& transaction, const IDBCursorInfo& info)
{
auto cursor = makeUnique<SQLiteIDBCursor>(transaction, info);
if (!cursor->establishStatement())
return nullptr;
if (!cursor->advance(1))
return nullptr;
return cursor;
}
std::unique_ptr<SQLiteIDBCursor> SQLiteIDBCursor::maybeCreateBackingStoreCursor(SQLiteIDBTransaction& transaction, const uint64_t objectStoreID, const uint64_t indexID, const IDBKeyRangeData& range)
{
auto cursor = makeUnique<SQLiteIDBCursor>(transaction, objectStoreID, indexID, range);
if (!cursor->establishStatement())
return nullptr;
if (!cursor->advance(1))
return nullptr;
return cursor;
}
SQLiteIDBCursor::SQLiteIDBCursor(SQLiteIDBTransaction& transaction, const IDBCursorInfo& info)
: m_transaction(&transaction)
, m_cursorIdentifier(info.identifier())
, m_objectStoreID(info.objectStoreIdentifier())
, m_indexID(info.cursorSource() == IndexedDB::CursorSource::Index ? info.sourceIdentifier() : IDBIndexInfo::InvalidId)
, m_cursorDirection(info.cursorDirection())
, m_cursorType(info.cursorType())
, m_keyRange(info.range())
{
ASSERT(m_objectStoreID);
}
SQLiteIDBCursor::SQLiteIDBCursor(SQLiteIDBTransaction& transaction, const uint64_t objectStoreID, const uint64_t indexID, const IDBKeyRangeData& range)
: m_transaction(&transaction)
, m_cursorIdentifier(transaction.transactionIdentifier())
, m_objectStoreID(objectStoreID)
, m_indexID(indexID ? indexID : IDBIndexInfo::InvalidId)
, m_cursorDirection(IndexedDB::CursorDirection::Next)
, m_cursorType(IndexedDB::CursorType::KeyAndValue)
, m_keyRange(range)
, m_backingStoreCursor(true)
{
ASSERT(m_objectStoreID);
}
SQLiteIDBCursor::~SQLiteIDBCursor()
{
if (m_backingStoreCursor)
m_transaction->closeCursor(*this);
}
void SQLiteIDBCursor::currentData(IDBGetResult& result, const Optional<IDBKeyPath>& keyPath, ShouldIncludePrefetchedRecords shouldIncludePrefetchedRecords)
{
ASSERT(!m_fetchedRecords.isEmpty());
auto& currentRecord = m_fetchedRecords.first();
if (currentRecord.completed) {
ASSERT(!currentRecord.errored);
result = { };
return;
}
if (shouldIncludePrefetchedRecords == ShouldIncludePrefetchedRecords::No) {
result = { currentRecord.record.key, currentRecord.record.primaryKey, IDBValue(currentRecord.record.value), keyPath };
return;
}
Vector<IDBCursorRecord> prefetchedRecords;
prefetchedRecords.reserveCapacity(m_fetchedRecords.size());
for (auto& record : m_fetchedRecords) {
if (record.isTerminalRecord())
break;
prefetchedRecords.append(record.record);
}
// First record will be returned as current record.
if (!prefetchedRecords.isEmpty())
prefetchedRecords.remove(0);
result = { currentRecord.record.key, currentRecord.record.primaryKey, IDBValue(currentRecord.record.value), keyPath, WTFMove(prefetchedRecords) };
}
static String buildPreIndexStatement(bool isDirectionNext)
{
StringBuilder builder;
builder.appendLiteral("SELECT rowid, key, value FROM IndexRecords WHERE indexID = ? AND key = CAST(? AS TEXT) AND value ");
if (isDirectionNext)
builder.append('>');
else
builder.append('<');
builder.appendLiteral(" CAST(? AS TEXT) ORDER BY value");
if (!isDirectionNext)
builder.appendLiteral(" DESC");
builder.append(';');
return builder.toString();
}
static String buildIndexStatement(const IDBKeyRangeData& keyRange, IndexedDB::CursorDirection cursorDirection)
{
StringBuilder builder;
builder.appendLiteral("SELECT rowid, key, value FROM IndexRecords WHERE indexID = ? AND key ");
if (!keyRange.lowerKey.isNull() && !keyRange.lowerOpen)
builder.appendLiteral(">=");
else
builder.append('>');
builder.appendLiteral(" CAST(? AS TEXT) AND key ");
if (!keyRange.upperKey.isNull() && !keyRange.upperOpen)
builder.appendLiteral("<=");
else
builder.append('<');
builder.appendLiteral(" CAST(? AS TEXT) ORDER BY key");
if (cursorDirection == IndexedDB::CursorDirection::Prev || cursorDirection == IndexedDB::CursorDirection::Prevunique)
builder.appendLiteral(" DESC");
builder.appendLiteral(", value");
if (cursorDirection == IndexedDB::CursorDirection::Prev)
builder.appendLiteral(" DESC");
builder.append(';');
return builder.toString();
}
static String buildObjectStoreStatement(const IDBKeyRangeData& keyRange, IndexedDB::CursorDirection cursorDirection)
{
StringBuilder builder;
builder.appendLiteral("SELECT rowid, key, value FROM Records WHERE objectStoreID = ? AND key ");
if (!keyRange.lowerKey.isNull() && !keyRange.lowerOpen)
builder.appendLiteral(">=");
else
builder.append('>');
builder.appendLiteral(" CAST(? AS TEXT) AND key ");
if (!keyRange.upperKey.isNull() && !keyRange.upperOpen)
builder.appendLiteral("<=");
else
builder.append('<');
builder.appendLiteral(" CAST(? AS TEXT) ORDER BY key");
if (cursorDirection == IndexedDB::CursorDirection::Prev || cursorDirection == IndexedDB::CursorDirection::Prevunique)
builder.appendLiteral(" DESC");
builder.append(';');
return builder.toString();
}
bool SQLiteIDBCursor::establishStatement()
{
ASSERT(!m_statement);
String sql;
if (m_indexID != IDBIndexInfo::InvalidId) {
sql = buildIndexStatement(m_keyRange, m_cursorDirection);
m_boundID = m_indexID;
} else {
sql = buildObjectStoreStatement(m_keyRange, m_cursorDirection);
m_boundID = m_objectStoreID;
}
m_currentLowerKey = m_keyRange.lowerKey.isNull() ? IDBKeyData::minimum() : m_keyRange.lowerKey;
m_currentUpperKey = m_keyRange.upperKey.isNull() ? IDBKeyData::maximum() : m_keyRange.upperKey;
return createSQLiteStatement(sql);
}
bool SQLiteIDBCursor::createSQLiteStatement(const String& sql)
{
LOG(IndexedDB, "Creating cursor with SQL query: \"%s\"", sql.utf8().data());
ASSERT(!m_currentLowerKey.isNull());
ASSERT(!m_currentUpperKey.isNull());
ASSERT(m_transaction->sqliteTransaction());
m_statement = makeUnique<SQLiteStatement>(m_transaction->sqliteTransaction()->database(), sql);
if (m_statement->prepare() != SQLITE_OK) {
LOG_ERROR("Could not create cursor statement (prepare/id) - '%s'", m_transaction->sqliteTransaction()->database().lastErrorMsg());
return false;
}
return bindArguments();
}
void SQLiteIDBCursor::objectStoreRecordsChanged()
{
if (m_statementNeedsReset)
return;
ASSERT(!m_fetchedRecords.isEmpty());
m_currentKeyForUniqueness = m_fetchedRecords.first().record.key;
if (m_indexID != IDBIndexInfo::InvalidId)
m_currentIndexRecordValue = m_fetchedRecords.first().record.primaryKey;
// If ObjectStore or Index contents changed, we need to reset the statement and bind new parameters to it.
// This is to pick up any changes that might exist.
m_statementNeedsReset = true;
if (isDirectionNext()) {
m_currentLowerKey = m_currentKeyForUniqueness;
if (!m_keyRange.lowerOpen) {
m_keyRange.lowerOpen = true;
m_keyRange.lowerKey = m_currentLowerKey;
m_statement = nullptr;
}
} else {
m_currentUpperKey = m_currentKeyForUniqueness;
if (!m_keyRange.upperOpen) {
m_keyRange.upperOpen = true;
m_keyRange.upperKey = m_currentUpperKey;
m_statement = nullptr;
}
}
// We also need to throw away any fetched records as they may no longer be valid.
m_fetchedRecords.clear();
m_fetchedRecordsSize = 0;
m_prefetchCount = 0;
}
void SQLiteIDBCursor::resetAndRebindStatement()
{
ASSERT(!m_currentLowerKey.isNull());
ASSERT(!m_currentUpperKey.isNull());
ASSERT(m_transaction->sqliteTransaction());
ASSERT(m_statementNeedsReset);
m_statementNeedsReset = false;
if (!m_statement && !establishStatement()) {
LOG_ERROR("Unable to establish new statement for cursor iteration");
return;
}
if (m_statement->reset() != SQLITE_OK) {
LOG_ERROR("Could not reset cursor statement to respond to object store changes");
return;
}
bindArguments();
}
bool SQLiteIDBCursor::bindArguments()
{
LOG(IndexedDB, "Cursor is binding lower key '%s' and upper key '%s'", m_currentLowerKey.loggingString().utf8().data(), m_currentUpperKey.loggingString().utf8().data());
int currentBindArgument = 1;
if (m_statement->bindInt64(currentBindArgument++, m_boundID) != SQLITE_OK) {
LOG_ERROR("Could not bind id argument (bound ID)");
return false;
}
RefPtr<SharedBuffer> buffer = serializeIDBKeyData(m_currentLowerKey);
if (m_statement->bindBlob(currentBindArgument++, buffer->data(), buffer->size()) != SQLITE_OK) {
LOG_ERROR("Could not create cursor statement (lower key)");
return false;
}
buffer = serializeIDBKeyData(m_currentUpperKey);
if (m_statement->bindBlob(currentBindArgument++, buffer->data(), buffer->size()) != SQLITE_OK) {
LOG_ERROR("Could not create cursor statement (upper key)");
return false;
}
return true;
}
bool SQLiteIDBCursor::resetAndRebindPreIndexStatementIfNecessary()
{
if (m_indexID == IDBIndexInfo::InvalidId)
return true;
if (m_currentIndexRecordValue.isNull())
return true;
auto& database = m_transaction->sqliteTransaction()->database();
if (!m_preIndexStatement) {
m_preIndexStatement = makeUnique<SQLiteStatement>(database, buildPreIndexStatement(isDirectionNext()));
if (m_preIndexStatement->prepare() != SQLITE_OK) {
LOG_ERROR("Could not prepare pre statement - '%s'", database.lastErrorMsg());
return false;
}
}
if (m_preIndexStatement->reset() != SQLITE_OK) {
LOG_ERROR("Could not reset pre statement - '%s'", database.lastErrorMsg());
return false;
}
auto key = isDirectionNext() ? m_currentLowerKey : m_currentUpperKey;
int currentBindArgument = 1;
if (m_preIndexStatement->bindInt64(currentBindArgument++, m_boundID) != SQLITE_OK) {
LOG_ERROR("Could not bind id argument to pre statement (bound ID)");
return false;
}
RefPtr<SharedBuffer> buffer = serializeIDBKeyData(key);
if (m_preIndexStatement->bindBlob(currentBindArgument++, buffer->data(), buffer->size()) != SQLITE_OK) {
LOG_ERROR("Could not bind id argument to pre statement (key)");
return false;
}
buffer = serializeIDBKeyData(m_currentIndexRecordValue);
if (m_preIndexStatement->bindBlob(currentBindArgument++, buffer->data(), buffer->size()) != SQLITE_OK) {
LOG_ERROR("Could not bind id argument to pre statement (value)");
return false;
}
return true;
}
bool SQLiteIDBCursor::prefetchOneRecord()
{
LOG(IndexedDB, "SQLiteIDBCursor::prefetchOneRecord() - Cursor already has %zu fetched records", m_fetchedRecords.size());
if (m_fetchedRecordsSize >= prefetchSizeLimit || m_fetchedRecords.isEmpty() || m_fetchedRecords.size() >= prefetchLimit || m_fetchedRecords.last().isTerminalRecord())
return false;
m_currentKeyForUniqueness = m_fetchedRecords.last().record.key;
return fetch() && m_fetchedRecords.size() < prefetchLimit && m_fetchedRecordsSize < prefetchSizeLimit;
}
void SQLiteIDBCursor::increaseCountToPrefetch()
{
m_prefetchCount = m_prefetchCount ? m_prefetchCount * 2 : 1;
}
bool SQLiteIDBCursor::prefetch()
{
for (unsigned i = 0; i < m_prefetchCount; ++i) {
if (!prefetchOneRecord())
return false;
}
increaseCountToPrefetch();
return true;
}
bool SQLiteIDBCursor::advance(uint64_t count)
{
LOG(IndexedDB, "SQLiteIDBCursor::advance() - Count %" PRIu64 ", %zu fetched records", count, m_fetchedRecords.size());
ASSERT(count);
if (!m_fetchedRecords.isEmpty() && m_fetchedRecords.first().isTerminalRecord()) {
LOG_ERROR("Attempt to advance a completed cursor");
return false;
}
if (!m_fetchedRecords.isEmpty())
m_currentKeyForUniqueness = m_fetchedRecords.last().record.key;
// Drop already-fetched records up to `count` to see if we've already fetched the record we're looking for.
bool hadCurrentRecord = !m_fetchedRecords.isEmpty();
for (; count && !m_fetchedRecords.isEmpty(); --count) {
if (m_fetchedRecords.first().isTerminalRecord())
break;
ASSERT(m_fetchedRecordsSize >= m_fetchedRecords.first().record.size());
m_fetchedRecordsSize -= m_fetchedRecords.first().record.size();
m_fetchedRecords.removeFirst();
}
// If we still have any records left, the first record is our new current record.
if (!m_fetchedRecords.isEmpty())
return true;
ASSERT(m_fetchedRecords.isEmpty());
// If we started out with a current record, we burnt a count on removing it.
// Replace that count now.
if (hadCurrentRecord)
++count;
for (; count; --count) {
if (!m_fetchedRecords.isEmpty()) {
ASSERT(m_fetchedRecords.size() == 1);
m_currentKeyForUniqueness = m_fetchedRecords.first().record.key;
ASSERT(m_fetchedRecordsSize >= m_fetchedRecords.first().record.size());
m_fetchedRecordsSize -= m_fetchedRecords.first().record.size();
m_fetchedRecords.removeFirst();
}
if (!fetch())
return false;
ASSERT(!m_fetchedRecords.isEmpty());
ASSERT(!m_fetchedRecords.first().errored);
if (m_fetchedRecords.first().completed)
break;
}
return true;
}
bool SQLiteIDBCursor::fetch()
{
ASSERT(m_fetchedRecords.isEmpty() || !m_fetchedRecords.last().isTerminalRecord());
m_fetchedRecords.append({ });
bool isUnique = m_cursorDirection == IndexedDB::CursorDirection::Nextunique || m_cursorDirection == IndexedDB::CursorDirection::Prevunique;
if (!isUnique) {
bool fetchSucceeded = fetchNextRecord(m_fetchedRecords.last());
if (fetchSucceeded)
m_fetchedRecordsSize += m_fetchedRecords.last().record.size();
return fetchSucceeded;
}
while (fetchNextRecord(m_fetchedRecords.last())) {
m_fetchedRecordsSize += m_fetchedRecords.last().record.size();
if (m_currentKeyForUniqueness.compare(m_fetchedRecords.last().record.key))
return true;
if (m_fetchedRecords.last().completed)
return false;
m_fetchedRecordsSize -= m_fetchedRecords.last().record.size();
}
return false;
}
bool SQLiteIDBCursor::fetchNextRecord(SQLiteCursorRecord& record)
{
if (m_statementNeedsReset) {
resetAndRebindPreIndexStatementIfNecessary();
resetAndRebindStatement();
}
FetchResult result;
do {
result = internalFetchNextRecord(record);
} while (result == FetchResult::ShouldFetchAgain);
return result == FetchResult::Success;
}
void SQLiteIDBCursor::markAsErrored(SQLiteCursorRecord& record)
{
record.record = { };
record.completed = true;
record.errored = true;
record.rowID = 0;
}
SQLiteIDBCursor::FetchResult SQLiteIDBCursor::internalFetchNextRecord(SQLiteCursorRecord& record)
{
ASSERT(m_transaction->sqliteTransaction());
ASSERT(m_statement);
ASSERT(!m_fetchedRecords.isEmpty());
ASSERT(!m_fetchedRecords.last().isTerminalRecord());
record.record.value = { };
auto& database = m_transaction->sqliteTransaction()->database();
SQLiteStatement* statement = nullptr;
int result;
if (m_preIndexStatement) {
ASSERT(m_indexID != IDBIndexInfo::InvalidId);
result = m_preIndexStatement->step();
if (result == SQLITE_ROW)
statement = m_preIndexStatement.get();
else if (result != SQLITE_DONE)
LOG_ERROR("Error advancing with pre statement - (%i) %s", result, database.lastErrorMsg());
}
if (!statement) {
result = m_statement->step();
if (result == SQLITE_DONE) {
record = { };
record.completed = true;
return FetchResult::Success;
}
if (result != SQLITE_ROW) {
LOG_ERROR("Error advancing cursor - (%i) %s", result, database.lastErrorMsg());
markAsErrored(record);
return FetchResult::Failure;
}
statement = m_statement.get();
}
record.rowID = statement->getColumnInt64(0);
ASSERT(record.rowID);
Vector<uint8_t> keyData;
statement->getColumnBlobAsVector(1, keyData);
if (!deserializeIDBKeyData(keyData.data(), keyData.size(), record.record.key)) {
LOG_ERROR("Unable to deserialize key data from database while advancing cursor");
markAsErrored(record);
return FetchResult::Failure;
}
statement->getColumnBlobAsVector(2, keyData);
// The primaryKey of an ObjectStore cursor is the same as its key.
if (m_indexID == IDBIndexInfo::InvalidId) {
record.record.primaryKey = record.record.key;
Vector<String> blobURLs, blobFilePaths;
auto error = m_transaction->backingStore().getBlobRecordsForObjectStoreRecord(record.rowID, blobURLs, blobFilePaths);
if (!error.isNull()) {
LOG_ERROR("Unable to fetch blob records from database while advancing cursor");
markAsErrored(record);
return FetchResult::Failure;
}
if (m_cursorType == IndexedDB::CursorType::KeyAndValue)
record.record.value = { ThreadSafeDataBuffer::create(WTFMove(keyData)), blobURLs, blobFilePaths };
} else {
if (!deserializeIDBKeyData(keyData.data(), keyData.size(), record.record.primaryKey)) {
LOG_ERROR("Unable to deserialize value data from database while advancing index cursor");
markAsErrored(record);
return FetchResult::Failure;
}
if (!m_cachedObjectStoreStatement || m_cachedObjectStoreStatement->reset() != SQLITE_OK) {
m_cachedObjectStoreStatement = makeUnique<SQLiteStatement>(database, "SELECT value FROM Records WHERE key = CAST(? AS TEXT) and objectStoreID = ?;");
if (m_cachedObjectStoreStatement->prepare() != SQLITE_OK)
m_cachedObjectStoreStatement = nullptr;
}
if (!m_cachedObjectStoreStatement
|| m_cachedObjectStoreStatement->bindBlob(1, keyData.data(), keyData.size()) != SQLITE_OK
|| m_cachedObjectStoreStatement->bindInt64(2, m_objectStoreID) != SQLITE_OK) {
LOG_ERROR("Could not create index cursor statement into object store records (%i) '%s'", database.lastError(), database.lastErrorMsg());
markAsErrored(record);
return FetchResult::Failure;
}
int result = m_cachedObjectStoreStatement->step();
if (result == SQLITE_ROW) {
m_cachedObjectStoreStatement->getColumnBlobAsVector(0, keyData);
record.record.value = { ThreadSafeDataBuffer::create(WTFMove(keyData)) };
} else if (result == SQLITE_DONE) {
// This indicates that the record we're trying to retrieve has been removed from the object store.
// Skip over it.
return FetchResult::ShouldFetchAgain;
} else {
LOG_ERROR("Could not step index cursor statement into object store records (%i) '%s'", database.lastError(), database.lastErrorMsg());
markAsErrored(record);
return FetchResult::Failure;
}
}
return FetchResult::Success;
}
bool SQLiteIDBCursor::iterate(const IDBKeyData& targetKey, const IDBKeyData& targetPrimaryKey)
{
ASSERT(m_transaction->sqliteTransaction());
ASSERT(m_statement);
bool result = advance(1);
ASSERT(!m_fetchedRecords.isEmpty());
// Iterating with no key is equivalent to advancing 1 step.
if (targetKey.isNull() || !result)
return result;
while (!m_fetchedRecords.first().isTerminalRecord()) {
if (!result)
return false;
// Search for the next key >= the target if the cursor is a Next cursor, or the next key <= if the cursor is a Previous cursor.
if (m_cursorDirection == IndexedDB::CursorDirection::Next || m_cursorDirection == IndexedDB::CursorDirection::Nextunique) {
if (m_fetchedRecords.first().record.key.compare(targetKey) >= 0)
break;
} else if (m_fetchedRecords.first().record.key.compare(targetKey) <= 0)
break;
result = advance(1);
}
if (targetPrimaryKey.isValid()) {
while (!m_fetchedRecords.first().isTerminalRecord() && !m_fetchedRecords.first().record.key.compare(targetKey)) {
if (!result)
return false;
// Search for the next primary key >= the primary target if the cursor is a Next cursor, or the next key <= if the cursor is a Previous cursor.
if (m_cursorDirection == IndexedDB::CursorDirection::Next || m_cursorDirection == IndexedDB::CursorDirection::Nextunique) {
if (m_fetchedRecords.first().record.primaryKey.compare(targetPrimaryKey) >= 0)
break;
} else if (m_fetchedRecords.first().record.primaryKey.compare(targetPrimaryKey) <= 0)
break;
result = advance(1);
}
}
return result;
}
const IDBKeyData& SQLiteIDBCursor::currentKey() const
{
ASSERT(!m_fetchedRecords.isEmpty());
return m_fetchedRecords.first().record.key;
}
const IDBKeyData& SQLiteIDBCursor::currentPrimaryKey() const
{
ASSERT(!m_fetchedRecords.isEmpty());
return m_fetchedRecords.first().record.primaryKey;
}
const IDBValue& SQLiteIDBCursor::currentValue() const
{
ASSERT(!m_fetchedRecords.isEmpty());
return m_fetchedRecords.first().record.value;
}
bool SQLiteIDBCursor::didComplete() const
{
ASSERT(!m_fetchedRecords.isEmpty());
return m_fetchedRecords.first().completed;
}
bool SQLiteIDBCursor::didError() const
{
ASSERT(!m_fetchedRecords.isEmpty());
return m_fetchedRecords.first().errored;
}
int64_t SQLiteIDBCursor::currentRecordRowID() const
{
ASSERT(!m_fetchedRecords.isEmpty());
return m_fetchedRecords.first().rowID;
}
} // namespace IDBServer
} // namespace WebCore
#endif // ENABLE(INDEXED_DATABASE)