history_database.cc revision 3345a6884c488ff3a535c2c9acdd33d74b37e311
1// Copyright (c) 2009 The Chromium Authors. All rights reserved. 2// Use of this source code is governed by a BSD-style license that can be 3// found in the LICENSE file. 4 5#include "chrome/browser/history/history_database.h" 6 7#include <algorithm> 8#include <set> 9#include <string> 10#include "app/sql/transaction.h" 11#include "base/command_line.h" 12#include "base/file_util.h" 13#if defined(OS_MACOSX) 14#include "base/mac_util.h" 15#endif 16#include "base/histogram.h" 17#include "base/rand_util.h" 18#include "base/string_util.h" 19#include "chrome/browser/diagnostics/sqlite_diagnostics.h" 20#include "chrome/common/chrome_switches.h" 21 22namespace history { 23 24namespace { 25 26// Current version number. We write databases at the "current" version number, 27// but any previous version that can read the "compatible" one can make do with 28// or database without *too* many bad effects. 29static const int kCurrentVersionNumber = 19; 30static const int kCompatibleVersionNumber = 16; 31static const char kEarlyExpirationThresholdKey[] = "early_expiration_threshold"; 32 33void ComputeDatabaseMetrics(const FilePath& history_name, 34 sql::Connection& db) { 35 if (base::RandInt(1, 100) != 50) 36 return; // Only do this computation sometimes since it can be expensive. 37 38 int64 file_size = 0; 39 if (!file_util::GetFileSize(history_name, &file_size)) 40 return; 41 int file_mb = static_cast<int>(file_size / (1024 * 1024)); 42 UMA_HISTOGRAM_MEMORY_MB("History.DatabaseFileMB", file_mb); 43 44 sql::Statement url_count(db.GetUniqueStatement("SELECT count(*) FROM urls")); 45 if (!url_count || !url_count.Step()) 46 return; 47 UMA_HISTOGRAM_COUNTS("History.URLTableCount", url_count.ColumnInt(0)); 48 49 sql::Statement visit_count(db.GetUniqueStatement( 50 "SELECT count(*) FROM visits")); 51 if (!visit_count || !visit_count.Step()) 52 return; 53 UMA_HISTOGRAM_COUNTS("History.VisitTableCount", visit_count.ColumnInt(0)); 54} 55 56} // namespace 57 58HistoryDatabase::HistoryDatabase() 59 : needs_version_17_migration_(false) { 60} 61 62HistoryDatabase::~HistoryDatabase() { 63} 64 65sql::InitStatus HistoryDatabase::Init(const FilePath& history_name, 66 const FilePath& bookmarks_path) { 67 // Set the exceptional sqlite error handler. 68 db_.set_error_delegate(GetErrorHandlerForHistoryDb()); 69 70 // Set the database page size to something a little larger to give us 71 // better performance (we're typically seek rather than bandwidth limited). 72 // This only has an effect before any tables have been created, otherwise 73 // this is a NOP. Must be a power of 2 and a max of 8192. 74 db_.set_page_size(4096); 75 76 // Increase the cache size. The page size, plus a little extra, times this 77 // value, tells us how much memory the cache will use maximum. 78 // 6000 * 4MB = 24MB 79 // TODO(brettw) scale this value to the amount of available memory. 80 db_.set_cache_size(6000); 81 82 // Note that we don't set exclusive locking here. That's done by 83 // BeginExclusiveMode below which is called later (we have to be in shared 84 // mode to start out for the in-memory backend to read the data). 85 86 if (!db_.Open(history_name)) 87 return sql::INIT_FAILURE; 88 89 // Wrap the rest of init in a tranaction. This will prevent the database from 90 // getting corrupted if we crash in the middle of initialization or migration. 91 sql::Transaction committer(&db_); 92 if (!committer.Begin()) 93 return sql::INIT_FAILURE; 94 95#if defined(OS_MACOSX) 96 // Exclude the history file and its journal from backups. 97 mac_util::SetFileBackupExclusion(history_name, true); 98 FilePath::StringType history_name_string(history_name.value()); 99 history_name_string += "-journal"; 100 FilePath history_journal_name(history_name_string); 101 mac_util::SetFileBackupExclusion(history_journal_name, true); 102#endif 103 104 // Prime the cache. 105 db_.Preload(); 106 107 // Create the tables and indices. 108 // NOTE: If you add something here, also add it to 109 // RecreateAllButStarAndURLTables. 110 if (!meta_table_.Init(&db_, GetCurrentVersion(), kCompatibleVersionNumber)) 111 return sql::INIT_FAILURE; 112 if (!CreateURLTable(false) || !InitVisitTable() || 113 !InitKeywordSearchTermsTable() || !InitDownloadTable() || 114 !InitSegmentTables()) 115 return sql::INIT_FAILURE; 116 CreateMainURLIndex(); 117 CreateSupplimentaryURLIndices(); 118 119 // Version check. 120 sql::InitStatus version_status = EnsureCurrentVersion(bookmarks_path); 121 if (version_status != sql::INIT_OK) 122 return version_status; 123 124 ComputeDatabaseMetrics(history_name, db_); 125 return committer.Commit() ? sql::INIT_OK : sql::INIT_FAILURE; 126} 127 128void HistoryDatabase::BeginExclusiveMode() { 129 // We can't use set_exclusive_locking() since that only has an effect before 130 // the DB is opened. 131 db_.Execute("PRAGMA locking_mode=EXCLUSIVE"); 132} 133 134// static 135int HistoryDatabase::GetCurrentVersion() { 136 return kCurrentVersionNumber; 137} 138 139void HistoryDatabase::BeginTransaction() { 140 db_.BeginTransaction(); 141} 142 143void HistoryDatabase::CommitTransaction() { 144 db_.CommitTransaction(); 145} 146 147bool HistoryDatabase::RecreateAllTablesButURL() { 148 if (!DropVisitTable()) 149 return false; 150 if (!InitVisitTable()) 151 return false; 152 153 if (!DropKeywordSearchTermsTable()) 154 return false; 155 if (!InitKeywordSearchTermsTable()) 156 return false; 157 158 if (!DropSegmentTables()) 159 return false; 160 if (!InitSegmentTables()) 161 return false; 162 163 // We also add the supplementary URL indices at this point. This index is 164 // over parts of the URL table that weren't automatically created when the 165 // temporary URL table was 166 CreateSupplimentaryURLIndices(); 167 return true; 168} 169 170void HistoryDatabase::Vacuum() { 171 DCHECK_EQ(0, db_.transaction_nesting()) << 172 "Can not have a transaction when vacuuming."; 173 db_.Execute("VACUUM"); 174} 175 176bool HistoryDatabase::SetSegmentID(VisitID visit_id, SegmentID segment_id) { 177 sql::Statement s(db_.GetCachedStatement(SQL_FROM_HERE, 178 "UPDATE visits SET segment_id = ? WHERE id = ?")); 179 if (!s) { 180 NOTREACHED() << db_.GetErrorMessage(); 181 return false; 182 } 183 s.BindInt64(0, segment_id); 184 s.BindInt64(1, visit_id); 185 DCHECK(db_.GetLastChangeCount() == 1); 186 return s.Run(); 187} 188 189SegmentID HistoryDatabase::GetSegmentID(VisitID visit_id) { 190 sql::Statement s(db_.GetCachedStatement(SQL_FROM_HERE, 191 "SELECT segment_id FROM visits WHERE id = ?")); 192 if (!s) { 193 NOTREACHED() << db_.GetErrorMessage(); 194 return 0; 195 } 196 197 s.BindInt64(0, visit_id); 198 if (s.Step()) { 199 if (s.ColumnType(0) == sql::COLUMN_TYPE_NULL) 200 return 0; 201 else 202 return s.ColumnInt64(0); 203 } 204 return 0; 205} 206 207base::Time HistoryDatabase::GetEarlyExpirationThreshold() { 208 if (!cached_early_expiration_threshold_.is_null()) 209 return cached_early_expiration_threshold_; 210 211 int64 threshold; 212 if (!meta_table_.GetValue(kEarlyExpirationThresholdKey, &threshold)) { 213 // Set to a very early non-zero time, so it's before all history, but not 214 // zero to avoid re-retrieval. 215 threshold = 1L; 216 } 217 218 cached_early_expiration_threshold_ = base::Time::FromInternalValue(threshold); 219 return cached_early_expiration_threshold_; 220} 221 222void HistoryDatabase::UpdateEarlyExpirationThreshold(base::Time threshold) { 223 meta_table_.SetValue(kEarlyExpirationThresholdKey, 224 threshold.ToInternalValue()); 225 cached_early_expiration_threshold_ = threshold; 226} 227 228sql::Connection& HistoryDatabase::GetDB() { 229 return db_; 230} 231 232// Migration ------------------------------------------------------------------- 233 234sql::InitStatus HistoryDatabase::EnsureCurrentVersion( 235 const FilePath& tmp_bookmarks_path) { 236 // We can't read databases newer than we were designed for. 237 if (meta_table_.GetCompatibleVersionNumber() > kCurrentVersionNumber) { 238 LOG(WARNING) << "History database is too new."; 239 return sql::INIT_TOO_NEW; 240 } 241 242 // NOTICE: If you are changing structures for things shared with the archived 243 // history file like URLs, visits, or downloads, that will need migration as 244 // well. Instead of putting such migration code in this class, it should be 245 // in the corresponding file (url_database.cc, etc.) and called from here and 246 // from the archived_database.cc. 247 248 int cur_version = meta_table_.GetVersionNumber(); 249 250 // Put migration code here 251 252 if (cur_version == 15) { 253 if (!MigrateBookmarksToFile(tmp_bookmarks_path) || 254 !DropStarredIDFromURLs()) { 255 LOG(WARNING) << "Unable to update history database to version 16."; 256 return sql::INIT_FAILURE; 257 } 258 ++cur_version; 259 meta_table_.SetVersionNumber(cur_version); 260 meta_table_.SetCompatibleVersionNumber( 261 std::min(cur_version, kCompatibleVersionNumber)); 262 } 263 264 if (cur_version == 16) { 265#if !defined(OS_WIN) 266 // In this version we bring the time format on Mac & Linux in sync with the 267 // Windows version so that profiles can be moved between computers. 268 MigrateTimeEpoch(); 269#endif 270 // On all platforms we bump the version number, so on Windows this 271 // migration is a NOP. We keep the compatible version at 16 since things 272 // will basically still work, just history will be in the future if an 273 // old version reads it. 274 ++cur_version; 275 meta_table_.SetVersionNumber(cur_version); 276 } 277 278 if (cur_version == 17) { 279 // Version 17 was for thumbnails to top sites migration. We ended up 280 // disabling it though, so 17->18 does nothing. 281 ++cur_version; 282 meta_table_.SetVersionNumber(cur_version); 283 } 284 285 if (cur_version == 18) { 286 // This is the version prior to adding url_source column. We need to 287 // migrate the database. 288 cur_version = 19; 289 meta_table_.SetVersionNumber(cur_version); 290 } 291 292 // When the version is too old, we just try to continue anyway, there should 293 // not be a released product that makes a database too old for us to handle. 294 LOG_IF(WARNING, cur_version < GetCurrentVersion()) << 295 "History database version " << cur_version << " is too old to handle."; 296 297 return sql::INIT_OK; 298} 299 300#if !defined(OS_WIN) 301void HistoryDatabase::MigrateTimeEpoch() { 302 // Update all the times in the URLs and visits table in the main database. 303 // For visits, clear the indexed flag since we'll delete the FTS databases in 304 // the next step. 305 db_.Execute( 306 "UPDATE urls " 307 "SET last_visit_time = last_visit_time + 11644473600000000 " 308 "WHERE id IN (SELECT id FROM urls WHERE last_visit_time > 0);"); 309 db_.Execute( 310 "UPDATE visits " 311 "SET visit_time = visit_time + 11644473600000000, is_indexed = 0 " 312 "WHERE id IN (SELECT id FROM visits WHERE visit_time > 0);"); 313 db_.Execute( 314 "UPDATE segment_usage " 315 "SET time_slot = time_slot + 11644473600000000 " 316 "WHERE id IN (SELECT id FROM segment_usage WHERE time_slot > 0);"); 317 318 // Erase all the full text index files. These will take a while to update and 319 // are less important, so we just blow them away. Same with the archived 320 // database. 321 needs_version_17_migration_ = true; 322} 323#endif 324 325void HistoryDatabase::MigrationToTopSitesDone() { 326 // TODO(sky): implement me. 327} 328 329} // namespace history 330