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..ae344c583f42f804b83eb89be87fdd75554e0bdb |
--- /dev/null |
+++ b/webkit/dom_storage/dom_storage_database.cc |
@@ -0,0 +1,267 @@ |
+// 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()) { |
+ NOTREACHED() << "Unable to open DOM storage database in memory."; |
+ 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; |
+ } |
+ } |
+ |
+ // 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 (!database_exists) { |
+ // This is a new database, create the table and we're done! |
+ if (CreateTableV2()) |
michaeln
2012/02/08 22:12:08
Do we want to DeleteAndRecreate() in this case of
benm (inactive)
2012/02/09 11:59:57
My thinking was that it doesn't hurt to have anoth
|
+ return true; |
+ } else { |
+ // The database exists already - check if we need to upgrade |
+ // and whether it's usable (i.e. not corrupted). |
+ SchemaVersion current_version = DetectSchemaVersion(); |
+ |
+ if (current_version == V2) { |
+ return true; |
+ } else if (current_version == V1) { |
+ if (UpgradeVersion1To2()) |
+ return true; |
+ } |
+ } |
+ |
+ // This is the exceptional case - to try and recover we'll attempt |
+ // to delete the file and start again. |
+ Close(); |
+ return DeleteFileAndRecreate(); |
+} |
+ |
+DomStorageDatabase::SchemaVersion DomStorageDatabase::DetectSchemaVersion() { |
+ DCHECK(IsOpen()); |
+ |
+ // Connection::Open() may succeed even if the file we try and open is not a |
+ // database, 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 when we later try and |
+ // run statements. So we run a query here that will not DCHECK but fail |
+ // on an invalid database to verify that what we've opened is usable. |
+ // 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. |
+ if (db_->ExecuteAndReturnErrorCode("PRAGMA quick_check(1)") != SQLITE_OK) |
michaeln
2012/02/08 22:12:08
Thnx for the explanation about statements yacking
benm (inactive)
2012/02/09 11:59:57
I just tried VACUUM and it seems to work. But I'm
|
+ return INVALID; |
+ |
+ // Look at the current schema - if it doesn't look right, assume corrupt. |
+ if (!db_->DoesTableExist("ItemTable") || |
+ !db_->DoesColumnExist("ItemTable", "key") || |
+ !db_->DoesColumnExist("ItemTable", "value")) |
+ return INVALID; |
+ |
+ // We must use a unique statement here as we aren't going to step it. |
+ sql::Statement statement( |
+ db_->GetUniqueStatement("SELECT key,value from ItemTable LIMIT 1")); |
+ if (statement.DeclaredColumnType(0) != sql::COLUMN_TYPE_TEXT) |
+ return INVALID; |
+ |
+ switch (statement.DeclaredColumnType(1)) { |
+ case sql::COLUMN_TYPE_BLOB: |
+ return V2; |
+ case sql::COLUMN_TYPE_TEXT: |
+ return V1; |
+ default: |
+ return INVALID; |
+ } |
+ NOTREACHED(); |
+ return INVALID; |
+} |
+ |
+bool DomStorageDatabase::CreateTableV2() { |
+ DCHECK(IsOpen()); |
+ |
+ return db_->Execute( |
+ "CREATE TABLE 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. |
michaeln
2012/02/08 22:12:08
why are we careful not to avoid deleting if the in
benm (inactive)
2012/02/09 11:59:57
I was a bit scared about accidentally deleting ent
|
+ if (!file_util::DirectoryExists(file_path_) && |
+ file_util::Delete(file_path_, false)) |
+ return LazyOpen(true); |
+ |
+ failed_to_open_ = true; |
+ return false; |
+} |
+ |
+bool DomStorageDatabase::UpgradeVersion1To2() { |
+ DCHECK(IsOpen()); |
+ DCHECK(DetectSchemaVersion() == V1); |
+ |
+ sql::Statement statement(db_->GetCachedStatement(SQL_FROM_HERE, |
+ "SELECT * FROM ItemTable")); |
+ DCHECK(statement.is_valid()); |
+ |
+ // 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")) { |
+ if (CreateTableV2()) { |
+ if (CommitChanges(false, values)) |
+ return migration.Commit(); |
michaeln
2012/02/08 22:12:08
style: not sure, would this be more readable?
Tra
benm (inactive)
2012/02/09 11:59:57
I like it.
|
+ } |
+ } |
+ return false; |
+} |
+ |
+void DomStorageDatabase::Close() { |
+ db_.reset(NULL); |
+} |
+ |
+} // namespace dom_storage |