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