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