| /* |
| * 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 = 8; |
| |
| 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) |
| { |
| ASSERT(!m_fetchedRecords.isEmpty()); |
| |
| auto& currentRecord = m_fetchedRecords.first(); |
| if (currentRecord.completed) { |
| ASSERT(!currentRecord.errored); |
| result = { }; |
| return; |
| } |
| |
| result = { currentRecord.record.key, currentRecord.record.primaryKey, currentRecord.record.value ? *currentRecord.record.value : IDBValue(), keyPath}; |
| } |
| |
| static String buildIndexStatement(const IDBKeyRangeData& keyRange, IndexedDB::CursorDirection cursorDirection) |
| { |
| StringBuilder builder; |
| |
| builder.appendLiteral("SELECT rowid, key, value FROM IndexRecords WHERE indexID = ? AND 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.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_cursorDirection != IndexedDB::CursorDirection::Nextunique && m_cursorDirection != IndexedDB::CursorDirection::Prevunique) { |
| if (!m_fetchedRecords.last().isTerminalRecord()) |
| fetch(ShouldFetchForSameKey::Yes); |
| |
| while (m_fetchedRecords.last().record.key != m_fetchedRecords.first().record.key) |
| m_fetchedRecords.removeLast(); |
| } else |
| m_fetchedRecords.clear(); |
| |
| // 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. |
| // We also need to throw away any fetched records as they may no longer be valid. |
| |
| m_statementNeedsReset = true; |
| |
| if (m_cursorDirection == IndexedDB::CursorDirection::Next || m_cursorDirection == IndexedDB::CursorDirection::Nextunique) { |
| 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; |
| } |
| } |
| } |
| |
| 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; |
| } |
| |
| if (m_indexID != IDBIndexInfo::InvalidId && m_statement->bindInt64(currentBindArgument++, m_objectStoreID) != SQLITE_OK) { |
| LOG_ERROR("Could not bind object store id argument for an index cursor"); |
| 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::prefetch() |
| { |
| LOG(IndexedDB, "SQLiteIDBCursor::prefetch() - Cursor already has %zu fetched records", m_fetchedRecords.size()); |
| |
| if (m_fetchedRecords.isEmpty() || m_fetchedRecords.size() >= prefetchLimit || m_fetchedRecords.last().isTerminalRecord()) |
| return false; |
| |
| m_currentKeyForUniqueness = m_fetchedRecords.last().record.key; |
| fetch(); |
| |
| return m_fetchedRecords.size() < prefetchLimit; |
| } |
| |
| 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; |
| |
| 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; |
| 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(ShouldFetchForSameKey shouldFetchForSameKey) |
| { |
| ASSERT(m_fetchedRecords.isEmpty() || !m_fetchedRecords.last().isTerminalRecord()); |
| |
| m_fetchedRecords.append({ }); |
| |
| bool isUnique = m_cursorDirection == IndexedDB::CursorDirection::Nextunique || m_cursorDirection == IndexedDB::CursorDirection::Prevunique || shouldFetchForSameKey == ShouldFetchForSameKey::Yes; |
| if (!isUnique) |
| return fetchNextRecord(m_fetchedRecords.last()); |
| |
| while (fetchNextRecord(m_fetchedRecords.last())) { |
| if (m_currentKeyForUniqueness.compare(m_fetchedRecords.last().record.key)) |
| return true; |
| |
| if (m_fetchedRecords.last().completed) |
| return false; |
| |
| if (shouldFetchForSameKey == ShouldFetchForSameKey::Yes) |
| m_fetchedRecords.append({ }); |
| } |
| |
| return false; |
| } |
| |
| bool SQLiteIDBCursor::fetchNextRecord(SQLiteCursorRecord& record) |
| { |
| if (m_statementNeedsReset) |
| 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 = nullptr; |
| |
| int result = m_statement->step(); |
| if (result == SQLITE_DONE) { |
| // When a cursor reaches its end, that is indicated by having undefined keys/values |
| record = { }; |
| record.completed = true; |
| |
| return FetchResult::Success; |
| } |
| |
| if (result != SQLITE_ROW) { |
| LOG_ERROR("Error advancing cursor - (%i) %s", result, m_transaction->sqliteTransaction()->database().lastErrorMsg()); |
| markAsErrored(record); |
| return FetchResult::Failure; |
| } |
| |
| record.rowID = m_statement->getColumnInt64(0); |
| ASSERT(record.rowID); |
| |
| Vector<uint8_t> keyData; |
| m_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; |
| } |
| |
| m_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 = makeUnique<IDBValue>(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>(m_statement->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'", m_statement->database().lastError(), m_statement->database().lastErrorMsg()); |
| markAsErrored(record); |
| return FetchResult::Failure; |
| } |
| |
| int result = m_cachedObjectStoreStatement->step(); |
| |
| if (result == SQLITE_ROW) { |
| m_cachedObjectStoreStatement->getColumnBlobAsVector(0, keyData); |
| record.record.value = makeUnique<IDBValue>(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'", m_statement->database().lastError(), m_statement->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; |
| } |
| |
| IDBValue* SQLiteIDBCursor::currentValue() const |
| { |
| ASSERT(!m_fetchedRecords.isEmpty()); |
| return m_fetchedRecords.first().record.value.get(); |
| } |
| |
| 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) |