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