diff options
Diffstat (limited to 'Source/WebCore/storage/IDBSQLiteBackingStore.cpp')
-rw-r--r-- | Source/WebCore/storage/IDBSQLiteBackingStore.cpp | 996 |
1 files changed, 996 insertions, 0 deletions
diff --git a/Source/WebCore/storage/IDBSQLiteBackingStore.cpp b/Source/WebCore/storage/IDBSQLiteBackingStore.cpp new file mode 100644 index 0000000..e43b7a3 --- /dev/null +++ b/Source/WebCore/storage/IDBSQLiteBackingStore.cpp @@ -0,0 +1,996 @@ +/* + * Copyright (C) 2011 Google 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 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 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 "IDBSQLiteBackingStore.h" + +#if ENABLE(INDEXED_DATABASE) + +#include "FileSystem.h" +#include "IDBFactoryBackendImpl.h" +#include "IDBKey.h" +#include "IDBKeyRange.h" +#include "SQLiteDatabase.h" +#include "SQLiteStatement.h" +#include "SQLiteTransaction.h" +#include "SecurityOrigin.h" + +namespace WebCore { + +IDBSQLiteBackingStore::IDBSQLiteBackingStore(String identifier, IDBFactoryBackendImpl* factory) + : m_identifier(identifier) + , m_factory(factory) +{ + m_factory->addIDBBackingStore(identifier, this); +} + +IDBSQLiteBackingStore::~IDBSQLiteBackingStore() +{ + m_factory->removeIDBBackingStore(m_identifier); +} + +static bool runCommands(SQLiteDatabase& sqliteDatabase, const char** commands, size_t numberOfCommands) +{ + SQLiteTransaction transaction(sqliteDatabase, false); + transaction.begin(); + for (size_t i = 0; i < numberOfCommands; ++i) { + if (!sqliteDatabase.executeCommand(commands[i])) { + LOG_ERROR("Failed to run the following command for IndexedDB: %s", commands[i]); + return false; + } + } + transaction.commit(); + return true; +} + +static bool createTables(SQLiteDatabase& sqliteDatabase) +{ + if (sqliteDatabase.tableExists("Databases")) + return true; + static const char* commands[] = { + "CREATE TABLE Databases (id INTEGER PRIMARY KEY, name TEXT NOT NULL, description TEXT NOT NULL, version TEXT NOT NULL)", + "CREATE UNIQUE INDEX Databases_name ON Databases(name)", + + "CREATE TABLE ObjectStores (id INTEGER PRIMARY KEY, name TEXT NOT NULL, keyPath TEXT, doAutoIncrement INTEGER NOT NULL, databaseId INTEGER NOT NULL REFERENCES Databases(id))", + "CREATE UNIQUE INDEX ObjectStores_composit ON ObjectStores(databaseId, name)", + + "CREATE TABLE Indexes (id INTEGER PRIMARY KEY, objectStoreId INTEGER NOT NULL REFERENCES ObjectStore(id), name TEXT NOT NULL, keyPath TEXT, isUnique INTEGER NOT NULL)", + "CREATE UNIQUE INDEX Indexes_composit ON Indexes(objectStoreId, name)", + + "CREATE TABLE ObjectStoreData (id INTEGER PRIMARY KEY, objectStoreId INTEGER NOT NULL REFERENCES ObjectStore(id), keyString TEXT, keyDate INTEGER, keyNumber INTEGER, value TEXT NOT NULL)", + "CREATE UNIQUE INDEX ObjectStoreData_composit ON ObjectStoreData(keyString, keyDate, keyNumber, objectStoreId)", + + "CREATE TABLE IndexData (id INTEGER PRIMARY KEY, indexId INTEGER NOT NULL REFERENCES Indexes(id), keyString TEXT, keyDate INTEGER, keyNumber INTEGER, objectStoreDataId INTEGER NOT NULL REFERENCES ObjectStoreData(id))", + "CREATE INDEX IndexData_composit ON IndexData(keyString, keyDate, keyNumber, indexId)", + "CREATE INDEX IndexData_objectStoreDataId ON IndexData(objectStoreDataId)", + "CREATE INDEX IndexData_indexId ON IndexData(indexId)", + }; + + return runCommands(sqliteDatabase, commands, sizeof(commands) / sizeof(commands[0])); +} + +static bool createMetaDataTable(SQLiteDatabase& sqliteDatabase) +{ + static const char* commands[] = { + "CREATE TABLE MetaData (name TEXT PRIMARY KEY, value NONE)", + "INSERT INTO MetaData VALUES ('version', 1)", + }; + + return runCommands(sqliteDatabase, commands, sizeof(commands) / sizeof(commands[0])); +} + +static bool getDatabaseSchemaVersion(SQLiteDatabase& sqliteDatabase, int* databaseVersion) +{ + SQLiteStatement query(sqliteDatabase, "SELECT value FROM MetaData WHERE name = 'version'"); + if (query.prepare() != SQLResultOk || query.step() != SQLResultRow) + return false; + + *databaseVersion = query.getColumnInt(0); + return query.finalize() == SQLResultOk; +} + +static bool migrateDatabase(SQLiteDatabase& sqliteDatabase) +{ + if (!sqliteDatabase.tableExists("MetaData")) { + if (!createMetaDataTable(sqliteDatabase)) + return false; + } + + int databaseVersion; + if (!getDatabaseSchemaVersion(sqliteDatabase, &databaseVersion)) + return false; + + if (databaseVersion == 1) { + static const char* commands[] = { + "DROP TABLE IF EXISTS ObjectStoreData2", + "CREATE TABLE ObjectStoreData2 (id INTEGER PRIMARY KEY, objectStoreId INTEGER NOT NULL REFERENCES ObjectStore(id), keyString TEXT, keyDate REAL, keyNumber REAL, value TEXT NOT NULL)", + "INSERT INTO ObjectStoreData2 SELECT * FROM ObjectStoreData", + "DROP TABLE ObjectStoreData", // This depends on SQLite not enforcing referential consistency. + "ALTER TABLE ObjectStoreData2 RENAME TO ObjectStoreData", + "CREATE UNIQUE INDEX ObjectStoreData_composit ON ObjectStoreData(keyString, keyDate, keyNumber, objectStoreId)", + "DROP TABLE IF EXISTS IndexData2", // This depends on SQLite not enforcing referential consistency. + "CREATE TABLE IndexData2 (id INTEGER PRIMARY KEY, indexId INTEGER NOT NULL REFERENCES Indexes(id), keyString TEXT, keyDate REAL, keyNumber REAL, objectStoreDataId INTEGER NOT NULL REFERENCES ObjectStoreData(id))", + "INSERT INTO IndexData2 SELECT * FROM IndexData", + "DROP TABLE IndexData", + "ALTER TABLE IndexData2 RENAME TO IndexData", + "CREATE INDEX IndexData_composit ON IndexData(keyString, keyDate, keyNumber, indexId)", + "CREATE INDEX IndexData_objectStoreDataId ON IndexData(objectStoreDataId)", + "CREATE INDEX IndexData_indexId ON IndexData(indexId)", + "UPDATE MetaData SET value = 2 WHERE name = 'version'", + }; + + if (!runCommands(sqliteDatabase, commands, sizeof(commands) / sizeof(commands[0]))) + return false; + + databaseVersion = 2; + } + + if (databaseVersion == 2) { + // We need to make the ObjectStoreData.value be a BLOB instead of TEXT. + static const char* commands[] = { + "DROP TABLE IF EXISTS ObjectStoreData", // This drops associated indices. + "CREATE TABLE ObjectStoreData (id INTEGER PRIMARY KEY, objectStoreId INTEGER NOT NULL REFERENCES ObjectStore(id), keyString TEXT, keyDate REAL, keyNumber REAL, value BLOB NOT NULL)", + "CREATE UNIQUE INDEX ObjectStoreData_composit ON ObjectStoreData(keyString, keyDate, keyNumber, objectStoreId)", + "UPDATE MetaData SET value = 3 WHERE name = 'version'", + }; + + if (!runCommands(sqliteDatabase, commands, sizeof(commands) / sizeof(commands[0]))) + return false; + + databaseVersion = 3; + } + + return true; +} + +PassRefPtr<IDBBackingStore> IDBSQLiteBackingStore::open(SecurityOrigin* securityOrigin, const String& pathBase, int64_t maximumSize, const String& fileIdentifier, IDBFactoryBackendImpl* factory) +{ + RefPtr<IDBSQLiteBackingStore> backingStore(adoptRef(new IDBSQLiteBackingStore(fileIdentifier, factory))); + + String path = ":memory:"; + if (!pathBase.isEmpty()) { + if (!makeAllDirectories(pathBase)) { + // FIXME: Is there any other thing we could possibly do to recover at this point? If so, do it rather than just erroring out. + LOG_ERROR("Unabled to create LocalStorage database path %s", pathBase.utf8().data()); + return 0; + } + path = pathByAppendingComponent(pathBase, securityOrigin->databaseIdentifier() + ".indexeddb"); + } + + if (!backingStore->m_db.open(path)) { + // FIXME: Is there any other thing we could possibly do to recover at this point? If so, do it rather than just erroring out. + LOG_ERROR("Failed to open database file %s for IndexedDB", path.utf8().data()); + return 0; + } + + // FIXME: Error checking? + backingStore->m_db.setMaximumSize(maximumSize); + backingStore->m_db.turnOnIncrementalAutoVacuum(); + + if (!createTables(backingStore->m_db)) + return 0; + if (!migrateDatabase(backingStore->m_db)) + return 0; + + return backingStore.release(); +} + +bool IDBSQLiteBackingStore::extractIDBDatabaseMetaData(const String& name, String& foundVersion, int64_t& foundId) +{ + SQLiteStatement databaseQuery(m_db, "SELECT id, version FROM Databases WHERE name = ?"); + if (databaseQuery.prepare() != SQLResultOk) { + ASSERT_NOT_REACHED(); + return false; + } + databaseQuery.bindText(1, name); + if (databaseQuery.step() != SQLResultRow) + return false; + + foundId = databaseQuery.getColumnInt64(0); + foundVersion = databaseQuery.getColumnText(1); + + if (databaseQuery.step() == SQLResultRow) + ASSERT_NOT_REACHED(); + return true; +} + +bool IDBSQLiteBackingStore::setIDBDatabaseMetaData(const String& name, const String& version, int64_t& rowId, bool invalidRowId) +{ + ASSERT(!name.isNull()); + ASSERT(!version.isNull()); + + String sql = invalidRowId ? "INSERT INTO Databases (name, description, version) VALUES (?, '', ?)" : "UPDATE Databases SET name = ?, version = ? WHERE id = ?"; + SQLiteStatement query(m_db, sql); + if (query.prepare() != SQLResultOk) { + ASSERT_NOT_REACHED(); + return false; + } + + query.bindText(1, name); + query.bindText(2, version); + if (!invalidRowId) + query.bindInt64(3, rowId); + + if (query.step() != SQLResultDone) + return false; + + if (invalidRowId) + rowId = m_db.lastInsertRowID(); + + return true; +} + +void IDBSQLiteBackingStore::getObjectStores(int64_t databaseId, Vector<int64_t>& foundIds, Vector<String>& foundNames, Vector<String>& foundKeyPaths, Vector<bool>& foundAutoIncrementFlags) +{ + SQLiteStatement query(m_db, "SELECT id, name, keyPath, doAutoIncrement FROM ObjectStores WHERE databaseId = ?"); + bool ok = query.prepare() == SQLResultOk; + ASSERT_UNUSED(ok, ok); // FIXME: Better error handling? + + ASSERT(foundIds.isEmpty()); + ASSERT(foundNames.isEmpty()); + ASSERT(foundKeyPaths.isEmpty()); + ASSERT(foundAutoIncrementFlags.isEmpty()); + + query.bindInt64(1, databaseId); + + while (query.step() == SQLResultRow) { + foundIds.append(query.getColumnInt64(0)); + foundNames.append(query.getColumnText(1)); + foundKeyPaths.append(query.getColumnText(2)); + foundAutoIncrementFlags.append(!!query.getColumnInt(3)); + } +} + +bool IDBSQLiteBackingStore::createObjectStore(const String& name, const String& keyPath, bool autoIncrement, int64_t databaseId, int64_t& assignedObjectStoreId) +{ + SQLiteStatement query(m_db, "INSERT INTO ObjectStores (name, keyPath, doAutoIncrement, databaseId) VALUES (?, ?, ?, ?)"); + if (query.prepare() != SQLResultOk) + return false; + + query.bindText(1, name); + query.bindText(2, keyPath); + query.bindInt(3, static_cast<int>(autoIncrement)); + query.bindInt64(4, databaseId); + + if (query.step() != SQLResultDone) + return false; + + assignedObjectStoreId = m_db.lastInsertRowID(); + return true; +} + +static void doDelete(SQLiteDatabase& db, const char* sql, int64_t id) +{ + SQLiteStatement deleteQuery(db, sql); + bool ok = deleteQuery.prepare() == SQLResultOk; + ASSERT_UNUSED(ok, ok); // FIXME: Better error handling. + deleteQuery.bindInt64(1, id); + ok = deleteQuery.step() == SQLResultDone; + ASSERT_UNUSED(ok, ok); // FIXME: Better error handling. +} + +void IDBSQLiteBackingStore::deleteObjectStore(int64_t objectStoreId) +{ + doDelete(m_db, "DELETE FROM ObjectStores WHERE id = ?", objectStoreId); + doDelete(m_db, "DELETE FROM ObjectStoreData WHERE objectStoreId = ?", objectStoreId); + doDelete(m_db, "DELETE FROM IndexData WHERE indexId IN (SELECT id FROM Indexes WHERE objectStoreId = ?)", objectStoreId); + doDelete(m_db, "DELETE FROM Indexes WHERE objectStoreId = ?", objectStoreId); +} + +static String whereSyntaxForKey(const IDBKey& key, String qualifiedTableName = "") +{ + switch (key.type()) { + case IDBKey::StringType: + return qualifiedTableName + "keyString = ? AND " + qualifiedTableName + "keyDate IS NULL AND " + qualifiedTableName + "keyNumber IS NULL "; + case IDBKey::NumberType: + return qualifiedTableName + "keyString IS NULL AND " + qualifiedTableName + "keyDate IS NULL AND " + qualifiedTableName + "keyNumber = ? "; + case IDBKey::DateType: + return qualifiedTableName + "keyString IS NULL AND " + qualifiedTableName + "keyDate = ? AND " + qualifiedTableName + "keyNumber IS NULL "; + case IDBKey::NullType: + return qualifiedTableName + "keyString IS NULL AND " + qualifiedTableName + "keyDate IS NULL AND " + qualifiedTableName + "keyNumber IS NULL "; + } + + ASSERT_NOT_REACHED(); + return ""; +} + +// Returns the number of items bound. +static int bindKeyToQuery(SQLiteStatement& query, int column, const IDBKey& key) +{ + switch (key.type()) { + case IDBKey::StringType: + query.bindText(column, key.string()); + return 1; + case IDBKey::DateType: + query.bindDouble(column, key.date()); + return 1; + case IDBKey::NumberType: + query.bindDouble(column, key.number()); + return 1; + case IDBKey::NullType: + return 0; + } + + ASSERT_NOT_REACHED(); + return 0; +} + +static String lowerCursorWhereFragment(const IDBKey& key, String comparisonOperator, String qualifiedTableName = "") +{ + switch (key.type()) { + case IDBKey::StringType: + return "? " + comparisonOperator + " " + qualifiedTableName + "keyString AND "; + case IDBKey::DateType: + return "(? " + comparisonOperator + " " + qualifiedTableName + "keyDate OR NOT " + qualifiedTableName + "keyString IS NULL) AND "; + case IDBKey::NumberType: + return "(? " + comparisonOperator + " " + qualifiedTableName + "keyNumber OR NOT " + qualifiedTableName + "keyString IS NULL OR NOT " + qualifiedTableName + "keyDate IS NULL) AND "; + case IDBKey::NullType: + if (comparisonOperator == "<") + return "NOT(" + qualifiedTableName + "keyString IS NULL AND " + qualifiedTableName + "keyDate IS NULL AND " + qualifiedTableName + "keyNumber IS NULL) AND "; + return ""; // If it's =, the upper bound half will do the constraining. If it's <=, then that's a no-op. + } + ASSERT_NOT_REACHED(); + return ""; +} + +static String upperCursorWhereFragment(const IDBKey& key, String comparisonOperator, String qualifiedTableName = "") +{ + switch (key.type()) { + case IDBKey::StringType: + return "(" + qualifiedTableName + "keyString " + comparisonOperator + " ? OR " + qualifiedTableName + "keyString IS NULL) AND "; + case IDBKey::DateType: + return "(" + qualifiedTableName + "keyDate " + comparisonOperator + " ? OR " + qualifiedTableName + "keyDate IS NULL) AND " + qualifiedTableName + "keyString IS NULL AND "; + case IDBKey::NumberType: + return "(" + qualifiedTableName + "keyNumber " + comparisonOperator + " ? OR " + qualifiedTableName + "keyNumber IS NULL) AND " + qualifiedTableName + "keyString IS NULL AND " + qualifiedTableName + "keyDate IS NULL AND "; + case IDBKey::NullType: + if (comparisonOperator == "<") + return "0 != 0 AND "; + return qualifiedTableName + "keyString IS NULL AND " + qualifiedTableName + "keyDate IS NULL AND " + qualifiedTableName + "keyNumber IS NULL AND "; + } + ASSERT_NOT_REACHED(); + return ""; +} + +String IDBSQLiteBackingStore::getObjectStoreRecord(int64_t objectStoreId, const IDBKey& key) +{ + SQLiteStatement query(m_db, "SELECT keyString, keyDate, keyNumber, value FROM ObjectStoreData WHERE objectStoreId = ? AND " + whereSyntaxForKey(key)); + bool ok = query.prepare() == SQLResultOk; + ASSERT_UNUSED(ok, ok); // FIXME: Better error handling? + + query.bindInt64(1, objectStoreId); + bindKeyToQuery(query, 2, key); + if (query.step() != SQLResultRow) + return String(); // Null String means record not found. + + ASSERT((key.type() == IDBKey::StringType) != query.isColumnNull(0)); + ASSERT((key.type() == IDBKey::DateType) != query.isColumnNull(1)); + ASSERT((key.type() == IDBKey::NumberType) != query.isColumnNull(2)); + + String record = query.getColumnBlobAsString(3); + ASSERT(query.step() != SQLResultRow); + + return record; +} + +static void bindKeyToQueryWithNulls(SQLiteStatement& query, int baseColumn, const IDBKey& key) +{ + switch (key.type()) { + case IDBKey::StringType: + query.bindText(baseColumn + 0, key.string()); + query.bindNull(baseColumn + 1); + query.bindNull(baseColumn + 2); + break; + case IDBKey::DateType: + query.bindNull(baseColumn + 0); + query.bindDouble(baseColumn + 1, key.date()); + query.bindNull(baseColumn + 2); + break; + case IDBKey::NumberType: + query.bindNull(baseColumn + 0); + query.bindNull(baseColumn + 1); + query.bindDouble(baseColumn + 2, key.number()); + break; + case IDBKey::NullType: + query.bindNull(baseColumn + 0); + query.bindNull(baseColumn + 1); + query.bindNull(baseColumn + 2); + break; + default: + ASSERT_NOT_REACHED(); + } +} + +bool IDBSQLiteBackingStore::putObjectStoreRecord(int64_t objectStoreId, const IDBKey& key, const String& value, int64_t& rowId, bool invalidRowId) +{ + String sql = !invalidRowId ? "UPDATE ObjectStoreData SET keyString = ?, keyDate = ?, keyNumber = ?, value = ? WHERE id = ?" + : "INSERT INTO ObjectStoreData (keyString, keyDate, keyNumber, value, objectStoreId) VALUES (?, ?, ?, ?, ?)"; + SQLiteStatement query(m_db, sql); + if (query.prepare() != SQLResultOk) + return false; + + bindKeyToQueryWithNulls(query, 1, key); + query.bindBlob(4, value); + if (!invalidRowId) + query.bindInt64(5, rowId); + else + query.bindInt64(5, objectStoreId); + + if (query.step() != SQLResultDone) + return false; + + if (invalidRowId) + rowId = m_db.lastInsertRowID(); + + return true; +} + +void IDBSQLiteBackingStore::clearObjectStore(int64_t objectStoreId) +{ + doDelete(m_db, "DELETE FROM IndexData WHERE objectStoreDataId IN (SELECT id FROM ObjectStoreData WHERE objectStoreId = ?)", objectStoreId); + doDelete(m_db, "DELETE FROM ObjectStoreData WHERE objectStoreId = ?", objectStoreId); +} + +void IDBSQLiteBackingStore::deleteObjectStoreRecord(int64_t, int64_t objectStoreDataId) +{ + SQLiteStatement osQuery(m_db, "DELETE FROM ObjectStoreData WHERE id = ?"); + bool ok = osQuery.prepare() == SQLResultOk; + ASSERT_UNUSED(ok, ok); // FIXME: Better error handling? + + osQuery.bindInt64(1, objectStoreDataId); + + ok = osQuery.step() == SQLResultDone; + ASSERT_UNUSED(ok, ok); + + SQLiteStatement indexQuery(m_db, "DELETE FROM IndexData WHERE objectStoreDataId = ?"); + ok = indexQuery.prepare() == SQLResultOk; + ASSERT_UNUSED(ok, ok); // FIXME: Better error handling? + + indexQuery.bindInt64(1, objectStoreDataId); + + ok = indexQuery.step() == SQLResultDone; + ASSERT_UNUSED(ok, ok); +} + +double IDBSQLiteBackingStore::nextAutoIncrementNumber(int64_t objectStoreId) +{ + SQLiteStatement query(m_db, "SELECT max(keyNumber) + 1 FROM ObjectStoreData WHERE objectStoreId = ? AND keyString IS NULL AND keyDate IS NULL"); + bool ok = query.prepare() == SQLResultOk; + ASSERT_UNUSED(ok, ok); + + query.bindInt64(1, objectStoreId); + + if (query.step() != SQLResultRow || query.isColumnNull(0)) + return 1; + + return query.getColumnDouble(0); +} + +bool IDBSQLiteBackingStore::keyExistsInObjectStore(int64_t objectStoreId, const IDBKey& key, int64_t& foundObjectStoreDataId) +{ + String sql = String("SELECT id FROM ObjectStoreData WHERE objectStoreId = ? AND ") + whereSyntaxForKey(key); + SQLiteStatement query(m_db, sql); + bool ok = query.prepare() == SQLResultOk; + ASSERT_UNUSED(ok, ok); // FIXME: Better error handling? + + query.bindInt64(1, objectStoreId); + bindKeyToQuery(query, 2, key); + + if (query.step() != SQLResultRow) + return false; + + foundObjectStoreDataId = query.getColumnInt64(0); + return true; +} + +bool IDBSQLiteBackingStore::forEachObjectStoreRecord(int64_t objectStoreId, ObjectStoreRecordCallback& callback) +{ + SQLiteStatement query(m_db, "SELECT id, value FROM ObjectStoreData WHERE objectStoreId = ?"); + if (query.prepare() != SQLResultOk) + return false; + + query.bindInt64(1, objectStoreId); + + while (query.step() == SQLResultRow) { + int64_t objectStoreDataId = query.getColumnInt64(0); + String value = query.getColumnBlobAsString(1); + if (!callback.callback(objectStoreDataId, value)) + return false; + } + + return true; +} + +void IDBSQLiteBackingStore::getIndexes(int64_t objectStoreId, Vector<int64_t>& foundIds, Vector<String>& foundNames, Vector<String>& foundKeyPaths, Vector<bool>& foundUniqueFlags) +{ + SQLiteStatement query(m_db, "SELECT id, name, keyPath, isUnique FROM Indexes WHERE objectStoreId = ?"); + bool ok = query.prepare() == SQLResultOk; + ASSERT_UNUSED(ok, ok); // FIXME: Better error handling? + + ASSERT(foundIds.isEmpty()); + ASSERT(foundNames.isEmpty()); + ASSERT(foundKeyPaths.isEmpty()); + ASSERT(foundUniqueFlags.isEmpty()); + + query.bindInt64(1, objectStoreId); + + while (query.step() == SQLResultRow) { + foundIds.append(query.getColumnInt64(0)); + foundNames.append(query.getColumnText(1)); + foundKeyPaths.append(query.getColumnText(2)); + foundUniqueFlags.append(!!query.getColumnInt(3)); + } +} + +bool IDBSQLiteBackingStore::createIndex(int64_t objectStoreId, const String& name, const String& keyPath, bool isUnique, int64_t& indexId) +{ + SQLiteStatement query(m_db, "INSERT INTO Indexes (objectStoreId, name, keyPath, isUnique) VALUES (?, ?, ?, ?)"); + if (query.prepare() != SQLResultOk) + return false; + + query.bindInt64(1, objectStoreId); + query.bindText(2, name); + query.bindText(3, keyPath); + query.bindInt(4, static_cast<int>(isUnique)); + + if (query.step() != SQLResultDone) + return false; + + indexId = m_db.lastInsertRowID(); + return true; +} + +void IDBSQLiteBackingStore::deleteIndex(int64_t indexId) +{ + doDelete(m_db, "DELETE FROM Indexes WHERE id = ?", indexId); + doDelete(m_db, "DELETE FROM IndexData WHERE indexId = ?", indexId); +} + +bool IDBSQLiteBackingStore::putIndexDataForRecord(int64_t indexId, const IDBKey& key, int64_t objectStoreDataId) +{ + SQLiteStatement query(m_db, "INSERT INTO IndexData (keyString, keyDate, keyNumber, indexId, objectStoreDataId) VALUES (?, ?, ?, ?, ?)"); + if (query.prepare() != SQLResultOk) + return false; + + bindKeyToQueryWithNulls(query, 1, key); + query.bindInt64(4, indexId); + query.bindInt64(5, objectStoreDataId); + + return query.step() == SQLResultDone; +} + +bool IDBSQLiteBackingStore::deleteIndexDataForRecord(int64_t objectStoreDataId) +{ + SQLiteStatement query(m_db, "DELETE FROM IndexData WHERE objectStoreDataId = ?"); + if (query.prepare() != SQLResultOk) + return false; + + query.bindInt64(1, objectStoreDataId); + return query.step() == SQLResultDone; +} + +String IDBSQLiteBackingStore::getObjectViaIndex(int64_t indexId, const IDBKey& key) +{ + String sql = String("SELECT ") + + "ObjectStoreData.value " + + "FROM IndexData INNER JOIN ObjectStoreData ON IndexData.objectStoreDataId = ObjectStoreData.id " + + "WHERE IndexData.indexId = ? AND " + whereSyntaxForKey(key, "IndexData.") + + "ORDER BY IndexData.id LIMIT 1"; // Order by insertion order when all else fails. + SQLiteStatement query(m_db, sql); + bool ok = query.prepare() == SQLResultOk; + ASSERT_UNUSED(ok, ok); // FIXME: Better error handling? + + query.bindInt64(1, indexId); + bindKeyToQuery(query, 2, key); + + if (query.step() != SQLResultRow) + return String(); + + String foundValue = query.getColumnBlobAsString(0); + ASSERT(query.step() != SQLResultRow); + return foundValue; +} + +static PassRefPtr<IDBKey> keyFromQuery(SQLiteStatement& query, int baseColumn) +{ + if (query.columnCount() <= baseColumn) + return 0; + + if (!query.isColumnNull(baseColumn)) + return IDBKey::createString(query.getColumnText(baseColumn)); + + if (!query.isColumnNull(baseColumn + 1)) + return IDBKey::createDate(query.getColumnDouble(baseColumn + 1)); + + if (!query.isColumnNull(baseColumn + 2)) + return IDBKey::createNumber(query.getColumnDouble(baseColumn + 2)); + + return IDBKey::createNull(); +} + +PassRefPtr<IDBKey> IDBSQLiteBackingStore::getPrimaryKeyViaIndex(int64_t indexId, const IDBKey& key) +{ + String sql = String("SELECT ") + + "ObjectStoreData.keyString, ObjectStoreData.keyDate, ObjectStoreData.keyNumber " + + "FROM IndexData INNER JOIN ObjectStoreData ON IndexData.objectStoreDataId = ObjectStoreData.id " + + "WHERE IndexData.indexId = ? AND " + whereSyntaxForKey(key, "IndexData.") + + "ORDER BY IndexData.id LIMIT 1"; // Order by insertion order when all else fails. + SQLiteStatement query(m_db, sql); + bool ok = query.prepare() == SQLResultOk; + ASSERT_UNUSED(ok, ok); // FIXME: Better error handling? + + query.bindInt64(1, indexId); + bindKeyToQuery(query, 2, key); + + if (query.step() != SQLResultRow) + return 0; + + RefPtr<IDBKey> foundKey = keyFromQuery(query, 0); + ASSERT(query.step() != SQLResultRow); + return foundKey.release(); +} + +bool IDBSQLiteBackingStore::keyExistsInIndex(int64_t indexId, const IDBKey& key) +{ + String sql = String("SELECT id FROM IndexData WHERE indexId = ? AND ") + whereSyntaxForKey(key); + SQLiteStatement query(m_db, sql); + bool ok = query.prepare() == SQLResultOk; + ASSERT_UNUSED(ok, ok); // FIXME: Better error handling? + + query.bindInt64(1, indexId); + bindKeyToQuery(query, 2, key); + + return query.step() == SQLResultRow; +} + +namespace { + +class CursorImplCommon : public IDBSQLiteBackingStore::Cursor { +public: + CursorImplCommon(SQLiteDatabase& sqliteDatabase, String query, bool uniquenessConstraint, bool iterateForward) + : m_query(sqliteDatabase, query) + , m_db(sqliteDatabase) + , m_uniquenessConstraint(uniquenessConstraint) + , m_iterateForward(iterateForward) + { + } + virtual ~CursorImplCommon() {} + + // IDBBackingStore::Cursor + virtual bool continueFunction(const IDBKey*); + virtual PassRefPtr<IDBKey> key() { return m_currentKey; } + virtual PassRefPtr<IDBKey> primaryKey() { return m_currentKey; } + virtual String value() = 0; + virtual int64_t objectStoreDataId() = 0; + virtual int64_t indexDataId() = 0; + + virtual void loadCurrentRow() = 0; + virtual bool currentRowExists() = 0; + + SQLiteStatement m_query; + +protected: + SQLiteDatabase& m_db; + bool m_uniquenessConstraint; + bool m_iterateForward; + int64_t m_currentId; + RefPtr<IDBKey> m_currentKey; +}; + +bool CursorImplCommon::continueFunction(const IDBKey* key) +{ + while (true) { + if (m_query.step() != SQLResultRow) + return false; + + RefPtr<IDBKey> oldKey = m_currentKey; + loadCurrentRow(); + + // Skip if this entry has been deleted from the object store. + if (!currentRowExists()) + continue; + + // If a key was supplied, we must loop until we find a key greater than or equal to it (or hit the end). + if (key) { + if (m_iterateForward) { + if (m_currentKey->isLessThan(key)) + continue; + } else { + if (key->isLessThan(m_currentKey.get())) + continue; + } + } + + // If we don't have a uniqueness constraint, we can stop now. + if (!m_uniquenessConstraint) + break; + if (!m_currentKey->isEqual(oldKey.get())) + break; + } + + return true; +} + +class ObjectStoreCursorImpl : public CursorImplCommon { +public: + ObjectStoreCursorImpl(SQLiteDatabase& sqliteDatabase, String query, bool uniquenessConstraint, bool iterateForward) + : CursorImplCommon(sqliteDatabase, query, uniquenessConstraint, iterateForward) + { + } + + // CursorImplCommon. + virtual String value() { return m_currentValue; } + virtual int64_t objectStoreDataId() { return m_currentId; } + virtual int64_t indexDataId() { ASSERT_NOT_REACHED(); return 0; } + virtual void loadCurrentRow(); + virtual bool currentRowExists(); + +private: + String m_currentValue; +}; + +void ObjectStoreCursorImpl::loadCurrentRow() +{ + m_currentId = m_query.getColumnInt64(0); + m_currentKey = keyFromQuery(m_query, 1); + m_currentValue = m_query.getColumnBlobAsString(4); +} + +bool ObjectStoreCursorImpl::currentRowExists() +{ + String sql = "SELECT id FROM ObjectStoreData WHERE id = ?"; + SQLiteStatement statement(m_db, sql); + + bool ok = statement.prepare() == SQLResultOk; + ASSERT_UNUSED(ok, ok); + + statement.bindInt64(1, m_currentId); + return statement.step() == SQLResultRow; +} + +class IndexKeyCursorImpl : public CursorImplCommon { +public: + IndexKeyCursorImpl(SQLiteDatabase& sqliteDatabase, String query, bool uniquenessConstraint, bool iterateForward) + : CursorImplCommon(sqliteDatabase, query, uniquenessConstraint, iterateForward) + { + } + + // CursorImplCommon + virtual PassRefPtr<IDBKey> primaryKey() { return m_currentPrimaryKey; } + virtual String value() { ASSERT_NOT_REACHED(); return String(); } + virtual int64_t objectStoreDataId() { ASSERT_NOT_REACHED(); return 0; } + virtual int64_t indexDataId() { return m_currentId; } + virtual void loadCurrentRow(); + virtual bool currentRowExists(); + +private: + RefPtr<IDBKey> m_currentPrimaryKey; +}; + +void IndexKeyCursorImpl::loadCurrentRow() +{ + m_currentId = m_query.getColumnInt64(0); + m_currentKey = keyFromQuery(m_query, 1); + m_currentPrimaryKey = keyFromQuery(m_query, 4); +} + +bool IndexKeyCursorImpl::currentRowExists() +{ + String sql = "SELECT id FROM IndexData WHERE id = ?"; + SQLiteStatement statement(m_db, sql); + + bool ok = statement.prepare() == SQLResultOk; + ASSERT_UNUSED(ok, ok); + + statement.bindInt64(1, m_currentId); + return statement.step() == SQLResultRow; +} + +class IndexCursorImpl : public CursorImplCommon { +public: + IndexCursorImpl(SQLiteDatabase& sqliteDatabase, String query, bool uniquenessConstraint, bool iterateForward) + : CursorImplCommon(sqliteDatabase, query, uniquenessConstraint, iterateForward) + { + } + + // CursorImplCommon + virtual PassRefPtr<IDBKey> primaryKey() { return m_currentPrimaryKey; } + virtual String value() { return m_currentValue; } + virtual int64_t objectStoreDataId() { ASSERT_NOT_REACHED(); return 0; } + virtual int64_t indexDataId() { return m_currentId; } + virtual void loadCurrentRow(); + virtual bool currentRowExists(); + +private: + RefPtr<IDBKey> m_currentPrimaryKey; + String m_currentValue; +}; + +void IndexCursorImpl::loadCurrentRow() +{ + m_currentId = m_query.getColumnInt64(0); + m_currentKey = keyFromQuery(m_query, 1); + m_currentValue = m_query.getColumnBlobAsString(4); + m_currentPrimaryKey = keyFromQuery(m_query, 5); +} + +bool IndexCursorImpl::currentRowExists() +{ + String sql = "SELECT id FROM IndexData WHERE id = ?"; + SQLiteStatement statement(m_db, sql); + + bool ok = statement.prepare() == SQLResultOk; + ASSERT_UNUSED(ok, ok); + + statement.bindInt64(1, m_currentId); + return statement.step() == SQLResultRow; +} + +} // namespace + +PassRefPtr<IDBBackingStore::Cursor> IDBSQLiteBackingStore::openObjectStoreCursor(int64_t objectStoreId, const IDBKeyRange* range, IDBCursor::Direction direction) +{ + bool lowerBound = range && range->lower(); + bool upperBound = range && range->upper(); + + String sql = "SELECT id, keyString, keyDate, keyNumber, value FROM ObjectStoreData WHERE "; + if (lowerBound) + sql += lowerCursorWhereFragment(*range->lower(), range->lowerOpen() ? "<" : "<="); + if (upperBound) + sql += upperCursorWhereFragment(*range->upper(), range->upperOpen() ? "<" : "<="); + sql += "objectStoreId = ? ORDER BY "; + + if (direction == IDBCursor::NEXT || direction == IDBCursor::NEXT_NO_DUPLICATE) + sql += "keyString, keyDate, keyNumber"; + else + sql += "keyString DESC, keyDate DESC, keyNumber DESC"; + + RefPtr<ObjectStoreCursorImpl> cursor = adoptRef(new ObjectStoreCursorImpl(m_db, sql, direction == IDBCursor::NEXT_NO_DUPLICATE || direction == IDBCursor::PREV_NO_DUPLICATE, + direction == IDBCursor::NEXT_NO_DUPLICATE || direction == IDBCursor::NEXT)); + + bool ok = cursor->m_query.prepare() == SQLResultOk; + ASSERT_UNUSED(ok, ok); // FIXME: Better error handling? + + int currentColumn = 1; + if (lowerBound) + currentColumn += bindKeyToQuery(cursor->m_query, currentColumn, *range->lower()); + if (upperBound) + currentColumn += bindKeyToQuery(cursor->m_query, currentColumn, *range->upper()); + cursor->m_query.bindInt64(currentColumn, objectStoreId); + + if (cursor->m_query.step() != SQLResultRow) + return 0; + + cursor->loadCurrentRow(); + return cursor.release(); +} + +PassRefPtr<IDBBackingStore::Cursor> IDBSQLiteBackingStore::openIndexKeyCursor(int64_t indexId, const IDBKeyRange* range, IDBCursor::Direction direction) +{ + String sql = String("SELECT IndexData.id, IndexData.keyString, IndexData.keyDate, IndexData.keyNumber, ") + + ("ObjectStoreData.keyString, ObjectStoreData.keyDate, ObjectStoreData.keyNumber ") + + "FROM IndexData INNER JOIN ObjectStoreData ON IndexData.objectStoreDataId = ObjectStoreData.id WHERE "; + + bool lowerBound = range && range->lower(); + bool upperBound = range && range->upper(); + + if (lowerBound) + sql += lowerCursorWhereFragment(*range->lower(), range->lowerOpen() ? "<" : "<=", "IndexData."); + if (upperBound) + sql += upperCursorWhereFragment(*range->upper(), range->upperOpen() ? "<" : "<=", "IndexData."); + sql += "IndexData.indexId = ? ORDER BY "; + + if (direction == IDBCursor::NEXT || direction == IDBCursor::NEXT_NO_DUPLICATE) + sql += "IndexData.keyString, IndexData.keyDate, IndexData.keyNumber, IndexData.id"; + else + sql += "IndexData.keyString DESC, IndexData.keyDate DESC, IndexData.keyNumber DESC, IndexData.id DESC"; + + RefPtr<IndexKeyCursorImpl> cursor = adoptRef(new IndexKeyCursorImpl(m_db, sql, direction == IDBCursor::NEXT_NO_DUPLICATE || direction == IDBCursor::PREV_NO_DUPLICATE, + direction == IDBCursor::NEXT_NO_DUPLICATE || direction == IDBCursor::NEXT)); + + bool ok = cursor->m_query.prepare() == SQLResultOk; + ASSERT_UNUSED(ok, ok); // FIXME: Better error handling? + + int indexColumn = 1; + if (lowerBound) + indexColumn += bindKeyToQuery(cursor->m_query, indexColumn, *range->lower()); + if (upperBound) + indexColumn += bindKeyToQuery(cursor->m_query, indexColumn, *range->upper()); + cursor->m_query.bindInt64(indexColumn, indexId); + + if (cursor->m_query.step() != SQLResultRow) + return 0; + + cursor->loadCurrentRow(); + return cursor.release(); +} + +PassRefPtr<IDBBackingStore::Cursor> IDBSQLiteBackingStore::openIndexCursor(int64_t indexId, const IDBKeyRange* range, IDBCursor::Direction direction) +{ + String sql = String("SELECT IndexData.id, IndexData.keyString, IndexData.keyDate, IndexData.keyNumber, ") + + ("ObjectStoreData.value, ObjectStoreData.keyString, ObjectStoreData.keyDate, ObjectStoreData.keyNumber ") + + "FROM IndexData INNER JOIN ObjectStoreData ON IndexData.objectStoreDataId = ObjectStoreData.id WHERE "; + + bool lowerBound = range && range->lower(); + bool upperBound = range && range->upper(); + + if (lowerBound) + sql += lowerCursorWhereFragment(*range->lower(), range->lowerOpen() ? "<" : "<=", "IndexData."); + if (upperBound) + sql += upperCursorWhereFragment(*range->upper(), range->upperOpen() ? "<" : "<=", "IndexData."); + sql += "IndexData.indexId = ? ORDER BY "; + + if (direction == IDBCursor::NEXT || direction == IDBCursor::NEXT_NO_DUPLICATE) + sql += "IndexData.keyString, IndexData.keyDate, IndexData.keyNumber, IndexData.id"; + else + sql += "IndexData.keyString DESC, IndexData.keyDate DESC, IndexData.keyNumber DESC, IndexData.id DESC"; + + RefPtr<IndexCursorImpl> cursor = adoptRef(new IndexCursorImpl(m_db, sql, direction == IDBCursor::NEXT_NO_DUPLICATE || direction == IDBCursor::PREV_NO_DUPLICATE, + direction == IDBCursor::NEXT_NO_DUPLICATE || direction == IDBCursor::NEXT)); + + bool ok = cursor->m_query.prepare() == SQLResultOk; + ASSERT_UNUSED(ok, ok); // FIXME: Better error handling? + + int indexColumn = 1; + if (lowerBound) + indexColumn += bindKeyToQuery(cursor->m_query, indexColumn, *range->lower()); + if (upperBound) + indexColumn += bindKeyToQuery(cursor->m_query, indexColumn, *range->upper()); + cursor->m_query.bindInt64(indexColumn, indexId); + + if (cursor->m_query.step() != SQLResultRow) + return 0; + + cursor->loadCurrentRow(); + return cursor.release(); +} + +namespace { + +class TransactionImpl : public IDBBackingStore::Transaction { +public: + TransactionImpl(SQLiteDatabase& db) + : m_transaction(db) + { + } + + // IDBBackingStore::Transaction + virtual void begin() { m_transaction.begin(); } + virtual void commit() { m_transaction.commit(); } + virtual void rollback() { m_transaction.rollback(); } + +private: + SQLiteTransaction m_transaction; +}; + +} // namespace + +PassRefPtr<IDBBackingStore::Transaction> IDBSQLiteBackingStore::createTransaction() +{ + return adoptRef(new TransactionImpl(m_db)); +} + +} // namespace WebCore + +#endif // ENABLE(INDEXED_DATABASE) |