1 // Copyright (c) 2012 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 "base/file_util.h" 6 #include "base/memory/ref_counted.h" 7 #include "base/metrics/histogram.h" 8 #include "base/strings/string_split.h" 9 #include "base/strings/string_util.h" 10 #include "chrome/browser/history/history_types.h" 11 #include "chrome/browser/history/top_sites.h" 12 #include "chrome/browser/history/top_sites_database.h" 13 #include "chrome/common/thumbnail_score.h" 14 #include "sql/connection.h" 15 #include "sql/recovery.h" 16 #include "sql/statement.h" 17 #include "sql/transaction.h" 18 #include "third_party/sqlite/sqlite3.h" 19 20 // Description of database table: 21 // 22 // thumbnails 23 // url URL of the sites for which we have a thumbnail. 24 // url_rank Index of the URL in that thumbnail, 0-based. The thumbnail 25 // with the highest rank will be the next one evicted. Forced 26 // thumbnails have a rank of -1. 27 // title The title to display under that thumbnail. 28 // redirects A space separated list of URLs that are known to redirect 29 // to this url. 30 // boring_score How "boring" that thumbnail is. See ThumbnailScore. 31 // good_clipping True if the thumbnail was clipped from the bottom, keeping 32 // the entire width of the window. See ThumbnailScore. 33 // at_top True if the thumbnail was captured at the top of the 34 // website. 35 // last_updated The time at which this thumbnail was last updated. 36 // load_completed True if the thumbnail was captured after the page load was 37 // completed. 38 // last_forced If this is a forced thumbnail, records the last time it 39 // was forced. If it's not a forced thumbnail, 0. 40 41 namespace { 42 43 // For this database, schema migrations are deprecated after two 44 // years. This means that the oldest non-deprecated version should be 45 // two years old or greater (thus the migrations to get there are 46 // older). Databases containing deprecated versions will be cleared 47 // at startup. Since this database is a cache, losing old data is not 48 // fatal (in fact, very old data may be expired immediately at startup 49 // anyhow). 50 51 // Version 3: b6d6a783/r231648 by beaudoin (at) chromium.org on 2013-10-29 52 // Version 2: eb0b24e6/r87284 by satorux (at) chromium.org on 2011-05-31 53 // Version 1: 809cc4d8/r64072 by sky (at) chromium.org on 2010-10-27 (deprecated) 54 55 // NOTE(shess): When changing the version, add a new golden file for 56 // the new version and a test to verify that Init() works with it. 57 // NOTE(shess): RecoverDatabaseOrRaze() depends on the specific 58 // version number. The code is subtle and in development, contact me 59 // if the necessary changes are not obvious. 60 static const int kVersionNumber = 3; 61 static const int kDeprecatedVersionNumber = 1; // and earlier. 62 63 bool InitTables(sql::Connection* db) { 64 const char kThumbnailsSql[] = 65 "CREATE TABLE IF NOT EXISTS thumbnails (" 66 "url LONGVARCHAR PRIMARY KEY," 67 "url_rank INTEGER," 68 "title LONGVARCHAR," 69 "thumbnail BLOB," 70 "redirects LONGVARCHAR," 71 "boring_score DOUBLE DEFAULT 1.0," 72 "good_clipping INTEGER DEFAULT 0," 73 "at_top INTEGER DEFAULT 0," 74 "last_updated INTEGER DEFAULT 0," 75 "load_completed INTEGER DEFAULT 0," 76 "last_forced INTEGER DEFAULT 0)"; 77 return db->Execute(kThumbnailsSql); 78 } 79 80 // Encodes redirects into a string. 81 std::string GetRedirects(const history::MostVisitedURL& url) { 82 std::vector<std::string> redirects; 83 for (size_t i = 0; i < url.redirects.size(); i++) 84 redirects.push_back(url.redirects[i].spec()); 85 return JoinString(redirects, ' '); 86 } 87 88 // Decodes redirects from a string and sets them for the url. 89 void SetRedirects(const std::string& redirects, history::MostVisitedURL* url) { 90 std::vector<std::string> redirects_vector; 91 base::SplitStringAlongWhitespace(redirects, &redirects_vector); 92 for (size_t i = 0; i < redirects_vector.size(); ++i) 93 url->redirects.push_back(GURL(redirects_vector[i])); 94 } 95 96 // Track various failure (and success) cases in recovery code. 97 // 98 // TODO(shess): The recovery code is complete, but by nature runs in challenging 99 // circumstances, so initially the default error response is to leave the 100 // existing database in place. This histogram is intended to expose the 101 // failures seen in the fleet. Frequent failure cases can be explored more 102 // deeply to see if the complexity to fix them is warranted. Infrequent failure 103 // cases can be resolved by marking the database unrecoverable (which will 104 // delete the data). 105 // 106 // Based on the thumbnail_database.cc recovery code, FAILED_SCOPER should 107 // dominate, followed distantly by FAILED_META, with few or no other failures. 108 enum RecoveryEventType { 109 // Database successfully recovered. 110 RECOVERY_EVENT_RECOVERED = 0, 111 112 // Database successfully deprecated. 113 RECOVERY_EVENT_DEPRECATED, 114 115 // Sqlite.RecoveryEvent can usually be used to get more detail about the 116 // specific failure (see sql/recovery.cc). 117 RECOVERY_EVENT_FAILED_SCOPER, 118 RECOVERY_EVENT_FAILED_META_VERSION, 119 RECOVERY_EVENT_FAILED_META_WRONG_VERSION, 120 RECOVERY_EVENT_FAILED_META_INIT, 121 RECOVERY_EVENT_FAILED_SCHEMA_INIT, 122 RECOVERY_EVENT_FAILED_AUTORECOVER_THUMBNAILS, 123 RECOVERY_EVENT_FAILED_COMMIT, 124 125 // Track invariants resolved by FixThumbnailsTable(). 126 RECOVERY_EVENT_INVARIANT_RANK, 127 RECOVERY_EVENT_INVARIANT_REDIRECT, 128 RECOVERY_EVENT_INVARIANT_CONTIGUOUS, 129 130 // Always keep this at the end. 131 RECOVERY_EVENT_MAX, 132 }; 133 134 void RecordRecoveryEvent(RecoveryEventType recovery_event) { 135 UMA_HISTOGRAM_ENUMERATION("History.TopSitesRecovery", 136 recovery_event, RECOVERY_EVENT_MAX); 137 } 138 139 // Most corruption comes down to atomic updates between pages being broken 140 // somehow. This can result in either missing data, or overlapping data, 141 // depending on the operation broken. This table has large rows, which will use 142 // overflow pages, so it is possible (though unlikely) that a chain could fit 143 // together and yield a row with errors. 144 void FixThumbnailsTable(sql::Connection* db) { 145 // Enforce invariant separating forced and non-forced thumbnails. 146 const char kFixRankSql[] = 147 "DELETE FROM thumbnails " 148 "WHERE (url_rank = -1 AND last_forced = 0) " 149 "OR (url_rank <> -1 AND last_forced <> 0)"; 150 ignore_result(db->Execute(kFixRankSql)); 151 if (db->GetLastChangeCount() > 0) 152 RecordRecoveryEvent(RECOVERY_EVENT_INVARIANT_RANK); 153 154 // Enforce invariant that url is in its own redirects. 155 const char kFixRedirectsSql[] = 156 "DELETE FROM thumbnails " 157 "WHERE url <> substr(redirects, -length(url), length(url))"; 158 ignore_result(db->Execute(kFixRedirectsSql)); 159 if (db->GetLastChangeCount() > 0) 160 RecordRecoveryEvent(RECOVERY_EVENT_INVARIANT_REDIRECT); 161 162 // Enforce invariant that url_rank>=0 forms a contiguous series. 163 // TODO(shess): I have not found an UPDATE+SUBSELECT method of managing this. 164 // It can be done with a temporary table and a subselect, but doing it 165 // manually is easier to follow. Another option would be to somehow integrate 166 // the renumbering into the table recovery code. 167 const char kByRankSql[] = 168 "SELECT url_rank, rowid FROM thumbnails WHERE url_rank <> -1 " 169 "ORDER BY url_rank"; 170 sql::Statement select_statement(db->GetUniqueStatement(kByRankSql)); 171 172 const char kAdjustRankSql[] = 173 "UPDATE thumbnails SET url_rank = ? WHERE rowid = ?"; 174 sql::Statement update_statement(db->GetUniqueStatement(kAdjustRankSql)); 175 176 // Update any rows where |next_rank| doesn't match |url_rank|. 177 int next_rank = 0; 178 bool adjusted = false; 179 while (select_statement.Step()) { 180 const int url_rank = select_statement.ColumnInt(0); 181 if (url_rank != next_rank) { 182 adjusted = true; 183 update_statement.Reset(true); 184 update_statement.BindInt(0, next_rank); 185 update_statement.BindInt64(1, select_statement.ColumnInt64(1)); 186 update_statement.Run(); 187 } 188 ++next_rank; 189 } 190 if (adjusted) 191 RecordRecoveryEvent(RECOVERY_EVENT_INVARIANT_CONTIGUOUS); 192 } 193 194 // Recover the database to the extent possible, razing it if recovery is not 195 // possible. 196 void RecoverDatabaseOrRaze(sql::Connection* db, const base::FilePath& db_path) { 197 // NOTE(shess): If the version changes, review this code. 198 DCHECK_EQ(3, kVersionNumber); 199 200 // It is almost certain that some operation against |db| will fail, prevent 201 // reentry. 202 db->reset_error_callback(); 203 204 // For generating histogram stats. 205 size_t thumbnails_recovered = 0; 206 int64 original_size = 0; 207 base::GetFileSize(db_path, &original_size); 208 209 scoped_ptr<sql::Recovery> recovery = sql::Recovery::Begin(db, db_path); 210 if (!recovery) { 211 RecordRecoveryEvent(RECOVERY_EVENT_FAILED_SCOPER); 212 return; 213 } 214 215 // Setup the meta recovery table and fetch the version number from the corrupt 216 // database. 217 int version = 0; 218 if (!recovery->SetupMeta() || !recovery->GetMetaVersionNumber(&version)) { 219 // TODO(shess): Prior histograms indicate all failures are in creating the 220 // recover virtual table for corrupt.meta. The table may not exist, or the 221 // database may be too far gone. Either way, unclear how to resolve. 222 sql::Recovery::Rollback(recovery.Pass()); 223 RecordRecoveryEvent(RECOVERY_EVENT_FAILED_META_VERSION); 224 return; 225 } 226 227 // This code runs in a context which may be able to read version information 228 // that the regular deprecation path cannot. The effect of this code will be 229 // to raze the database. 230 if (version <= kDeprecatedVersionNumber) { 231 sql::Recovery::Unrecoverable(recovery.Pass()); 232 RecordRecoveryEvent(RECOVERY_EVENT_DEPRECATED); 233 return; 234 } 235 236 // TODO(shess): Earlier versions have been deprecated, later versions should 237 // be impossible. Unrecoverable() seems like a feasible response if this is 238 // infrequent enough. 239 if (version != 2 && version != 3) { 240 RecordRecoveryEvent(RECOVERY_EVENT_FAILED_META_WRONG_VERSION); 241 sql::Recovery::Rollback(recovery.Pass()); 242 return; 243 } 244 245 // Both v2 and v3 recover to current schema version. 246 sql::MetaTable recover_meta_table; 247 if (!recover_meta_table.Init(recovery->db(), kVersionNumber, 248 kVersionNumber)) { 249 sql::Recovery::Rollback(recovery.Pass()); 250 RecordRecoveryEvent(RECOVERY_EVENT_FAILED_META_INIT); 251 return; 252 } 253 254 // Create a fresh version of the schema. The recovery code uses 255 // conflict-resolution to handle duplicates, so any indices are necessary. 256 if (!InitTables(recovery->db())) { 257 // TODO(shess): Unable to create the new schema in the new database. The 258 // new database should be a temporary file, so being unable to work with it 259 // is pretty unclear. 260 // 261 // What are the potential responses, even? The recovery database could be 262 // opened as in-memory. If the temp database had a filesystem problem and 263 // the temp filesystem differs from the main database, then that could fix 264 // it. 265 sql::Recovery::Rollback(recovery.Pass()); 266 RecordRecoveryEvent(RECOVERY_EVENT_FAILED_SCHEMA_INIT); 267 return; 268 } 269 270 // The |1| is because v2 [thumbnails] has one less column than v3 did. In the 271 // v2 case the column will get default values. 272 if (!recovery->AutoRecoverTable("thumbnails", 1, &thumbnails_recovered)) { 273 sql::Recovery::Rollback(recovery.Pass()); 274 RecordRecoveryEvent(RECOVERY_EVENT_FAILED_AUTORECOVER_THUMBNAILS); 275 return; 276 } 277 278 // TODO(shess): Inline this? 279 FixThumbnailsTable(recovery->db()); 280 281 if (!sql::Recovery::Recovered(recovery.Pass())) { 282 // TODO(shess): Very unclear what this failure would actually mean, and what 283 // should be done. Add histograms to Recovered() implementation to get some 284 // insight. 285 RecordRecoveryEvent(RECOVERY_EVENT_FAILED_COMMIT); 286 return; 287 } 288 289 // Track the size of the recovered database relative to the size of the input 290 // database. The size should almost always be smaller, unless the input 291 // database was empty to start with. If the percentage results are very low, 292 // something is awry. 293 int64 final_size = 0; 294 if (original_size > 0 && 295 base::GetFileSize(db_path, &final_size) && 296 final_size > 0) { 297 UMA_HISTOGRAM_PERCENTAGE("History.TopSitesRecoveredPercentage", 298 final_size * 100 / original_size); 299 } 300 301 // Using 10,000 because these cases mostly care about "none recovered" and 302 // "lots recovered". More than 10,000 rows recovered probably means there's 303 // something wrong with the profile. 304 UMA_HISTOGRAM_COUNTS_10000("History.TopSitesRecoveredRowsThumbnails", 305 thumbnails_recovered); 306 307 RecordRecoveryEvent(RECOVERY_EVENT_RECOVERED); 308 } 309 310 void DatabaseErrorCallback(sql::Connection* db, 311 const base::FilePath& db_path, 312 int extended_error, 313 sql::Statement* stmt) { 314 // TODO(shess): Assert that this is running on a safe thread. AFAICT, should 315 // be the history thread, but at this level I can't see how to reach that. 316 317 // Attempt to recover corrupt databases. 318 int error = (extended_error & 0xFF); 319 if (error == SQLITE_CORRUPT || 320 error == SQLITE_CANTOPEN || 321 error == SQLITE_NOTADB) { 322 RecoverDatabaseOrRaze(db, db_path); 323 } 324 325 // TODO(shess): This database's error histograms look like: 326 // 84% SQLITE_CORRUPT, SQLITE_CANTOPEN, SQLITE_NOTADB 327 // 7% SQLITE_ERROR 328 // 6% SQLITE_IOERR variants 329 // 2% SQLITE_READONLY 330 // .4% SQLITE_FULL 331 // nominal SQLITE_TOBIG, SQLITE_AUTH, and SQLITE_BUSY. In the case of 332 // thumbnail_database.cc, as soon as the recovery code landed, SQLITE_IOERR 333 // shot to leadership. If the I/O error is system-level, there is probably no 334 // hope, but if it is restricted to something about the database file, it is 335 // possible that the recovery code could be brought to bear. In fact, it is 336 // possible that running recovery would be a reasonable default when errors 337 // are seen. 338 339 // The default handling is to assert on debug and to ignore on release. 340 if (!sql::Connection::ShouldIgnoreSqliteError(extended_error)) 341 DLOG(FATAL) << db->GetErrorMessage(); 342 } 343 344 } // namespace 345 346 namespace history { 347 348 // static 349 const int TopSitesDatabase::kRankOfForcedURL = -1; 350 351 // static 352 const int TopSitesDatabase::kRankOfNonExistingURL = -2; 353 354 TopSitesDatabase::TopSitesDatabase() { 355 } 356 357 TopSitesDatabase::~TopSitesDatabase() { 358 } 359 360 bool TopSitesDatabase::Init(const base::FilePath& db_name) { 361 // Retry failed InitImpl() in case the recovery system fixed things. 362 // TODO(shess): Instrument to figure out if there are any persistent failure 363 // cases which do not resolve themselves. 364 const size_t kAttempts = 2; 365 366 for (size_t i = 0; i < kAttempts; ++i) { 367 if (InitImpl(db_name)) 368 return true; 369 370 meta_table_.Reset(); 371 db_.reset(); 372 } 373 return false; 374 } 375 376 bool TopSitesDatabase::InitImpl(const base::FilePath& db_name) { 377 const bool file_existed = base::PathExists(db_name); 378 379 db_.reset(CreateDB(db_name)); 380 if (!db_) 381 return false; 382 383 // An older version had data with no meta table. Deprecate by razing. 384 // TODO(shess): Just have RazeIfDeprecated() handle this case. 385 const bool does_meta_exist = sql::MetaTable::DoesTableExist(db_.get()); 386 if (!does_meta_exist && file_existed) { 387 if (!db_->Raze()) 388 return false; 389 } 390 391 // Clear databases which are too old to process. 392 DCHECK_LT(kDeprecatedVersionNumber, kVersionNumber); 393 sql::MetaTable::RazeIfDeprecated(db_.get(), kDeprecatedVersionNumber); 394 395 // Scope initialization in a transaction so we can't be partially 396 // initialized. 397 sql::Transaction transaction(db_.get()); 398 // TODO(shess): Failure to open transaction is bad, address it. 399 if (!transaction.Begin()) 400 return false; 401 402 if (!meta_table_.Init(db_.get(), kVersionNumber, kVersionNumber)) 403 return false; 404 405 if (!InitTables(db_.get())) 406 return false; 407 408 if (meta_table_.GetVersionNumber() == 2) { 409 if (!UpgradeToVersion3()) { 410 LOG(WARNING) << "Unable to upgrade top sites database to version 3."; 411 return false; 412 } 413 } 414 415 // Version check. 416 if (meta_table_.GetVersionNumber() != kVersionNumber) 417 return false; 418 419 // Initialization is complete. 420 if (!transaction.Commit()) 421 return false; 422 423 return true; 424 } 425 426 bool TopSitesDatabase::UpgradeToVersion3() { 427 // Add 'last_forced' column. 428 if (!db_->Execute( 429 "ALTER TABLE thumbnails ADD last_forced INTEGER DEFAULT 0")) { 430 NOTREACHED(); 431 return false; 432 } 433 meta_table_.SetVersionNumber(3); 434 return true; 435 } 436 437 void TopSitesDatabase::GetPageThumbnails(MostVisitedURLList* urls, 438 URLToImagesMap* thumbnails) { 439 sql::Statement statement(db_->GetCachedStatement( 440 SQL_FROM_HERE, 441 "SELECT url, url_rank, title, thumbnail, redirects, " 442 "boring_score, good_clipping, at_top, last_updated, load_completed, " 443 "last_forced FROM thumbnails ORDER BY url_rank, last_forced")); 444 445 if (!statement.is_valid()) { 446 LOG(WARNING) << db_->GetErrorMessage(); 447 return; 448 } 449 450 urls->clear(); 451 thumbnails->clear(); 452 453 while (statement.Step()) { 454 // Results are sorted by url_rank. For forced thumbnails with url_rank = -1, 455 // thumbnails are sorted by last_forced. 456 MostVisitedURL url; 457 GURL gurl(statement.ColumnString(0)); 458 url.url = gurl; 459 url.title = statement.ColumnString16(2); 460 url.last_forced_time = 461 base::Time::FromInternalValue(statement.ColumnInt64(10)); 462 std::string redirects = statement.ColumnString(4); 463 SetRedirects(redirects, &url); 464 urls->push_back(url); 465 466 std::vector<unsigned char> data; 467 statement.ColumnBlobAsVector(3, &data); 468 Images thumbnail; 469 if (!data.empty()) 470 thumbnail.thumbnail = base::RefCountedBytes::TakeVector(&data); 471 thumbnail.thumbnail_score.boring_score = statement.ColumnDouble(5); 472 thumbnail.thumbnail_score.good_clipping = statement.ColumnBool(6); 473 thumbnail.thumbnail_score.at_top = statement.ColumnBool(7); 474 thumbnail.thumbnail_score.time_at_snapshot = 475 base::Time::FromInternalValue(statement.ColumnInt64(8)); 476 thumbnail.thumbnail_score.load_completed = statement.ColumnBool(9); 477 (*thumbnails)[gurl] = thumbnail; 478 } 479 } 480 481 void TopSitesDatabase::SetPageThumbnail(const MostVisitedURL& url, 482 int new_rank, 483 const Images& thumbnail) { 484 sql::Transaction transaction(db_.get()); 485 transaction.Begin(); 486 487 int rank = GetURLRank(url); 488 if (rank == kRankOfNonExistingURL) { 489 AddPageThumbnail(url, new_rank, thumbnail); 490 } else { 491 UpdatePageRankNoTransaction(url, new_rank); 492 UpdatePageThumbnail(url, thumbnail); 493 } 494 495 transaction.Commit(); 496 } 497 498 bool TopSitesDatabase::UpdatePageThumbnail( 499 const MostVisitedURL& url, const Images& thumbnail) { 500 sql::Statement statement(db_->GetCachedStatement( 501 SQL_FROM_HERE, 502 "UPDATE thumbnails SET " 503 "title = ?, thumbnail = ?, redirects = ?, " 504 "boring_score = ?, good_clipping = ?, at_top = ?, last_updated = ?, " 505 "load_completed = ?, last_forced = ?" 506 "WHERE url = ? ")); 507 statement.BindString16(0, url.title); 508 if (thumbnail.thumbnail.get() && thumbnail.thumbnail->front()) { 509 statement.BindBlob(1, thumbnail.thumbnail->front(), 510 static_cast<int>(thumbnail.thumbnail->size())); 511 } 512 statement.BindString(2, GetRedirects(url)); 513 const ThumbnailScore& score = thumbnail.thumbnail_score; 514 statement.BindDouble(3, score.boring_score); 515 statement.BindBool(4, score.good_clipping); 516 statement.BindBool(5, score.at_top); 517 statement.BindInt64(6, score.time_at_snapshot.ToInternalValue()); 518 statement.BindBool(7, score.load_completed); 519 statement.BindInt64(8, url.last_forced_time.ToInternalValue()); 520 statement.BindString(9, url.url.spec()); 521 522 return statement.Run(); 523 } 524 525 void TopSitesDatabase::AddPageThumbnail(const MostVisitedURL& url, 526 int new_rank, 527 const Images& thumbnail) { 528 sql::Statement statement(db_->GetCachedStatement( 529 SQL_FROM_HERE, 530 "INSERT OR REPLACE INTO thumbnails " 531 "(url, url_rank, title, thumbnail, redirects, " 532 "boring_score, good_clipping, at_top, last_updated, load_completed, " 533 "last_forced) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)")); 534 statement.BindString(0, url.url.spec()); 535 statement.BindInt(1, kRankOfForcedURL); // Fist make it a forced thumbnail. 536 statement.BindString16(2, url.title); 537 if (thumbnail.thumbnail.get() && thumbnail.thumbnail->front()) { 538 statement.BindBlob(3, thumbnail.thumbnail->front(), 539 static_cast<int>(thumbnail.thumbnail->size())); 540 } 541 statement.BindString(4, GetRedirects(url)); 542 const ThumbnailScore& score = thumbnail.thumbnail_score; 543 statement.BindDouble(5, score.boring_score); 544 statement.BindBool(6, score.good_clipping); 545 statement.BindBool(7, score.at_top); 546 statement.BindInt64(8, score.time_at_snapshot.ToInternalValue()); 547 statement.BindBool(9, score.load_completed); 548 int64 last_forced = url.last_forced_time.ToInternalValue(); 549 DCHECK((last_forced == 0) == (new_rank != kRankOfForcedURL)) 550 << "Thumbnail without a forced time stamp has a forced rank, or the " 551 << "opposite."; 552 statement.BindInt64(10, last_forced); 553 if (!statement.Run()) 554 return; 555 556 // Update rank if this is not a forced thumbnail. 557 if (new_rank != kRankOfForcedURL) 558 UpdatePageRankNoTransaction(url, new_rank); 559 } 560 561 void TopSitesDatabase::UpdatePageRank(const MostVisitedURL& url, 562 int new_rank) { 563 DCHECK((url.last_forced_time.ToInternalValue() == 0) == 564 (new_rank != kRankOfForcedURL)) 565 << "Thumbnail without a forced time stamp has a forced rank, or the " 566 << "opposite."; 567 sql::Transaction transaction(db_.get()); 568 transaction.Begin(); 569 UpdatePageRankNoTransaction(url, new_rank); 570 transaction.Commit(); 571 } 572 573 // Caller should have a transaction open. 574 void TopSitesDatabase::UpdatePageRankNoTransaction( 575 const MostVisitedURL& url, int new_rank) { 576 DCHECK_GT(db_->transaction_nesting(), 0); 577 DCHECK((url.last_forced_time.is_null()) == (new_rank != kRankOfForcedURL)) 578 << "Thumbnail without a forced time stamp has a forced rank, or the " 579 << "opposite."; 580 581 int prev_rank = GetURLRank(url); 582 if (prev_rank == kRankOfNonExistingURL) { 583 LOG(WARNING) << "Updating rank of an unknown URL: " << url.url.spec(); 584 return; 585 } 586 587 // Shift the ranks. 588 if (prev_rank > new_rank) { 589 if (new_rank == kRankOfForcedURL) { 590 // From non-forced to forced, shift down. 591 // Example: 2 -> -1 592 // -1, -1, -1, 0, 1, [2 -> -1], [3 -> 2], [4 -> 3] 593 sql::Statement shift_statement(db_->GetCachedStatement( 594 SQL_FROM_HERE, 595 "UPDATE thumbnails " 596 "SET url_rank = url_rank - 1 " 597 "WHERE url_rank > ?")); 598 shift_statement.BindInt(0, prev_rank); 599 shift_statement.Run(); 600 } else { 601 // From non-forced to non-forced, shift up. 602 // Example: 3 -> 1 603 // -1, -1, -1, 0, [1 -> 2], [2 -> 3], [3 -> 1], 4 604 sql::Statement shift_statement(db_->GetCachedStatement( 605 SQL_FROM_HERE, 606 "UPDATE thumbnails " 607 "SET url_rank = url_rank + 1 " 608 "WHERE url_rank >= ? AND url_rank < ?")); 609 shift_statement.BindInt(0, new_rank); 610 shift_statement.BindInt(1, prev_rank); 611 shift_statement.Run(); 612 } 613 } else if (prev_rank < new_rank) { 614 if (prev_rank == kRankOfForcedURL) { 615 // From non-forced to forced, shift up. 616 // Example: -1 -> 2 617 // -1, [-1 -> 2], -1, 0, 1, [2 -> 3], [3 -> 4], [4 -> 5] 618 sql::Statement shift_statement(db_->GetCachedStatement( 619 SQL_FROM_HERE, 620 "UPDATE thumbnails " 621 "SET url_rank = url_rank + 1 " 622 "WHERE url_rank >= ?")); 623 shift_statement.BindInt(0, new_rank); 624 shift_statement.Run(); 625 } else { 626 // From non-forced to non-forced, shift down. 627 // Example: 1 -> 3. 628 // -1, -1, -1, 0, [1 -> 3], [2 -> 1], [3 -> 2], 4 629 sql::Statement shift_statement(db_->GetCachedStatement( 630 SQL_FROM_HERE, 631 "UPDATE thumbnails " 632 "SET url_rank = url_rank - 1 " 633 "WHERE url_rank > ? AND url_rank <= ?")); 634 shift_statement.BindInt(0, prev_rank); 635 shift_statement.BindInt(1, new_rank); 636 shift_statement.Run(); 637 } 638 } 639 640 // Set the url's rank and last_forced, since the latter changes when a URL 641 // goes from forced to non-forced and vice-versa. 642 sql::Statement set_statement(db_->GetCachedStatement( 643 SQL_FROM_HERE, 644 "UPDATE thumbnails " 645 "SET url_rank = ?, last_forced = ? " 646 "WHERE url == ?")); 647 set_statement.BindInt(0, new_rank); 648 set_statement.BindInt64(1, url.last_forced_time.ToInternalValue()); 649 set_statement.BindString(2, url.url.spec()); 650 set_statement.Run(); 651 } 652 653 bool TopSitesDatabase::GetPageThumbnail(const GURL& url, 654 Images* thumbnail) { 655 sql::Statement statement(db_->GetCachedStatement( 656 SQL_FROM_HERE, 657 "SELECT thumbnail, boring_score, good_clipping, at_top, last_updated " 658 "FROM thumbnails WHERE url=?")); 659 statement.BindString(0, url.spec()); 660 if (!statement.Step()) 661 return false; 662 663 std::vector<unsigned char> data; 664 statement.ColumnBlobAsVector(0, &data); 665 thumbnail->thumbnail = base::RefCountedBytes::TakeVector(&data); 666 thumbnail->thumbnail_score.boring_score = statement.ColumnDouble(1); 667 thumbnail->thumbnail_score.good_clipping = statement.ColumnBool(2); 668 thumbnail->thumbnail_score.at_top = statement.ColumnBool(3); 669 thumbnail->thumbnail_score.time_at_snapshot = 670 base::Time::FromInternalValue(statement.ColumnInt64(4)); 671 return true; 672 } 673 674 int TopSitesDatabase::GetURLRank(const MostVisitedURL& url) { 675 sql::Statement select_statement(db_->GetCachedStatement( 676 SQL_FROM_HERE, 677 "SELECT url_rank " 678 "FROM thumbnails WHERE url=?")); 679 select_statement.BindString(0, url.url.spec()); 680 if (select_statement.Step()) 681 return select_statement.ColumnInt(0); 682 683 return kRankOfNonExistingURL; 684 } 685 686 // Remove the record for this URL. Returns true iff removed successfully. 687 bool TopSitesDatabase::RemoveURL(const MostVisitedURL& url) { 688 int old_rank = GetURLRank(url); 689 if (old_rank == kRankOfNonExistingURL) 690 return false; 691 692 sql::Transaction transaction(db_.get()); 693 transaction.Begin(); 694 if (old_rank != kRankOfForcedURL) { 695 // Decrement all following ranks. 696 sql::Statement shift_statement(db_->GetCachedStatement( 697 SQL_FROM_HERE, 698 "UPDATE thumbnails " 699 "SET url_rank = url_rank - 1 " 700 "WHERE url_rank > ?")); 701 shift_statement.BindInt(0, old_rank); 702 703 if (!shift_statement.Run()) 704 return false; 705 } 706 707 sql::Statement delete_statement( 708 db_->GetCachedStatement(SQL_FROM_HERE, 709 "DELETE FROM thumbnails WHERE url = ?")); 710 delete_statement.BindString(0, url.url.spec()); 711 712 if (!delete_statement.Run()) 713 return false; 714 715 return transaction.Commit(); 716 } 717 718 sql::Connection* TopSitesDatabase::CreateDB(const base::FilePath& db_name) { 719 scoped_ptr<sql::Connection> db(new sql::Connection()); 720 // Settings copied from ThumbnailDatabase. 721 db->set_histogram_tag("TopSites"); 722 db->set_error_callback(base::Bind(&DatabaseErrorCallback, 723 db.get(), db_name)); 724 db->set_page_size(4096); 725 db->set_cache_size(32); 726 727 if (!db->Open(db_name)) 728 return NULL; 729 return db.release(); 730 } 731 732 } // namespace history 733