1// Copyright 2014 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 "components/history/core/browser/url_database.h"
6
7#include <limits>
8#include <string>
9#include <vector>
10
11#include "base/i18n/case_conversion.h"
12#include "base/memory/scoped_vector.h"
13#include "base/strings/utf_string_conversions.h"
14#include "components/history/core/browser/keyword_search_term.h"
15#include "net/base/net_util.h"
16#include "sql/statement.h"
17#include "url/gurl.h"
18
19namespace history {
20
21const char URLDatabase::kURLRowFields[] = HISTORY_URL_ROW_FIELDS;
22const int URLDatabase::kNumURLRowFields = 9;
23
24URLDatabase::URLEnumeratorBase::URLEnumeratorBase()
25    : initialized_(false) {
26}
27
28URLDatabase::URLEnumeratorBase::~URLEnumeratorBase() {
29}
30
31URLDatabase::URLEnumerator::URLEnumerator() {
32}
33
34bool URLDatabase::URLEnumerator::GetNextURL(URLRow* r) {
35  if (statement_.Step()) {
36    FillURLRow(statement_, r);
37    return true;
38  }
39  return false;
40}
41
42URLDatabase::URLDatabase()
43    : has_keyword_search_terms_(false) {
44}
45
46URLDatabase::~URLDatabase() {
47}
48
49// static
50std::string URLDatabase::GURLToDatabaseURL(const GURL& gurl) {
51  // TODO(brettw): do something fancy here with encoding, etc.
52
53  // Strip username and password from URL before sending to DB.
54  GURL::Replacements replacements;
55  replacements.ClearUsername();
56  replacements.ClearPassword();
57
58  return (gurl.ReplaceComponents(replacements)).spec();
59}
60
61// Convenience to fill a history::URLRow. Must be in sync with the fields in
62// kURLRowFields.
63void URLDatabase::FillURLRow(sql::Statement& s, history::URLRow* i) {
64  DCHECK(i);
65  i->id_ = s.ColumnInt64(0);
66  i->url_ = GURL(s.ColumnString(1));
67  i->title_ = s.ColumnString16(2);
68  i->visit_count_ = s.ColumnInt(3);
69  i->typed_count_ = s.ColumnInt(4);
70  i->last_visit_ = base::Time::FromInternalValue(s.ColumnInt64(5));
71  i->hidden_ = s.ColumnInt(6) != 0;
72}
73
74bool URLDatabase::GetURLRow(URLID url_id, URLRow* info) {
75  // TODO(brettw) We need check for empty URLs to handle the case where
76  // there are old URLs in the database that are empty that got in before
77  // we added any checks. We should eventually be able to remove it
78  // when all inputs are using GURL (which prohibit empty input).
79  sql::Statement statement(GetDB().GetCachedStatement(SQL_FROM_HERE,
80      "SELECT" HISTORY_URL_ROW_FIELDS "FROM urls WHERE id=?"));
81  statement.BindInt64(0, url_id);
82
83  if (statement.Step()) {
84    FillURLRow(statement, info);
85    return true;
86  }
87  return false;
88}
89
90bool URLDatabase::GetAllTypedUrls(URLRows* urls) {
91  sql::Statement statement(GetDB().GetCachedStatement(SQL_FROM_HERE,
92      "SELECT" HISTORY_URL_ROW_FIELDS "FROM urls WHERE typed_count > 0"));
93
94  while (statement.Step()) {
95    URLRow info;
96    FillURLRow(statement, &info);
97    urls->push_back(info);
98  }
99  return true;
100}
101
102URLID URLDatabase::GetRowForURL(const GURL& url, history::URLRow* info) {
103  sql::Statement statement(GetDB().GetCachedStatement(SQL_FROM_HERE,
104      "SELECT" HISTORY_URL_ROW_FIELDS "FROM urls WHERE url=?"));
105  std::string url_string = GURLToDatabaseURL(url);
106  statement.BindString(0, url_string);
107
108  if (!statement.Step())
109    return 0;  // no data
110
111  if (info)
112    FillURLRow(statement, info);
113  return statement.ColumnInt64(0);
114}
115
116bool URLDatabase::UpdateURLRow(URLID url_id,
117                               const history::URLRow& info) {
118  sql::Statement statement(GetDB().GetCachedStatement(SQL_FROM_HERE,
119      "UPDATE urls SET title=?,visit_count=?,typed_count=?,last_visit_time=?,"
120        "hidden=?"
121      "WHERE id=?"));
122  statement.BindString16(0, info.title());
123  statement.BindInt(1, info.visit_count());
124  statement.BindInt(2, info.typed_count());
125  statement.BindInt64(3, info.last_visit().ToInternalValue());
126  statement.BindInt(4, info.hidden() ? 1 : 0);
127  statement.BindInt64(5, url_id);
128
129  return statement.Run() && GetDB().GetLastChangeCount() > 0;
130}
131
132URLID URLDatabase::AddURLInternal(const history::URLRow& info,
133                                  bool is_temporary) {
134  // This function is used to insert into two different tables, so we have to
135  // do some shuffling. Unfortinately, we can't use the macro
136  // HISTORY_URL_ROW_FIELDS because that specifies the table name which is
137  // invalid in the insert syntax.
138  #define ADDURL_COMMON_SUFFIX \
139      " (url, title, visit_count, typed_count, "\
140      "last_visit_time, hidden) "\
141      "VALUES (?,?,?,?,?,?)"
142  const char* statement_name;
143  const char* statement_sql;
144  if (is_temporary) {
145    statement_name = "AddURLTemporary";
146    statement_sql = "INSERT INTO temp_urls" ADDURL_COMMON_SUFFIX;
147  } else {
148    statement_name = "AddURL";
149    statement_sql = "INSERT INTO urls" ADDURL_COMMON_SUFFIX;
150  }
151  #undef ADDURL_COMMON_SUFFIX
152
153  sql::Statement statement(GetDB().GetCachedStatement(
154      sql::StatementID(statement_name), statement_sql));
155  statement.BindString(0, GURLToDatabaseURL(info.url()));
156  statement.BindString16(1, info.title());
157  statement.BindInt(2, info.visit_count());
158  statement.BindInt(3, info.typed_count());
159  statement.BindInt64(4, info.last_visit().ToInternalValue());
160  statement.BindInt(5, info.hidden() ? 1 : 0);
161
162  if (!statement.Run()) {
163    VLOG(0) << "Failed to add url " << info.url().possibly_invalid_spec()
164            << " to table history.urls.";
165    return 0;
166  }
167  return GetDB().GetLastInsertRowId();
168}
169
170bool URLDatabase::InsertOrUpdateURLRowByID(const history::URLRow& info) {
171  // SQLite does not support INSERT OR UPDATE, however, it does have INSERT OR
172  // REPLACE, which is feasible to use, because of the following.
173  //  * Before INSERTing, REPLACE will delete all pre-existing rows that cause
174  //    constraint violations. Here, we only have a PRIMARY KEY constraint, so
175  //    the only row that might get deleted is an old one with the same ID.
176  //  * Another difference between the two flavors is that the latter actually
177  //    deletes the old row, and thus the old values are lost in columns which
178  //    are not explicitly assigned new values. This is not an issue, however,
179  //    as we assign values to all columns.
180  //  * When rows are deleted due to constraint violations, the delete triggers
181  //    may not be invoked. As of now, we do not have any delete triggers.
182  // For more details, see: http://www.sqlite.org/lang_conflict.html.
183  sql::Statement statement(GetDB().GetCachedStatement(SQL_FROM_HERE,
184      "INSERT OR REPLACE INTO urls "
185      "(id, url, title, visit_count, typed_count, last_visit_time, hidden) "
186      "VALUES (?, ?, ?, ?, ?, ?, ?)"));
187
188  statement.BindInt64(0, info.id());
189  statement.BindString(1, GURLToDatabaseURL(info.url()));
190  statement.BindString16(2, info.title());
191  statement.BindInt(3, info.visit_count());
192  statement.BindInt(4, info.typed_count());
193  statement.BindInt64(5, info.last_visit().ToInternalValue());
194  statement.BindInt(6, info.hidden() ? 1 : 0);
195
196  return statement.Run();
197}
198
199bool URLDatabase::DeleteURLRow(URLID id) {
200  sql::Statement statement(GetDB().GetCachedStatement(SQL_FROM_HERE,
201      "DELETE FROM urls WHERE id = ?"));
202  statement.BindInt64(0, id);
203
204  if (!statement.Run())
205    return false;
206
207  // And delete any keyword visits.
208  return !has_keyword_search_terms_ || DeleteKeywordSearchTermForURL(id);
209}
210
211bool URLDatabase::CreateTemporaryURLTable() {
212  return CreateURLTable(true);
213}
214
215bool URLDatabase::CommitTemporaryURLTable() {
216  // See the comments in the header file as well as
217  // HistoryBackend::DeleteAllHistory() for more information on how this works
218  // and why it does what it does.
219
220  // Swap the url table out and replace it with the temporary one.
221  if (!GetDB().Execute("DROP TABLE urls")) {
222    NOTREACHED() << GetDB().GetErrorMessage();
223    return false;
224  }
225  if (!GetDB().Execute("ALTER TABLE temp_urls RENAME TO urls")) {
226    NOTREACHED() << GetDB().GetErrorMessage();
227    return false;
228  }
229
230  // Re-create the index over the now permanent URLs table -- this was not there
231  // for the temporary table.
232  CreateMainURLIndex();
233
234  return true;
235}
236
237bool URLDatabase::InitURLEnumeratorForEverything(URLEnumerator* enumerator) {
238  DCHECK(!enumerator->initialized_);
239  std::string sql("SELECT ");
240  sql.append(kURLRowFields);
241  sql.append(" FROM urls");
242  enumerator->statement_.Assign(GetDB().GetUniqueStatement(sql.c_str()));
243  enumerator->initialized_ = enumerator->statement_.is_valid();
244  return enumerator->statement_.is_valid();
245}
246
247bool URLDatabase::InitURLEnumeratorForSignificant(URLEnumerator* enumerator) {
248  DCHECK(!enumerator->initialized_);
249  std::string sql("SELECT ");
250  sql.append(kURLRowFields);
251  sql.append(" FROM urls WHERE last_visit_time >= ? OR visit_count >= ? OR "
252             "typed_count >= ?");
253  enumerator->statement_.Assign(GetDB().GetUniqueStatement(sql.c_str()));
254  enumerator->statement_.BindInt64(
255      0, AutocompleteAgeThreshold().ToInternalValue());
256  enumerator->statement_.BindInt(1, kLowQualityMatchVisitLimit);
257  enumerator->statement_.BindInt(2, kLowQualityMatchTypedLimit);
258  enumerator->initialized_ = enumerator->statement_.is_valid();
259  return enumerator->statement_.is_valid();
260}
261
262bool URLDatabase::AutocompleteForPrefix(const std::string& prefix,
263                                        size_t max_results,
264                                        bool typed_only,
265                                        URLRows* results) {
266  // NOTE: this query originally sorted by starred as the second parameter. But
267  // as bookmarks is no longer part of the db we no longer include the order
268  // by clause.
269  results->clear();
270  const char* sql;
271  int line;
272  if (typed_only) {
273    sql = "SELECT" HISTORY_URL_ROW_FIELDS "FROM urls "
274        "WHERE url >= ? AND url < ? AND hidden = 0 AND typed_count > 0 "
275        "ORDER BY typed_count DESC, visit_count DESC, last_visit_time DESC "
276        "LIMIT ?";
277    line = __LINE__;
278  } else {
279    sql = "SELECT" HISTORY_URL_ROW_FIELDS "FROM urls "
280        "WHERE url >= ? AND url < ? AND hidden = 0 "
281        "ORDER BY typed_count DESC, visit_count DESC, last_visit_time DESC "
282        "LIMIT ?";
283    line = __LINE__;
284  }
285  sql::Statement statement(
286      GetDB().GetCachedStatement(sql::StatementID(__FILE__, line), sql));
287
288  // We will find all strings between "prefix" and this string, which is prefix
289  // followed by the maximum character size. Use 8-bit strings for everything
290  // so we can be sure sqlite is comparing everything in 8-bit mode. Otherwise,
291  // it will have to convert strings either to UTF-8 or UTF-16 for comparison.
292  std::string end_query(prefix);
293  end_query.push_back(std::numeric_limits<unsigned char>::max());
294
295  statement.BindString(0, prefix);
296  statement.BindString(1, end_query);
297  statement.BindInt(2, static_cast<int>(max_results));
298
299  while (statement.Step()) {
300    history::URLRow info;
301    FillURLRow(statement, &info);
302    if (info.url().is_valid())
303      results->push_back(info);
304  }
305  return !results->empty();
306}
307
308bool URLDatabase::IsTypedHost(const std::string& host) {
309  const char* schemes[] = {
310    url::kHttpScheme,
311    url::kHttpsScheme,
312    url::kFtpScheme
313  };
314  URLRows dummy;
315  for (size_t i = 0; i < arraysize(schemes); ++i) {
316    std::string scheme_and_host(schemes[i]);
317    scheme_and_host += url::kStandardSchemeSeparator + host;
318    if (AutocompleteForPrefix(scheme_and_host + '/', 1, true, &dummy) ||
319        AutocompleteForPrefix(scheme_and_host + ':', 1, true, &dummy))
320      return true;
321  }
322  return false;
323}
324
325bool URLDatabase::FindShortestURLFromBase(const std::string& base,
326                                          const std::string& url,
327                                          int min_visits,
328                                          int min_typed,
329                                          bool allow_base,
330                                          history::URLRow* info) {
331  // Select URLs that start with |base| and are prefixes of |url|.  All parts
332  // of this query except the substr() call can be done using the index.  We
333  // could do this query with a couple of LIKE or GLOB statements as well, but
334  // those wouldn't use the index, and would run into problems with "wildcard"
335  // characters that appear in URLs (% for LIKE, or *, ? for GLOB).
336  std::string sql("SELECT ");
337  sql.append(kURLRowFields);
338  sql.append(" FROM urls WHERE url ");
339  sql.append(allow_base ? ">=" : ">");
340  sql.append(" ? AND url < :end AND url = substr(:end, 1, length(url)) "
341             "AND hidden = 0 AND visit_count >= ? AND typed_count >= ? "
342             "ORDER BY url LIMIT 1");
343  sql::Statement statement(GetDB().GetUniqueStatement(sql.c_str()));
344  statement.BindString(0, base);
345  statement.BindString(1, url);   // :end
346  statement.BindInt(2, min_visits);
347  statement.BindInt(3, min_typed);
348
349  if (!statement.Step())
350    return false;
351
352  DCHECK(info);
353  FillURLRow(statement, info);
354  return true;
355}
356
357bool URLDatabase::GetTextMatches(const base::string16& query,
358                                 URLRows* results) {
359  ScopedVector<query_parser::QueryNode> query_nodes;
360  query_parser_.ParseQueryNodes(query, &query_nodes.get());
361
362  results->clear();
363  sql::Statement statement(GetDB().GetCachedStatement(SQL_FROM_HERE,
364      "SELECT" HISTORY_URL_ROW_FIELDS "FROM urls WHERE hidden = 0"));
365
366  while (statement.Step()) {
367    query_parser::QueryWordVector query_words;
368    base::string16 url = base::i18n::ToLower(statement.ColumnString16(1));
369    query_parser_.ExtractQueryWords(url, &query_words);
370    GURL gurl(url);
371    if (gurl.is_valid()) {
372      // Decode punycode to match IDN.
373      // |query_words| won't be shown to user - therefore we can use empty
374      // |languages| to reduce dependency (no need to call PrefService).
375      base::string16 ascii = base::ASCIIToUTF16(gurl.host());
376      base::string16 utf = net::IDNToUnicode(gurl.host(), std::string());
377      if (ascii != utf)
378        query_parser_.ExtractQueryWords(utf, &query_words);
379    }
380    base::string16 title = base::i18n::ToLower(statement.ColumnString16(2));
381    query_parser_.ExtractQueryWords(title, &query_words);
382
383    if (query_parser_.DoesQueryMatch(query_words, query_nodes.get())) {
384      history::URLResult info;
385      FillURLRow(statement, &info);
386      if (info.url().is_valid())
387        results->push_back(info);
388    }
389  }
390  return !results->empty();
391}
392
393bool URLDatabase::InitKeywordSearchTermsTable() {
394  has_keyword_search_terms_ = true;
395  if (!GetDB().DoesTableExist("keyword_search_terms")) {
396    if (!GetDB().Execute("CREATE TABLE keyword_search_terms ("
397        "keyword_id INTEGER NOT NULL,"      // ID of the TemplateURL.
398        "url_id INTEGER NOT NULL,"          // ID of the url.
399        "lower_term LONGVARCHAR NOT NULL,"  // The search term, in lower case.
400        "term LONGVARCHAR NOT NULL)"))      // The actual search term.
401      return false;
402  }
403  return true;
404}
405
406bool URLDatabase::CreateKeywordSearchTermsIndices() {
407  // For searching.
408  if (!GetDB().Execute(
409          "CREATE INDEX IF NOT EXISTS keyword_search_terms_index1 ON "
410          "keyword_search_terms (keyword_id, lower_term)")) {
411    return false;
412  }
413
414  // For deletion.
415  if (!GetDB().Execute(
416          "CREATE INDEX IF NOT EXISTS keyword_search_terms_index2 ON "
417          "keyword_search_terms (url_id)")) {
418    return false;
419  }
420
421  // For query or deletion by term.
422  if (!GetDB().Execute(
423          "CREATE INDEX IF NOT EXISTS keyword_search_terms_index3 ON "
424          "keyword_search_terms (term)")) {
425    return false;
426  }
427  return true;
428}
429
430bool URLDatabase::DropKeywordSearchTermsTable() {
431  // This will implicitly delete the indices over the table.
432  return GetDB().Execute("DROP TABLE keyword_search_terms");
433}
434
435bool URLDatabase::SetKeywordSearchTermsForURL(URLID url_id,
436                                              KeywordID keyword_id,
437                                              const base::string16& term) {
438  DCHECK(url_id && keyword_id && !term.empty());
439
440  sql::Statement exist_statement(GetDB().GetCachedStatement(SQL_FROM_HERE,
441      "SELECT term FROM keyword_search_terms "
442      "WHERE keyword_id = ? AND url_id = ?"));
443  exist_statement.BindInt64(0, keyword_id);
444  exist_statement.BindInt64(1, url_id);
445
446  if (exist_statement.Step())
447    return true;  // Term already exists, no need to add it.
448
449  if (!exist_statement.Succeeded())
450    return false;
451
452  sql::Statement statement(GetDB().GetCachedStatement(SQL_FROM_HERE,
453      "INSERT INTO keyword_search_terms (keyword_id, url_id, lower_term, term) "
454      "VALUES (?,?,?,?)"));
455  statement.BindInt64(0, keyword_id);
456  statement.BindInt64(1, url_id);
457  statement.BindString16(2, base::i18n::ToLower(term));
458  statement.BindString16(3, term);
459  return statement.Run();
460}
461
462bool URLDatabase::GetKeywordSearchTermRow(URLID url_id,
463                                          KeywordSearchTermRow* row) {
464  DCHECK(url_id);
465  sql::Statement statement(GetDB().GetCachedStatement(SQL_FROM_HERE,
466      "SELECT keyword_id, term FROM keyword_search_terms WHERE url_id=?"));
467  statement.BindInt64(0, url_id);
468
469  if (!statement.Step())
470    return false;
471
472  if (row) {
473    row->url_id = url_id;
474    row->keyword_id = statement.ColumnInt64(0);
475    row->term = statement.ColumnString16(1);
476  }
477  return true;
478}
479
480bool URLDatabase::GetKeywordSearchTermRows(
481    const base::string16& term,
482    std::vector<KeywordSearchTermRow>* rows) {
483  sql::Statement statement(GetDB().GetCachedStatement(SQL_FROM_HERE,
484      "SELECT keyword_id, url_id FROM keyword_search_terms WHERE term=?"));
485  statement.BindString16(0, term);
486
487  if (!statement.is_valid())
488    return false;
489
490  while (statement.Step()) {
491    KeywordSearchTermRow row;
492    row.url_id = statement.ColumnInt64(1);
493    row.keyword_id = statement.ColumnInt64(0);
494    row.term = term;
495    rows->push_back(row);
496  }
497  return true;
498}
499
500void URLDatabase::DeleteAllSearchTermsForKeyword(
501    KeywordID keyword_id) {
502  DCHECK(keyword_id);
503  sql::Statement statement(GetDB().GetCachedStatement(SQL_FROM_HERE,
504      "DELETE FROM keyword_search_terms WHERE keyword_id=?"));
505  statement.BindInt64(0, keyword_id);
506
507  statement.Run();
508}
509
510void URLDatabase::GetMostRecentKeywordSearchTerms(
511    KeywordID keyword_id,
512    const base::string16& prefix,
513    int max_count,
514    std::vector<KeywordSearchTermVisit>* matches) {
515  // NOTE: the keyword_id can be zero if on first run the user does a query
516  // before the TemplateURLService has finished loading. As the chances of this
517  // occurring are small, we ignore it.
518  if (!keyword_id)
519    return;
520
521  DCHECK(!prefix.empty());
522  sql::Statement statement(GetDB().GetCachedStatement(SQL_FROM_HERE,
523      "SELECT DISTINCT kv.term, u.visit_count, u.last_visit_time "
524      "FROM keyword_search_terms kv "
525      "JOIN urls u ON kv.url_id = u.id "
526      "WHERE kv.keyword_id = ? AND kv.lower_term >= ? AND kv.lower_term < ? "
527      "ORDER BY u.last_visit_time DESC LIMIT ?"));
528
529  // NOTE: Keep this ToLower() call in sync with search_provider.cc.
530  base::string16 lower_prefix = base::i18n::ToLower(prefix);
531  // This magic gives us a prefix search.
532  base::string16 next_prefix = lower_prefix;
533  next_prefix[next_prefix.size() - 1] =
534      next_prefix[next_prefix.size() - 1] + 1;
535  statement.BindInt64(0, keyword_id);
536  statement.BindString16(1, lower_prefix);
537  statement.BindString16(2, next_prefix);
538  statement.BindInt(3, max_count);
539
540  KeywordSearchTermVisit visit;
541  while (statement.Step()) {
542    visit.term = statement.ColumnString16(0);
543    visit.visits = statement.ColumnInt(1);
544    visit.time = base::Time::FromInternalValue(statement.ColumnInt64(2));
545    matches->push_back(visit);
546  }
547}
548
549bool URLDatabase::DeleteKeywordSearchTerm(const base::string16& term) {
550  sql::Statement statement(GetDB().GetCachedStatement(SQL_FROM_HERE,
551      "DELETE FROM keyword_search_terms WHERE term=?"));
552  statement.BindString16(0, term);
553
554  return statement.Run();
555}
556
557bool URLDatabase::DeleteKeywordSearchTermForURL(URLID url_id) {
558  sql::Statement statement(GetDB().GetCachedStatement(
559      SQL_FROM_HERE, "DELETE FROM keyword_search_terms WHERE url_id=?"));
560  statement.BindInt64(0, url_id);
561  return statement.Run();
562}
563
564bool URLDatabase::DropStarredIDFromURLs() {
565  if (!GetDB().DoesColumnExist("urls", "starred_id"))
566    return true;  // urls is already updated, no need to continue.
567
568  // Create a temporary table to contain the new URLs table.
569  if (!CreateTemporaryURLTable()) {
570    NOTREACHED();
571    return false;
572  }
573
574  // Copy the contents.
575  if (!GetDB().Execute(
576      "INSERT INTO temp_urls (id, url, title, visit_count, typed_count, "
577      "last_visit_time, hidden, favicon_id) "
578      "SELECT id, url, title, visit_count, typed_count, last_visit_time, "
579      "hidden, favicon_id FROM urls")) {
580    NOTREACHED() << GetDB().GetErrorMessage();
581    return false;
582  }
583
584  // Rename/commit the tmp table.
585  CommitTemporaryURLTable();
586
587  return true;
588}
589
590bool URLDatabase::CreateURLTable(bool is_temporary) {
591  const char* name = is_temporary ? "temp_urls" : "urls";
592  if (GetDB().DoesTableExist(name))
593    return true;
594
595  // Note: revise implementation for InsertOrUpdateURLRowByID() if you add any
596  // new constraints to the schema.
597  std::string sql;
598  sql.append("CREATE TABLE ");
599  sql.append(name);
600  sql.append("("
601      "id INTEGER PRIMARY KEY,"
602      "url LONGVARCHAR,"
603      "title LONGVARCHAR,"
604      "visit_count INTEGER DEFAULT 0 NOT NULL,"
605      "typed_count INTEGER DEFAULT 0 NOT NULL,"
606      "last_visit_time INTEGER NOT NULL,"
607      "hidden INTEGER DEFAULT 0 NOT NULL,"
608      "favicon_id INTEGER DEFAULT 0 NOT NULL)"); // favicon_id is not used now.
609
610  return GetDB().Execute(sql.c_str());
611}
612
613bool URLDatabase::CreateMainURLIndex() {
614  return GetDB().Execute(
615      "CREATE INDEX IF NOT EXISTS urls_url_index ON urls (url)");
616}
617
618const int kLowQualityMatchTypedLimit = 1;
619const int kLowQualityMatchVisitLimit = 4;
620const int kLowQualityMatchAgeLimitInDays = 3;
621
622base::Time AutocompleteAgeThreshold() {
623  return (base::Time::Now() -
624          base::TimeDelta::FromDays(kLowQualityMatchAgeLimitInDays));
625}
626
627bool RowQualifiesAsSignificant(const URLRow& row,
628                               const base::Time& threshold) {
629  const base::Time& real_threshold =
630      threshold.is_null() ? AutocompleteAgeThreshold() : threshold;
631  return (row.typed_count() >= kLowQualityMatchTypedLimit) ||
632         (row.visit_count() >= kLowQualityMatchVisitLimit) ||
633         (row.last_visit() >= real_threshold);
634}
635
636}  // namespace history
637