// Copyright (c) 2006-2008 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #include #include #include #include #include "chrome/browser/history/visit_database.h" #include "chrome/browser/history/url_database.h" #include "chrome/common/page_transition_types.h" using base::Time; // Rows, in order, of the visit table. #define HISTORY_VISIT_ROW_FIELDS \ " id,url,visit_time,from_visit,transition,segment_id,is_indexed " namespace history { VisitDatabase::VisitDatabase() { } VisitDatabase::~VisitDatabase() { } bool VisitDatabase::InitVisitTable() { if (!DoesSqliteTableExist(GetDB(), "visits")) { if (sqlite3_exec(GetDB(), "CREATE TABLE visits(" "id INTEGER PRIMARY KEY," "url INTEGER NOT NULL," // key of the URL this corresponds to "visit_time INTEGER NOT NULL," "from_visit INTEGER," "transition INTEGER DEFAULT 0 NOT NULL," "segment_id INTEGER," "is_indexed BOOLEAN)", // True when we have indexed data for this visit. NULL, NULL, NULL) != SQLITE_OK) return false; } else if (!DoesSqliteColumnExist(GetDB(), "visits", "is_indexed", "BOOLEAN")) { // Old versions don't have the is_indexed column, we can just add that and // not worry about different database revisions, since old ones will // continue to work. // // TODO(brettw) this should be removed once we think everybody has been // updated (added early Mar 2008). if (sqlite3_exec(GetDB(), "ALTER TABLE visits ADD COLUMN is_indexed BOOLEAN", NULL, NULL, NULL) != SQLITE_OK) return false; } // Index over url so we can quickly find visits for a page. This will just // fail if it already exists and we'll ignore it. sqlite3_exec(GetDB(), "CREATE INDEX visits_url_index ON visits (url)", NULL, NULL, NULL); // Create an index over from visits so that we can efficiently find // referrers and redirects. Ignore failures because it likely already exists. sqlite3_exec(GetDB(), "CREATE INDEX visits_from_index ON visits (from_visit)", NULL, NULL, NULL); // Create an index over time so that we can efficiently find the visits in a // given time range (most history views are time-based). Ignore failures // because it likely already exists. sqlite3_exec(GetDB(), "CREATE INDEX visits_time_index ON visits (visit_time)", NULL, NULL, NULL); return true; } bool VisitDatabase::DropVisitTable() { // This will also drop the indices over the table. return sqlite3_exec(GetDB(), "DROP TABLE visits", NULL, NULL, NULL) == SQLITE_OK; } // Must be in sync with HISTORY_VISIT_ROW_FIELDS. // static void VisitDatabase::FillVisitRow(SQLStatement& statement, VisitRow* visit) { visit->visit_id = statement.column_int64(0); visit->url_id = statement.column_int64(1); visit->visit_time = Time::FromInternalValue(statement.column_int64(2)); visit->referring_visit = statement.column_int64(3); visit->transition = PageTransition::FromInt(statement.column_int(4)); visit->segment_id = statement.column_int64(5); visit->is_indexed = !!statement.column_int(6); } // static void VisitDatabase::FillVisitVector(SQLStatement& statement, VisitVector* visits) { while (statement.step() == SQLITE_ROW) { history::VisitRow visit; FillVisitRow(statement, &visit); visits->push_back(visit); } } VisitID VisitDatabase::AddVisit(VisitRow* visit) { SQLITE_UNIQUE_STATEMENT(statement, GetStatementCache(), "INSERT INTO visits " "(url, visit_time, from_visit, transition, segment_id, is_indexed) " "VALUES (?,?,?,?,?,?)"); if (!statement.is_valid()) return 0; statement->bind_int64(0, visit->url_id); statement->bind_int64(1, visit->visit_time.ToInternalValue()); statement->bind_int64(2, visit->referring_visit); statement->bind_int64(3, visit->transition); statement->bind_int64(4, visit->segment_id); statement->bind_int64(5, visit->is_indexed); if (statement->step() != SQLITE_DONE) return 0; visit->visit_id = sqlite3_last_insert_rowid(GetDB()); return visit->visit_id; } void VisitDatabase::DeleteVisit(const VisitRow& visit) { // Patch around this visit. Any visits that this went to will now have their // "source" be the deleted visit's source. SQLITE_UNIQUE_STATEMENT(update_chain, GetStatementCache(), "UPDATE visits SET from_visit=? " "WHERE from_visit=?"); if (!update_chain.is_valid()) return; update_chain->bind_int64(0, visit.referring_visit); update_chain->bind_int64(1, visit.visit_id); update_chain->step(); // Now delete the actual visit. SQLITE_UNIQUE_STATEMENT(del, GetStatementCache(), "DELETE FROM visits WHERE id=?"); if (!del.is_valid()) return; del->bind_int64(0, visit.visit_id); del->step(); } bool VisitDatabase::GetRowForVisit(VisitID visit_id, VisitRow* out_visit) { SQLITE_UNIQUE_STATEMENT(statement, GetStatementCache(), "SELECT" HISTORY_VISIT_ROW_FIELDS "FROM visits WHERE id=?"); if (!statement.is_valid()) return false; statement->bind_int64(0, visit_id); if (statement->step() != SQLITE_ROW) return false; FillVisitRow(*statement, out_visit); return true; } bool VisitDatabase::UpdateVisitRow(const VisitRow& visit) { SQLITE_UNIQUE_STATEMENT(statement, GetStatementCache(), "UPDATE visits SET " "url=?,visit_time=?,from_visit=?,transition=?,segment_id=?,is_indexed=? " "WHERE id=?"); if (!statement.is_valid()) return false; statement->bind_int64(0, visit.url_id); statement->bind_int64(1, visit.visit_time.ToInternalValue()); statement->bind_int64(2, visit.referring_visit); statement->bind_int64(3, visit.transition); statement->bind_int64(4, visit.segment_id); statement->bind_int64(5, visit.is_indexed); statement->bind_int64(6, visit.visit_id); return statement->step() == SQLITE_DONE; } bool VisitDatabase::GetVisitsForURL(URLID url_id, VisitVector* visits) { visits->clear(); SQLITE_UNIQUE_STATEMENT(statement, GetStatementCache(), "SELECT" HISTORY_VISIT_ROW_FIELDS "FROM visits " "WHERE url=? " "ORDER BY visit_time ASC"); if (!statement.is_valid()) return false; statement->bind_int64(0, url_id); FillVisitVector(*statement, visits); return true; } void VisitDatabase::GetAllVisitsInRange(Time begin_time, Time end_time, int max_results, VisitVector* visits) { visits->clear(); SQLITE_UNIQUE_STATEMENT(statement, GetStatementCache(), "SELECT" HISTORY_VISIT_ROW_FIELDS "FROM visits " "WHERE visit_time >= ? AND visit_time < ?" "ORDER BY visit_time LIMIT ?"); if (!statement.is_valid()) return; // See GetVisibleVisitsInRange for more info on how these times are bound. int64 end = end_time.ToInternalValue(); statement->bind_int64(0, begin_time.ToInternalValue()); statement->bind_int64(1, end ? end : std::numeric_limits::max()); statement->bind_int64(2, max_results ? max_results : std::numeric_limits::max()); FillVisitVector(*statement, visits); } void VisitDatabase::GetVisibleVisitsInRange(Time begin_time, Time end_time, bool most_recent_visit_only, int max_count, VisitVector* visits) { visits->clear(); // The visit_time values can be duplicated in a redirect chain, so we sort // by id too, to ensure a consistent ordering just in case. SQLITE_UNIQUE_STATEMENT(statement, GetStatementCache(), "SELECT" HISTORY_VISIT_ROW_FIELDS "FROM visits " "WHERE visit_time >= ? AND visit_time < ? " "AND (transition & ?) != 0 " // CHAIN_END "AND (transition & ?) NOT IN (?, ?) " // NO SUBFRAME "ORDER BY visit_time DESC, id DESC"); if (!statement.is_valid()) return; // Note that we use min/max values for querying unlimited ranges of time using // the same statement. Since the time has an index, this will be about the // same amount of work as just doing a query for everything with no qualifier. int64 end = end_time.ToInternalValue(); statement->bind_int64(0, begin_time.ToInternalValue()); statement->bind_int64(1, end ? end : std::numeric_limits::max()); statement->bind_int(2, PageTransition::CHAIN_END); statement->bind_int(3, PageTransition::CORE_MASK); statement->bind_int(4, PageTransition::AUTO_SUBFRAME); statement->bind_int(5, PageTransition::MANUAL_SUBFRAME); std::set found_urls; while (statement->step() == SQLITE_ROW) { VisitRow visit; FillVisitRow(*statement, &visit); if (most_recent_visit_only) { // Make sure the URL this visit corresponds to is unique if required. if (found_urls.find(visit.url_id) != found_urls.end()) continue; found_urls.insert(visit.url_id); } visits->push_back(visit); if (max_count > 0 && static_cast(visits->size()) >= max_count) break; } } VisitID VisitDatabase::GetMostRecentVisitForURL(URLID url_id, VisitRow* visit_row) { // The visit_time values can be duplicated in a redirect chain, so we sort // by id too, to ensure a consistent ordering just in case. SQLITE_UNIQUE_STATEMENT(statement, GetStatementCache(), "SELECT" HISTORY_VISIT_ROW_FIELDS "FROM visits " "WHERE url=? " "ORDER BY visit_time DESC, id DESC " "LIMIT 1"); if (!statement.is_valid()) return 0; statement->bind_int64(0, url_id); if (statement->step() != SQLITE_ROW) return 0; // No visits for this URL. if (visit_row) { FillVisitRow(*statement, visit_row); return visit_row->visit_id; } return statement->column_int64(0); } bool VisitDatabase::GetMostRecentVisitsForURL(URLID url_id, int max_results, VisitVector* visits) { visits->clear(); // The visit_time values can be duplicated in a redirect chain, so we sort // by id too, to ensure a consistent ordering just in case. SQLITE_UNIQUE_STATEMENT(statement, GetStatementCache(), "SELECT" HISTORY_VISIT_ROW_FIELDS "FROM visits " "WHERE url=? " "ORDER BY visit_time DESC, id DESC " "LIMIT ?"); if (!statement.is_valid()) return false; statement->bind_int64(0, url_id); statement->bind_int(1, max_results); FillVisitVector(*statement, visits); return true; } bool VisitDatabase::GetRedirectFromVisit(VisitID from_visit, VisitID* to_visit, GURL* to_url) { SQLITE_UNIQUE_STATEMENT(statement, GetStatementCache(), "SELECT v.id,u.url " "FROM visits v JOIN urls u ON v.url = u.id " "WHERE v.from_visit = ? " "AND (v.transition & ?) != 0"); // IS_REDIRECT_MASK if (!statement.is_valid()) return false; statement->bind_int64(0, from_visit); statement->bind_int(1, PageTransition::IS_REDIRECT_MASK); if (statement->step() != SQLITE_ROW) return false; // No redirect from this visit. if (to_visit) *to_visit = statement->column_int64(0); if (to_url) *to_url = GURL(statement->column_string(1)); return true; } bool VisitDatabase::GetVisitCountToHost(const GURL& url, int* count, Time* first_visit) { if (!url.SchemeIs("http") && !url.SchemeIs("https")) return false; // We need to search for URLs with a matching host/port. One way to query for // this is to use the LIKE operator, eg 'url LIKE http://google.com/%'. This // is inefficient though in that it doesn't use the index and each entry must // be visited. The same query can be executed by using >= and < operator. // The query becomes: // 'url >= http://google.com/' and url < http://google.com0'. // 0 is used as it is one character greater than '/'. GURL search_url(url); const std::string host_query_min = search_url.GetOrigin().spec(); if (host_query_min.empty()) return false; std::string host_query_max = host_query_min; host_query_max[host_query_max.size() - 1] = '0'; SQLITE_UNIQUE_STATEMENT(statement, GetStatementCache(), "SELECT MIN(v.visit_time), COUNT(*) " "FROM visits v INNER JOIN urls u ON v.url = u.id " "WHERE (u.url >= ? AND u.url < ?)"); if (!statement.is_valid()) return false; statement->bind_string(0, host_query_min); statement->bind_string(1, host_query_max); if (statement->step() != SQLITE_ROW) { // We've never been to this page before. *count = 0; return true; } *first_visit = Time::FromInternalValue(statement->column_int64(0)); *count = statement->column_int(1); return true; } } // namespace history