Chromium Code Reviews| Index: webkit/dom_storage/dom_storage_database.cc |
| diff --git a/webkit/dom_storage/dom_storage_database.cc b/webkit/dom_storage/dom_storage_database.cc |
| new file mode 100644 |
| index 0000000000000000000000000000000000000000..a6bba7bf3b0fc397d6ad5171fb734c1473ebd666 |
| --- /dev/null |
| +++ b/webkit/dom_storage/dom_storage_database.cc |
| @@ -0,0 +1,254 @@ |
| +// Copyright (c) 2012 The Chromium Authors. All rights reserved. |
| +// Use of this source code is governed by a BSD-style license that can be |
| +// found in the LICENSE file. |
| + |
| +#include "webkit/dom_storage/dom_storage_database.h" |
| + |
| +#include "base/file_util.h" |
| +#include "base/logging.h" |
| +#include "sql/diagnostic_error_delegate.h" |
| +#include "sql/statement.h" |
| +#include "sql/transaction.h" |
| +#include "third_party/sqlite/sqlite3.h" |
| + |
| +namespace { |
| + |
| +class HistogramUniquifier { |
| + public: |
| + static const char* name() { return "Sqlite.DomStorageDatabase.Error"; } |
| +}; |
| + |
| +sql::ErrorDelegate* GetErrorHandlerForDomStorageDatabase() { |
| + return new sql::DiagnosticErrorDelegate<HistogramUniquifier>(); |
| +} |
| + |
| +} // anon namespace |
| + |
| +namespace dom_storage { |
| + |
| +DomStorageDatabase::DomStorageDatabase(const FilePath& file_path) |
| + : file_path_(file_path), |
| + db_(NULL), |
| + failed_to_open_(false), |
| + tried_to_recreate_(false) { |
| + // Note: in normal use we should never get an empty backing path here. |
| + // However, the unit test for this class defines another constructor |
| + // that will bypass this check to allow an empty path that signifies |
| + // we should operate on an in-memory database for performance/reliability |
| + // reasons. |
| + DCHECK(!file_path_.empty()); |
| +} |
| + |
| +DomStorageDatabase::~DomStorageDatabase() { |
| +} |
| + |
| +void DomStorageDatabase::ReadAllValues(ValuesMap* result) { |
| + if (!LazyOpen(false)) |
| + return; |
| + |
| + sql::Statement statement(db_->GetCachedStatement(SQL_FROM_HERE, |
| + "SELECT * from ItemTable")); |
| + DCHECK(statement.is_valid()); |
| + |
| + while (statement.Step()) { |
| + string16 key = statement.ColumnString16(0); |
| + string16 value; |
| + statement.ColumnBlobAsString16(1, &value); |
| + (*result)[key] = NullableString16(value, false); |
| + } |
| +} |
| + |
| +bool DomStorageDatabase::CommitChanges(bool clear_all_first, |
| + const ValuesMap& changes) { |
| + if (!LazyOpen(!changes.empty())) |
| + return false; |
| + |
| + sql::Transaction transaction(db_.get()); |
| + if (!transaction.Begin()) |
| + return false; |
| + |
| + if (clear_all_first) { |
| + if (!db_->Execute("DELETE FROM ItemTable")) |
| + return false; |
| + } |
| + |
| + ValuesMap::const_iterator it = changes.begin(); |
| + for(; it != changes.end(); ++it) { |
| + sql::Statement statement; |
| + string16 key = it->first; |
| + NullableString16 value = it->second; |
| + if (value.is_null()) { |
| + statement.Assign(db_->GetCachedStatement(SQL_FROM_HERE, |
| + "DELETE FROM ItemTable WHERE key=?")); |
| + statement.BindString16(0, key); |
| + } else { |
| + statement.Assign(db_->GetCachedStatement(SQL_FROM_HERE, |
| + "INSERT INTO ItemTable VALUES (?,?)")); |
| + statement.BindString16(0, key); |
| + statement.BindBlob(1, value.string().data(), |
| + value.string().length() * sizeof(char16)); |
| + } |
| + DCHECK(statement.is_valid()); |
| + statement.Run(); |
| + } |
| + return transaction.Commit(); |
| +} |
| + |
| +bool DomStorageDatabase::LazyOpen(bool create_if_needed) { |
| + if (failed_to_open_) { |
| + // Don't try to open a database that we know has failed |
| + // already. |
| + return false; |
| + } |
| + |
| + if (IsOpen()) |
| + return true; |
| + |
| + bool database_exists = file_util::PathExists(file_path_); |
| + |
| + if (!database_exists && !create_if_needed) { |
| + // If the file doesn't exist already and we haven't been asked to create |
| + // a file on disk, then we don't bother opening the database. This means |
| + // we wait until we absolutely need to put something onto disk before we |
| + // do so. |
| + return false; |
| + } |
| + |
| + db_.reset(new sql::Connection()); |
| + db_->set_error_delegate(GetErrorHandlerForDomStorageDatabase()); |
| + |
| + if (file_path_.empty()) { |
| + // This code path should only be triggered by unit tests. |
| + if (!db_->OpenInMemory()) { |
| + LOG(ERROR) << "Unable to open DOM storage database in memory."; |
|
michaeln
2012/02/08 06:11:33
this could probably be a NOTREACHED() instead
benm (inactive)
2012/02/08 14:59:40
Done.
|
| + failed_to_open_ = true; |
| + return false; |
| + } |
| + } else { |
| + if (!db_->Open(file_path_)) { |
| + LOG(ERROR) << "Unable to open DOM storage database at " |
| + << file_path_.value() |
| + << " error: " << db_->GetErrorMessage(); |
| + if (database_exists && !tried_to_recreate_) |
| + return DeleteFileAndRecreate(); |
| + failed_to_open_ = true; |
| + return false; |
| + } |
| + } |
| + |
| + // Open() may succeed even if the file we try and open is not a database so |
| + // we check here that what we've got is something sensible. |
| + // TODO(benm): It might be useful to actually verify the output of the |
| + // quick_check too and try to recover in the case errors are detected. |
| + // However, in the case that the database is corrupted to the point that |
| + // SQLite doesn't actually think it's a database, |
| + // sql::Connection::GetCachedStatement will DCHECK. |
| + if (db_->ExecuteAndReturnErrorCode("PRAGMA quick_check(1)") != SQLITE_OK) { |
| + Close(); |
| + return DeleteFileAndRecreate(); |
| + } |
| + |
| + // sql::Connection uses UTF-8 encoding, but WebCore style databases use |
| + // UTF-16, so ensure we match. |
| + ignore_result(db_->Execute("PRAGMA encoding=\"UTF-16\"")); |
| + |
| + // If the table doesn't exist, try to create it at the current version. |
| + if (!db_->DoesTableExist("ItemTable")) { |
| + if (CreateTable()) |
| + return true; |
| + // Couldn't create the table. |
| + Close(); |
| + return DeleteFileAndRecreate(); |
| + } |
| + |
| + // Table exists, so ensure we're at the right version, upgrading if |
| + // necessary. |
| + if (UpgradeVersion1To2IfNeeded()) |
| + return true; |
| + |
| + // We were not able to verify that the database is at the correct version |
| + // so close the connection and attempt to recreate the database from |
| + // scratch. |
| + Close(); |
| + return DeleteFileAndRecreate(); |
| +} |
| + |
| +bool DomStorageDatabase::CreateTable() { |
| + DCHECK(IsOpen()); |
| + // Current version is 2. |
| + return CreateTableV2(); |
|
michaeln
2012/02/08 06:11:33
i still don't see the reason for two CreateTable m
benm (inactive)
2012/02/08 14:59:40
OK, done.
|
| +} |
| + |
| +bool DomStorageDatabase::CreateTableV2() { |
| + DCHECK(IsOpen()); |
| + |
| + return db_->Execute( |
| + "CREATE TABLE IF NOT EXISTS ItemTable (" |
| + "key TEXT UNIQUE ON CONFLICT REPLACE, " |
| + "value BLOB NOT NULL ON CONFLICT FAIL)"); |
| +} |
| + |
| +bool DomStorageDatabase::DeleteFileAndRecreate() { |
| + DCHECK(!IsOpen()); |
| + DCHECK(file_util::PathExists(file_path_)); |
| + |
| + // We should only try and do this once. |
| + if (tried_to_recreate_) |
| + return false; |
| + |
| + tried_to_recreate_ = true; |
| + |
| + // If it's not a directory and we can delete the file, try and open it again. |
| + if (!file_util::DirectoryExists(file_path_) && |
| + file_util::Delete(file_path_, false)) |
| + return LazyOpen(true); |
| + |
| + failed_to_open_ = true; |
| + return false; |
| +} |
| + |
| +bool DomStorageDatabase::UpgradeVersion1To2IfNeeded() { |
| + DCHECK(IsOpen()); |
| + sql::Statement statement(db_->GetCachedStatement(SQL_FROM_HERE, |
| + "SELECT * FROM ItemTable")); |
| + DCHECK(statement.is_valid()); |
| + |
| + // Quick check to see if we need to upgrade or not. The single |
| + // effect of V1 -> V2 is to change the value column from type |
| + // TEXT to type BLOB. |
| + sql::ColType value_column_type = statement.DeclaredColumnType(1); |
| + if (value_column_type == sql::COLUMN_TYPE_BLOB) |
| + return true; |
| + |
| + if (value_column_type != sql::COLUMN_TYPE_TEXT) { |
| + // Something is messed up. This is not a V1 database. |
| + return false; |
| + } |
| + |
| + // Need to migrate from TEXT value column to BLOB. |
| + // Store the current database content so we can re-insert |
| + // the data into the new V2 table. |
| + ValuesMap values; |
| + while (statement.Step()) { |
| + string16 key = statement.ColumnString16(0); |
| + NullableString16 value(statement.ColumnString16(1), false); |
| + values[key] = value; |
| + } |
| + |
| + sql::Transaction migration(db_.get()); |
| + if (!migration.Begin()) |
| + return false; |
| + |
| + if (db_->Execute("DROP TABLE ItemTable")) { |
| + CreateTableV2(); |
| + if (CommitChanges(false, values)) |
| + return migration.Commit(); |
| + } |
| + return false; |
| +} |
| + |
| +void DomStorageDatabase::Close() { |
| + db_.reset(NULL); |
| +} |
| + |
| +} // namespace dom_storage |