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