visit_database.cc revision 2a99a7e74a7f215066514fe81d2bfa6639d9eddd
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/visit_database.h"
6
7#include <algorithm>
8#include <limits>
9#include <map>
10#include <set>
11
12#include "base/logging.h"
13#include "base/strings/string_number_conversions.h"
14#include "chrome/browser/history/url_database.h"
15#include "chrome/browser/history/visit_filter.h"
16#include "chrome/common/url_constants.h"
17#include "content/public/common/page_transition_types.h"
18#include "sql/statement.h"
19
20namespace history {
21
22VisitDatabase::VisitDatabase() {
23}
24
25VisitDatabase::~VisitDatabase() {
26}
27
28bool VisitDatabase::InitVisitTable() {
29  if (!GetDB().DoesTableExist("visits")) {
30    if (!GetDB().Execute("CREATE TABLE visits("
31        "id INTEGER PRIMARY KEY,"
32        "url INTEGER NOT NULL," // key of the URL this corresponds to
33        "visit_time INTEGER NOT NULL,"
34        "from_visit INTEGER,"
35        "transition INTEGER DEFAULT 0 NOT NULL,"
36        "segment_id INTEGER,"
37        // True when we have indexed data for this visit.
38        "is_indexed BOOLEAN,"
39        "visit_duration INTEGER DEFAULT 0 NOT NULL)"))
40      return false;
41  } else if (!GetDB().DoesColumnExist("visits", "is_indexed")) {
42    // Old versions don't have the is_indexed column, we can just add that and
43    // not worry about different database revisions, since old ones will
44    // continue to work.
45    //
46    // TODO(brettw) this should be removed once we think everybody has been
47    // updated (added early Mar 2008).
48    if (!GetDB().Execute("ALTER TABLE visits ADD COLUMN is_indexed BOOLEAN"))
49      return false;
50  }
51
52  // Visit source table contains the source information for all the visits. To
53  // save space, we do not record those user browsed visits which would be the
54  // majority in this table. Only other sources are recorded.
55  // Due to the tight relationship between visit_source and visits table, they
56  // should be created and dropped at the same time.
57  if (!GetDB().DoesTableExist("visit_source")) {
58    if (!GetDB().Execute("CREATE TABLE visit_source("
59                         "id INTEGER PRIMARY KEY,source INTEGER NOT NULL)"))
60        return false;
61  }
62
63  // Index over url so we can quickly find visits for a page.
64  if (!GetDB().Execute(
65          "CREATE INDEX IF NOT EXISTS visits_url_index ON visits (url)"))
66    return false;
67
68  // Create an index over from visits so that we can efficiently find
69  // referrers and redirects.
70  if (!GetDB().Execute(
71          "CREATE INDEX IF NOT EXISTS visits_from_index ON "
72          "visits (from_visit)"))
73    return false;
74
75  // Create an index over time so that we can efficiently find the visits in a
76  // given time range (most history views are time-based).
77  if (!GetDB().Execute(
78          "CREATE INDEX IF NOT EXISTS visits_time_index ON "
79          "visits (visit_time)"))
80    return false;
81
82  return true;
83}
84
85bool VisitDatabase::DropVisitTable() {
86  // This will also drop the indices over the table.
87  return
88      GetDB().Execute("DROP TABLE IF EXISTS visit_source") &&
89      GetDB().Execute("DROP TABLE visits");
90}
91
92// Must be in sync with HISTORY_VISIT_ROW_FIELDS.
93// static
94void VisitDatabase::FillVisitRow(sql::Statement& statement, VisitRow* visit) {
95  visit->visit_id = statement.ColumnInt64(0);
96  visit->url_id = statement.ColumnInt64(1);
97  visit->visit_time = base::Time::FromInternalValue(statement.ColumnInt64(2));
98  visit->referring_visit = statement.ColumnInt64(3);
99  visit->transition = content::PageTransitionFromInt(statement.ColumnInt(4));
100  visit->segment_id = statement.ColumnInt64(5);
101  visit->is_indexed = !!statement.ColumnInt(6);
102  visit->visit_duration =
103      base::TimeDelta::FromInternalValue(statement.ColumnInt64(7));
104}
105
106// static
107bool VisitDatabase::FillVisitVector(sql::Statement& statement,
108                                    VisitVector* visits) {
109  if (!statement.is_valid())
110    return false;
111
112  while (statement.Step()) {
113    history::VisitRow visit;
114    FillVisitRow(statement, &visit);
115    visits->push_back(visit);
116  }
117
118  return statement.Succeeded();
119}
120
121VisitID VisitDatabase::AddVisit(VisitRow* visit, VisitSource source) {
122  sql::Statement statement(GetDB().GetCachedStatement(SQL_FROM_HERE,
123      "INSERT INTO visits "
124      "(url, visit_time, from_visit, transition, segment_id, is_indexed, "
125      "visit_duration) VALUES (?,?,?,?,?,?,?)"));
126  statement.BindInt64(0, visit->url_id);
127  statement.BindInt64(1, visit->visit_time.ToInternalValue());
128  statement.BindInt64(2, visit->referring_visit);
129  statement.BindInt64(3, visit->transition);
130  statement.BindInt64(4, visit->segment_id);
131  statement.BindInt64(5, visit->is_indexed);
132  statement.BindInt64(6, visit->visit_duration.ToInternalValue());
133
134  if (!statement.Run()) {
135    VLOG(0) << "Failed to execute visit insert statement:  "
136            << "url_id = " << visit->url_id;
137    return 0;
138  }
139
140  visit->visit_id = GetDB().GetLastInsertRowId();
141
142  if (source != SOURCE_BROWSED) {
143    // Record the source of this visit when it is not browsed.
144    sql::Statement statement1(GetDB().GetCachedStatement(SQL_FROM_HERE,
145        "INSERT INTO visit_source (id, source) VALUES (?,?)"));
146    statement1.BindInt64(0, visit->visit_id);
147    statement1.BindInt64(1, source);
148
149    if (!statement1.Run()) {
150      VLOG(0) << "Failed to execute visit_source insert statement:  "
151              << "id = " << visit->visit_id;
152      return 0;
153    }
154  }
155
156  return visit->visit_id;
157}
158
159void VisitDatabase::DeleteVisit(const VisitRow& visit) {
160  // Patch around this visit. Any visits that this went to will now have their
161  // "source" be the deleted visit's source.
162  sql::Statement update_chain(GetDB().GetCachedStatement(SQL_FROM_HERE,
163      "UPDATE visits SET from_visit=? WHERE from_visit=?"));
164  update_chain.BindInt64(0, visit.referring_visit);
165  update_chain.BindInt64(1, visit.visit_id);
166  if (!update_chain.Run())
167    return;
168
169  // Now delete the actual visit.
170  sql::Statement del(GetDB().GetCachedStatement(SQL_FROM_HERE,
171      "DELETE FROM visits WHERE id=?"));
172  del.BindInt64(0, visit.visit_id);
173  if (!del.Run())
174    return;
175
176  // Try to delete the entry in visit_source table as well.
177  // If the visit was browsed, there is no corresponding entry in visit_source
178  // table, and nothing will be deleted.
179  del.Assign(GetDB().GetCachedStatement(SQL_FROM_HERE,
180             "DELETE FROM visit_source WHERE id=?"));
181  del.BindInt64(0, visit.visit_id);
182  del.Run();
183}
184
185bool VisitDatabase::GetRowForVisit(VisitID visit_id, VisitRow* out_visit) {
186  sql::Statement statement(GetDB().GetCachedStatement(SQL_FROM_HERE,
187      "SELECT" HISTORY_VISIT_ROW_FIELDS "FROM visits WHERE id=?"));
188  statement.BindInt64(0, visit_id);
189
190  if (!statement.Step())
191    return false;
192
193  FillVisitRow(statement, out_visit);
194
195  // We got a different visit than we asked for, something is wrong.
196  DCHECK_EQ(visit_id, out_visit->visit_id);
197  if (visit_id != out_visit->visit_id)
198    return false;
199
200  return true;
201}
202
203bool VisitDatabase::UpdateVisitRow(const VisitRow& visit) {
204  // Don't store inconsistent data to the database.
205  DCHECK_NE(visit.visit_id, visit.referring_visit);
206  if (visit.visit_id == visit.referring_visit)
207    return false;
208
209  sql::Statement statement(GetDB().GetCachedStatement(SQL_FROM_HERE,
210      "UPDATE visits SET "
211      "url=?,visit_time=?,from_visit=?,transition=?,segment_id=?,is_indexed=?,"
212      "visit_duration=? WHERE id=?"));
213  statement.BindInt64(0, visit.url_id);
214  statement.BindInt64(1, visit.visit_time.ToInternalValue());
215  statement.BindInt64(2, visit.referring_visit);
216  statement.BindInt64(3, visit.transition);
217  statement.BindInt64(4, visit.segment_id);
218  statement.BindInt64(5, visit.is_indexed);
219  statement.BindInt64(6, visit.visit_duration.ToInternalValue());
220  statement.BindInt64(7, visit.visit_id);
221
222  return statement.Run();
223}
224
225bool VisitDatabase::GetVisitsForURL(URLID url_id, VisitVector* visits) {
226  visits->clear();
227
228  sql::Statement statement(GetDB().GetCachedStatement(SQL_FROM_HERE,
229      "SELECT" HISTORY_VISIT_ROW_FIELDS
230      "FROM visits "
231      "WHERE url=? "
232      "ORDER BY visit_time ASC"));
233  statement.BindInt64(0, url_id);
234  return FillVisitVector(statement, visits);
235}
236
237bool VisitDatabase::GetIndexedVisitsForURL(URLID url_id, VisitVector* visits) {
238  visits->clear();
239
240  sql::Statement statement(GetDB().GetCachedStatement(SQL_FROM_HERE,
241      "SELECT" HISTORY_VISIT_ROW_FIELDS
242      "FROM visits "
243      "WHERE url=? AND is_indexed=1"));
244  statement.BindInt64(0, url_id);
245  return FillVisitVector(statement, visits);
246}
247
248
249bool VisitDatabase::GetVisitsForTimes(const std::vector<base::Time>& times,
250                                      VisitVector* visits) {
251  visits->clear();
252
253  for (std::vector<base::Time>::const_iterator it = times.begin();
254       it != times.end(); ++it) {
255    sql::Statement statement(GetDB().GetCachedStatement(SQL_FROM_HERE,
256        "SELECT" HISTORY_VISIT_ROW_FIELDS "FROM visits "
257        "WHERE visit_time == ?"));
258
259    statement.BindInt64(0, it->ToInternalValue());
260
261    if (!FillVisitVector(statement, visits))
262      return false;
263  }
264  return true;
265}
266
267bool VisitDatabase::GetAllVisitsInRange(base::Time begin_time,
268                                        base::Time end_time,
269                                        int max_results,
270                                        VisitVector* visits) {
271  visits->clear();
272
273  sql::Statement statement(GetDB().GetCachedStatement(SQL_FROM_HERE,
274      "SELECT" HISTORY_VISIT_ROW_FIELDS "FROM visits "
275      "WHERE visit_time >= ? AND visit_time < ?"
276      "ORDER BY visit_time LIMIT ?"));
277
278  // See GetVisibleVisitsInRange for more info on how these times are bound.
279  int64 end = end_time.ToInternalValue();
280  statement.BindInt64(0, begin_time.ToInternalValue());
281  statement.BindInt64(1, end ? end : std::numeric_limits<int64>::max());
282  statement.BindInt64(2,
283      max_results ? max_results : std::numeric_limits<int64>::max());
284
285  return FillVisitVector(statement, visits);
286}
287
288bool VisitDatabase::GetVisitsInRangeForTransition(
289    base::Time begin_time,
290    base::Time end_time,
291    int max_results,
292    content::PageTransition transition,
293    VisitVector* visits) {
294  DCHECK(visits);
295  visits->clear();
296
297  sql::Statement statement(GetDB().GetCachedStatement(SQL_FROM_HERE,
298      "SELECT" HISTORY_VISIT_ROW_FIELDS "FROM visits "
299      "WHERE visit_time >= ? AND visit_time < ? "
300      "AND (transition & ?) == ?"
301      "ORDER BY visit_time LIMIT ?"));
302
303  // See GetVisibleVisitsInRange for more info on how these times are bound.
304  int64 end = end_time.ToInternalValue();
305  statement.BindInt64(0, begin_time.ToInternalValue());
306  statement.BindInt64(1, end ? end : std::numeric_limits<int64>::max());
307  statement.BindInt(2, content::PAGE_TRANSITION_CORE_MASK);
308  statement.BindInt(3, transition);
309  statement.BindInt64(4,
310      max_results ? max_results : std::numeric_limits<int64>::max());
311
312  return FillVisitVector(statement, visits);
313}
314
315bool VisitDatabase::GetVisibleVisitsInRange(const QueryOptions& options,
316                                            VisitVector* visits) {
317  visits->clear();
318  // The visit_time values can be duplicated in a redirect chain, so we sort
319  // by id too, to ensure a consistent ordering just in case.
320  sql::Statement statement(GetDB().GetCachedStatement(SQL_FROM_HERE,
321      "SELECT" HISTORY_VISIT_ROW_FIELDS "FROM visits "
322      "WHERE visit_time >= ? AND visit_time < ? "
323      "AND (transition & ?) != 0 "  // CHAIN_END
324      "AND (transition & ?) NOT IN (?, ?, ?) "  // NO SUBFRAME or
325                                                // KEYWORD_GENERATED
326      "ORDER BY visit_time DESC, id DESC"));
327
328  statement.BindInt64(0, options.EffectiveBeginTime());
329  statement.BindInt64(1, options.EffectiveEndTime());
330  statement.BindInt(2, content::PAGE_TRANSITION_CHAIN_END);
331  statement.BindInt(3, content::PAGE_TRANSITION_CORE_MASK);
332  statement.BindInt(4, content::PAGE_TRANSITION_AUTO_SUBFRAME);
333  statement.BindInt(5, content::PAGE_TRANSITION_MANUAL_SUBFRAME);
334  statement.BindInt(6, content::PAGE_TRANSITION_KEYWORD_GENERATED);
335
336  std::set<URLID> found_urls;
337
338  // Keeps track of the day that |found_urls| is holding the URLs for, in order
339  // to handle removing per-day duplicates.
340  base::Time found_urls_midnight;
341
342  while (statement.Step()) {
343    VisitRow visit;
344    FillVisitRow(statement, &visit);
345
346    if (options.duplicate_policy != QueryOptions::KEEP_ALL_DUPLICATES) {
347      if (options.duplicate_policy == QueryOptions::REMOVE_DUPLICATES_PER_DAY &&
348          found_urls_midnight != visit.visit_time.LocalMidnight()) {
349        found_urls.clear();
350        found_urls_midnight = visit.visit_time.LocalMidnight();
351      }
352      // Make sure the URL this visit corresponds to is unique.
353      if (found_urls.find(visit.url_id) != found_urls.end())
354        continue;
355      found_urls.insert(visit.url_id);
356    }
357
358    if (static_cast<int>(visits->size()) >= options.EffectiveMaxCount())
359      return true;
360    visits->push_back(visit);
361  }
362  return false;
363}
364
365void VisitDatabase::GetDirectVisitsDuringTimes(const VisitFilter& time_filter,
366                                                int max_results,
367                                                VisitVector* visits) {
368  visits->clear();
369  if (max_results)
370    visits->reserve(max_results);
371  for (VisitFilter::TimeVector::const_iterator it = time_filter.times().begin();
372       it != time_filter.times().end(); ++it) {
373    sql::Statement statement(GetDB().GetCachedStatement(SQL_FROM_HERE,
374        "SELECT" HISTORY_VISIT_ROW_FIELDS "FROM visits "
375        "WHERE visit_time >= ? AND visit_time < ? "
376        "AND (transition & ?) != 0 "  // CHAIN_START
377        "AND (transition & ?) IN (?, ?) "  // TYPED or AUTO_BOOKMARK only
378        "ORDER BY visit_time DESC, id DESC"));
379
380    statement.BindInt64(0, it->first.ToInternalValue());
381    statement.BindInt64(1, it->second.ToInternalValue());
382    statement.BindInt(2, content::PAGE_TRANSITION_CHAIN_START);
383    statement.BindInt(3, content::PAGE_TRANSITION_CORE_MASK);
384    statement.BindInt(4, content::PAGE_TRANSITION_TYPED);
385    statement.BindInt(5, content::PAGE_TRANSITION_AUTO_BOOKMARK);
386
387    while (statement.Step()) {
388      VisitRow visit;
389      FillVisitRow(statement, &visit);
390      visits->push_back(visit);
391
392      if (max_results > 0 && static_cast<int>(visits->size()) >= max_results)
393        return;
394    }
395  }
396}
397
398VisitID VisitDatabase::GetMostRecentVisitForURL(URLID url_id,
399                                                VisitRow* visit_row) {
400  // The visit_time values can be duplicated in a redirect chain, so we sort
401  // by id too, to ensure a consistent ordering just in case.
402  sql::Statement statement(GetDB().GetCachedStatement(SQL_FROM_HERE,
403      "SELECT" HISTORY_VISIT_ROW_FIELDS "FROM visits "
404      "WHERE url=? "
405      "ORDER BY visit_time DESC, id DESC "
406      "LIMIT 1"));
407  statement.BindInt64(0, url_id);
408  if (!statement.Step())
409    return 0;  // No visits for this URL.
410
411  if (visit_row) {
412    FillVisitRow(statement, visit_row);
413    return visit_row->visit_id;
414  }
415  return statement.ColumnInt64(0);
416}
417
418bool VisitDatabase::GetMostRecentVisitsForURL(URLID url_id,
419                                              int max_results,
420                                              VisitVector* visits) {
421  visits->clear();
422
423  // The visit_time values can be duplicated in a redirect chain, so we sort
424  // by id too, to ensure a consistent ordering just in case.
425  sql::Statement statement(GetDB().GetCachedStatement(SQL_FROM_HERE,
426      "SELECT" HISTORY_VISIT_ROW_FIELDS
427      "FROM visits "
428      "WHERE url=? "
429      "ORDER BY visit_time DESC, id DESC "
430      "LIMIT ?"));
431  statement.BindInt64(0, url_id);
432  statement.BindInt(1, max_results);
433
434  return FillVisitVector(statement, visits);
435}
436
437bool VisitDatabase::GetRedirectFromVisit(VisitID from_visit,
438                                         VisitID* to_visit,
439                                         GURL* to_url) {
440  sql::Statement statement(GetDB().GetCachedStatement(SQL_FROM_HERE,
441      "SELECT v.id,u.url "
442      "FROM visits v JOIN urls u ON v.url = u.id "
443      "WHERE v.from_visit = ? "
444      "AND (v.transition & ?) != 0"));  // IS_REDIRECT_MASK
445  statement.BindInt64(0, from_visit);
446  statement.BindInt(1, content::PAGE_TRANSITION_IS_REDIRECT_MASK);
447
448  if (!statement.Step())
449    return false;  // No redirect from this visit. (Or SQL error)
450  if (to_visit)
451    *to_visit = statement.ColumnInt64(0);
452  if (to_url)
453    *to_url = GURL(statement.ColumnString(1));
454  return true;
455}
456
457bool VisitDatabase::GetRedirectToVisit(VisitID to_visit,
458                                       VisitID* from_visit,
459                                       GURL* from_url) {
460  VisitRow row;
461  if (!GetRowForVisit(to_visit, &row))
462    return false;
463
464  if (from_visit)
465    *from_visit = row.referring_visit;
466
467  if (from_url) {
468    sql::Statement statement(GetDB().GetCachedStatement(SQL_FROM_HERE,
469        "SELECT u.url "
470        "FROM visits v JOIN urls u ON v.url = u.id "
471        "WHERE v.id = ?"));
472    statement.BindInt64(0, row.referring_visit);
473
474    if (!statement.Step())
475      return false;
476
477    *from_url = GURL(statement.ColumnString(0));
478  }
479  return true;
480}
481
482bool VisitDatabase::GetVisibleVisitCountToHost(const GURL& url,
483                                               int* count,
484                                               base::Time* first_visit) {
485  if (!url.SchemeIs(chrome::kHttpScheme) && !url.SchemeIs(chrome::kHttpsScheme))
486    return false;
487
488  // We need to search for URLs with a matching host/port. One way to query for
489  // this is to use the LIKE operator, eg 'url LIKE http://google.com/%'. This
490  // is inefficient though in that it doesn't use the index and each entry must
491  // be visited. The same query can be executed by using >= and < operator.
492  // The query becomes:
493  // 'url >= http://google.com/' and url < http://google.com0'.
494  // 0 is used as it is one character greater than '/'.
495  const std::string host_query_min = url.GetOrigin().spec();
496  if (host_query_min.empty())
497    return false;
498
499  // We also want to restrict ourselves to main frame navigations that are not
500  // in the middle of redirect chains, hence the transition checks.
501  sql::Statement statement(GetDB().GetCachedStatement(SQL_FROM_HERE,
502      "SELECT MIN(v.visit_time), COUNT(*) "
503      "FROM visits v INNER JOIN urls u ON v.url = u.id "
504      "WHERE u.url >= ? AND u.url < ? "
505      "AND (transition & ?) != 0 "
506      "AND (transition & ?) NOT IN (?, ?, ?)"));
507  statement.BindString(0, host_query_min);
508  statement.BindString(1,
509      host_query_min.substr(0, host_query_min.size() - 1) + '0');
510  statement.BindInt(2, content::PAGE_TRANSITION_CHAIN_END);
511  statement.BindInt(3, content::PAGE_TRANSITION_CORE_MASK);
512  statement.BindInt(4, content::PAGE_TRANSITION_AUTO_SUBFRAME);
513  statement.BindInt(5, content::PAGE_TRANSITION_MANUAL_SUBFRAME);
514  statement.BindInt(6, content::PAGE_TRANSITION_KEYWORD_GENERATED);
515
516  if (!statement.Step()) {
517    // We've never been to this page before.
518    *count = 0;
519    return true;
520  }
521
522  if (!statement.Succeeded())
523    return false;
524
525  *first_visit = base::Time::FromInternalValue(statement.ColumnInt64(0));
526  *count = statement.ColumnInt(1);
527  return true;
528}
529
530bool VisitDatabase::GetStartDate(base::Time* first_visit) {
531  sql::Statement statement(GetDB().GetCachedStatement(SQL_FROM_HERE,
532      "SELECT MIN(visit_time) FROM visits WHERE visit_time != 0"));
533  if (!statement.Step() || statement.ColumnInt64(0) == 0) {
534    *first_visit = base::Time::Now();
535    return false;
536  }
537  *first_visit = base::Time::FromInternalValue(statement.ColumnInt64(0));
538  return true;
539}
540
541void VisitDatabase::GetVisitsSource(const VisitVector& visits,
542                                    VisitSourceMap* sources) {
543  DCHECK(sources);
544  sources->clear();
545
546  // We query the source in batch. Here defines the batch size.
547  const size_t batch_size = 500;
548  size_t visits_size = visits.size();
549
550  size_t start_index = 0, end_index = 0;
551  while (end_index < visits_size) {
552    start_index = end_index;
553    end_index = end_index + batch_size < visits_size ? end_index + batch_size
554                                                     : visits_size;
555
556    // Compose the sql statement with a list of ids.
557    std::string sql = "SELECT id,source FROM visit_source ";
558    sql.append("WHERE id IN (");
559    // Append all the ids in the statement.
560    for (size_t j = start_index; j < end_index; j++) {
561      if (j != start_index)
562        sql.push_back(',');
563      sql.append(base::Int64ToString(visits[j].visit_id));
564    }
565    sql.append(") ORDER BY id");
566    sql::Statement statement(GetDB().GetUniqueStatement(sql.c_str()));
567
568    // Get the source entries out of the query result.
569    while (statement.Step()) {
570      std::pair<VisitID, VisitSource> source_entry(statement.ColumnInt64(0),
571          static_cast<VisitSource>(statement.ColumnInt(1)));
572      sources->insert(source_entry);
573    }
574  }
575}
576
577bool VisitDatabase::MigrateVisitsWithoutDuration() {
578  if (!GetDB().DoesTableExist("visits")) {
579    NOTREACHED() << " Visits table should exist before migration";
580    return false;
581  }
582
583  if (!GetDB().DoesColumnExist("visits", "visit_duration")) {
584    // Old versions don't have the visit_duration column, we modify the table
585    // to add that field.
586    if (!GetDB().Execute("ALTER TABLE visits "
587        "ADD COLUMN visit_duration INTEGER DEFAULT 0 NOT NULL"))
588      return false;
589  }
590  return true;
591}
592
593void VisitDatabase::GetBriefVisitInfoOfMostRecentVisits(
594    int max_visits,
595    std::vector<BriefVisitInfo>* result_vector) {
596  result_vector->clear();
597
598  sql::Statement statement(GetDB().GetUniqueStatement(
599      "SELECT url,visit_time,transition FROM visits "
600      "ORDER BY id DESC LIMIT ?"));
601
602  statement.BindInt64(0, max_visits);
603
604  if (!statement.is_valid())
605    return;
606
607  while (statement.Step()) {
608    BriefVisitInfo info;
609    info.url_id = statement.ColumnInt64(0);
610    info.time = base::Time::FromInternalValue(statement.ColumnInt64(1));
611    info.transition = content::PageTransitionFromInt(statement.ColumnInt(2));
612    result_vector->push_back(info);
613  }
614}
615
616}  // namespace history
617