| // 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 "chrome/browser/history/url_database.h" |
| |
| #include <algorithm> |
| #include <limits> |
| |
| #include "base/string_util.h" |
| #include "chrome/common/l10n_util.h" |
| #include "chrome/common/sqlite_utils.h" |
| #include "googleurl/src/gurl.h" |
| |
| using base::Time; |
| |
| namespace history { |
| |
| const char URLDatabase::kURLRowFields[] = HISTORY_URL_ROW_FIELDS; |
| const int URLDatabase::kNumURLRowFields = 9; |
| |
| bool URLDatabase::URLEnumerator::GetNextURL(URLRow* r) { |
| if (statement_.step() == SQLITE_ROW) { |
| FillURLRow(statement_, r); |
| return true; |
| } |
| return false; |
| } |
| |
| URLDatabase::URLDatabase() : has_keyword_search_terms_(false) { |
| } |
| |
| URLDatabase::~URLDatabase() { |
| } |
| |
| // static |
| std::string URLDatabase::GURLToDatabaseURL(const GURL& gurl) { |
| // TODO(brettw): do something fancy here with encoding, etc. |
| return gurl.spec(); |
| } |
| |
| // Convenience to fill a history::URLRow. Must be in sync with the fields in |
| // kURLRowFields. |
| void URLDatabase::FillURLRow(SQLStatement& s, history::URLRow* i) { |
| DCHECK(i); |
| i->id_ = s.column_int64(0); |
| i->url_ = GURL(s.column_string(1)); |
| i->title_ = s.column_wstring(2); |
| i->visit_count_ = s.column_int(3); |
| i->typed_count_ = s.column_int(4); |
| i->last_visit_ = Time::FromInternalValue(s.column_int64(5)); |
| i->hidden_ = s.column_int(6) != 0; |
| i->favicon_id_ = s.column_int64(7); |
| } |
| |
| bool URLDatabase::GetURLRow(URLID url_id, URLRow* info) { |
| // TODO(brettw) We need check for empty URLs to handle the case where |
| // there are old URLs in the database that are empty that got in before |
| // we added any checks. We should eventually be able to remove it |
| // when all inputs are using GURL (which prohibit empty input). |
| SQLITE_UNIQUE_STATEMENT(statement, GetStatementCache(), |
| "SELECT" HISTORY_URL_ROW_FIELDS "FROM urls WHERE id=?"); |
| if (!statement.is_valid()) |
| return false; |
| |
| statement->bind_int64(0, url_id); |
| if (statement->step() == SQLITE_ROW) { |
| FillURLRow(*statement, info); |
| return true; |
| } |
| return false; |
| } |
| |
| URLID URLDatabase::GetRowForURL(const GURL& url, history::URLRow* info) { |
| SQLITE_UNIQUE_STATEMENT(statement, GetStatementCache(), |
| "SELECT" HISTORY_URL_ROW_FIELDS "FROM urls WHERE url=?"); |
| if (!statement.is_valid()) |
| return 0; |
| |
| std::string url_string = GURLToDatabaseURL(url); |
| statement->bind_string(0, url_string); |
| if (statement->step() != SQLITE_ROW) |
| return 0; // no data |
| |
| if (info) |
| FillURLRow(*statement, info); |
| return statement->column_int64(0); |
| } |
| |
| bool URLDatabase::UpdateURLRow(URLID url_id, |
| const history::URLRow& info) { |
| SQLITE_UNIQUE_STATEMENT(statement, GetStatementCache(), |
| "UPDATE urls SET title=?,visit_count=?,typed_count=?,last_visit_time=?," |
| "hidden=?,favicon_id=?" |
| "WHERE id=?"); |
| if (!statement.is_valid()) |
| return false; |
| |
| statement->bind_wstring(0, info.title()); |
| statement->bind_int(1, info.visit_count()); |
| statement->bind_int(2, info.typed_count()); |
| statement->bind_int64(3, info.last_visit().ToInternalValue()); |
| statement->bind_int(4, info.hidden() ? 1 : 0); |
| statement->bind_int64(5, info.favicon_id()); |
| statement->bind_int64(6, url_id); |
| return statement->step() == SQLITE_DONE; |
| } |
| |
| URLID URLDatabase::AddURLInternal(const history::URLRow& info, |
| bool is_temporary) { |
| // This function is used to insert into two different tables, so we have to |
| // do some shuffling. Unfortinately, we can't use the macro |
| // HISTORY_URL_ROW_FIELDS because that specifies the table name which is |
| // invalid in the insert syntax. |
| #define ADDURL_COMMON_SUFFIX \ |
| " (url, title, visit_count, typed_count, "\ |
| "last_visit_time, hidden, favicon_id) "\ |
| "VALUES (?,?,?,?,?,?,?)" |
| const char* statement_name; |
| const char* statement_sql; |
| if (is_temporary) { |
| statement_name = "AddURLTemporary"; |
| statement_sql = "INSERT INTO temp_urls" ADDURL_COMMON_SUFFIX; |
| } else { |
| statement_name = "AddURL"; |
| statement_sql = "INSERT INTO urls" ADDURL_COMMON_SUFFIX; |
| } |
| #undef ADDURL_COMMON_SUFFIX |
| |
| SqliteCompiledStatement statement(statement_name, 0, GetStatementCache(), |
| statement_sql); |
| if (!statement.is_valid()) |
| return 0; |
| |
| statement->bind_string(0, GURLToDatabaseURL(info.url())); |
| statement->bind_wstring(1, info.title()); |
| statement->bind_int(2, info.visit_count()); |
| statement->bind_int(3, info.typed_count()); |
| statement->bind_int64(4, info.last_visit().ToInternalValue()); |
| statement->bind_int(5, info.hidden() ? 1 : 0); |
| statement->bind_int64(6, info.favicon_id()); |
| |
| if (statement->step() != SQLITE_DONE) |
| return 0; |
| return sqlite3_last_insert_rowid(GetDB()); |
| } |
| |
| bool URLDatabase::DeleteURLRow(URLID id) { |
| SQLITE_UNIQUE_STATEMENT(statement, GetStatementCache(), |
| "DELETE FROM urls WHERE id = ?"); |
| if (!statement.is_valid()) |
| return false; |
| |
| statement->bind_int64(0, id); |
| if (statement->step() != SQLITE_DONE) |
| return false; |
| |
| // And delete any keyword visits. |
| if (!has_keyword_search_terms_) |
| return true; |
| |
| SQLITE_UNIQUE_STATEMENT(del_keyword_visit, GetStatementCache(), |
| "DELETE FROM keyword_search_terms WHERE url_id=?"); |
| if (!del_keyword_visit.is_valid()) |
| return false; |
| del_keyword_visit->bind_int64(0, id); |
| return (del_keyword_visit->step() == SQLITE_DONE); |
| } |
| |
| bool URLDatabase::CreateTemporaryURLTable() { |
| return CreateURLTable(true); |
| } |
| |
| bool URLDatabase::CommitTemporaryURLTable() { |
| // See the comments in the header file as well as |
| // HistoryBackend::DeleteAllHistory() for more information on how this works |
| // and why it does what it does. |
| // |
| // Note that the main database overrides this to additionally create the |
| // supplimentary indices that the archived database doesn't need. |
| |
| // Swap the url table out and replace it with the temporary one. |
| if (sqlite3_exec(GetDB(), "DROP TABLE urls", |
| NULL, NULL, NULL) != SQLITE_OK) { |
| NOTREACHED(); |
| return false; |
| } |
| if (sqlite3_exec(GetDB(), "ALTER TABLE temp_urls RENAME TO urls", |
| NULL, NULL, NULL) != SQLITE_OK) { |
| NOTREACHED(); |
| return false; |
| } |
| |
| // Create the index over URLs. This is needed for the main, in-memory, and |
| // archived databases, so we always do it. The supplimentary indices used by |
| // the main database are not created here. When deleting all history, they |
| // are created by HistoryDatabase::RecreateAllButStarAndURLTables(). |
| CreateMainURLIndex(); |
| |
| return true; |
| } |
| |
| bool URLDatabase::InitURLEnumeratorForEverything(URLEnumerator* enumerator) { |
| DCHECK(!enumerator->initialized_); |
| std::string sql("SELECT "); |
| sql.append(kURLRowFields); |
| sql.append(" FROM urls"); |
| if (enumerator->statement_.prepare(GetDB(), sql.c_str()) != SQLITE_OK) { |
| NOTREACHED() << "Query statement prep failed"; |
| return false; |
| } |
| enumerator->initialized_ = true; |
| return true; |
| } |
| |
| bool URLDatabase::IsFavIconUsed(FavIconID favicon_id) { |
| SQLITE_UNIQUE_STATEMENT(statement, GetStatementCache(), |
| "SELECT id FROM urls WHERE favicon_id=? LIMIT 1"); |
| if (!statement.is_valid()) |
| return false; |
| |
| statement->bind_int64(0, favicon_id); |
| return statement->step() == SQLITE_ROW; |
| } |
| |
| void URLDatabase::AutocompleteForPrefix(const std::wstring& prefix, |
| size_t max_results, |
| std::vector<history::URLRow>* results) { |
| // NOTE: this query originally sorted by starred as the second parameter. But |
| // as bookmarks is no longer part of the db we no longer include the order |
| // by clause. |
| results->clear(); |
| SQLITE_UNIQUE_STATEMENT(statement, GetStatementCache(), |
| "SELECT" HISTORY_URL_ROW_FIELDS "FROM urls " |
| "WHERE url >= ? AND url < ? AND hidden = 0 " |
| "ORDER BY typed_count DESC, visit_count DESC, last_visit_time DESC " |
| "LIMIT ?"); |
| if (!statement.is_valid()) |
| return; |
| |
| // We will find all strings between "prefix" and this string, which is prefix |
| // followed by the maximum character size. Use 8-bit strings for everything |
| // so we can be sure sqlite is comparing everything in 8-bit mode. Otherwise, |
| // it will have to convert strings either to UTF-8 or UTF-16 for comparison. |
| std::string prefix_utf8(WideToUTF8(prefix)); |
| std::string end_query(prefix_utf8); |
| end_query.push_back(std::numeric_limits<unsigned char>::max()); |
| |
| statement->bind_string(0, prefix_utf8); |
| statement->bind_string(1, end_query); |
| statement->bind_int(2, static_cast<int>(max_results)); |
| |
| while (statement->step() == SQLITE_ROW) { |
| history::URLRow info; |
| FillURLRow(*statement, &info); |
| if (info.url().is_valid()) |
| results->push_back(info); |
| } |
| } |
| |
| bool URLDatabase::FindShortestURLFromBase(const std::string& base, |
| const std::string& url, |
| int min_visits, |
| int min_typed, |
| bool allow_base, |
| history::URLRow* info) { |
| // Select URLs that start with |base| and are prefixes of |url|. All parts |
| // of this query except the substr() call can be done using the index. We |
| // could do this query with a couple of LIKE or GLOB statements as well, but |
| // those wouldn't use the index, and would run into problems with "wildcard" |
| // characters that appear in URLs (% for LIKE, or *, ? for GLOB). |
| std::string sql("SELECT "); |
| sql.append(kURLRowFields); |
| sql.append(" FROM urls WHERE url "); |
| sql.append(allow_base ? ">=" : ">"); |
| sql.append(" ? AND url < :end AND url = substr(:end, 1, length(url)) " |
| "AND hidden = 0 AND visit_count >= ? AND typed_count >= ? " |
| "ORDER BY url LIMIT 1"); |
| SQLStatement statement; |
| if (statement.prepare(GetDB(), sql.c_str()) != SQLITE_OK) { |
| NOTREACHED() << "select statement prep failed"; |
| return false; |
| } |
| |
| statement.bind_string(0, base); |
| statement.bind_string(1, url); // :end |
| statement.bind_int(2, min_visits); |
| statement.bind_int(3, min_typed); |
| |
| if (statement.step() != SQLITE_ROW) |
| return false; |
| |
| DCHECK(info); |
| FillURLRow(statement, info); |
| return true; |
| } |
| |
| bool URLDatabase::InitKeywordSearchTermsTable() { |
| has_keyword_search_terms_ = true; |
| if (!DoesSqliteTableExist(GetDB(), "keyword_search_terms")) { |
| if (sqlite3_exec(GetDB(), "CREATE TABLE keyword_search_terms (" |
| "keyword_id INTEGER NOT NULL," // ID of the TemplateURL. |
| "url_id INTEGER NOT NULL," // ID of the url. |
| "lower_term LONGVARCHAR NOT NULL," // The search term, in lower case. |
| "term LONGVARCHAR NOT NULL)", // The actual search term. |
| NULL, NULL, NULL) != SQLITE_OK) |
| return false; |
| } |
| |
| // For searching. |
| sqlite3_exec(GetDB(), "CREATE INDEX keyword_search_terms_index1 ON " |
| "keyword_search_terms (keyword_id, lower_term)", |
| NULL, NULL, NULL); |
| |
| // For deletion. |
| sqlite3_exec(GetDB(), "CREATE INDEX keyword_search_terms_index2 ON " |
| "keyword_search_terms (url_id)", |
| NULL, NULL, NULL); |
| |
| return true; |
| } |
| |
| bool URLDatabase::DropKeywordSearchTermsTable() { |
| // This will implicitly delete the indices over the table. |
| return sqlite3_exec(GetDB(), "DROP TABLE keyword_search_terms", |
| NULL, NULL, NULL) == SQLITE_OK; |
| } |
| |
| bool URLDatabase::SetKeywordSearchTermsForURL(URLID url_id, |
| TemplateURL::IDType keyword_id, |
| const std::wstring& term) { |
| DCHECK(url_id && keyword_id && !term.empty()); |
| |
| SQLITE_UNIQUE_STATEMENT(exist_statement, GetStatementCache(), |
| "SELECT term FROM keyword_search_terms " |
| "WHERE keyword_id = ? AND url_id = ?"); |
| if (!exist_statement.is_valid()) |
| return false; |
| exist_statement->bind_int64(0, keyword_id); |
| exist_statement->bind_int64(1, url_id); |
| if (exist_statement->step() == SQLITE_ROW) |
| return true; // Term already exists, no need to add it. |
| |
| SQLITE_UNIQUE_STATEMENT(statement, GetStatementCache(), |
| "INSERT INTO keyword_search_terms (keyword_id, url_id, lower_term, term) " |
| "VALUES (?,?,?,?)"); |
| if (!statement.is_valid()) |
| return false; |
| |
| statement->bind_int64(0, keyword_id); |
| statement->bind_int64(1, url_id); |
| statement->bind_wstring(2, l10n_util::ToLower(term)); |
| statement->bind_wstring(3, term); |
| return (statement->step() == SQLITE_DONE); |
| } |
| |
| void URLDatabase::DeleteAllSearchTermsForKeyword( |
| TemplateURL::IDType keyword_id) { |
| DCHECK(keyword_id); |
| SQLITE_UNIQUE_STATEMENT(statement, GetStatementCache(), |
| "DELETE FROM keyword_search_terms WHERE keyword_id=?"); |
| if (!statement.is_valid()) |
| return; |
| |
| statement->bind_int64(0, keyword_id); |
| statement->step(); |
| } |
| |
| void URLDatabase::GetMostRecentKeywordSearchTerms( |
| TemplateURL::IDType keyword_id, |
| const std::wstring& prefix, |
| int max_count, |
| std::vector<KeywordSearchTermVisit>* matches) { |
| // NOTE: the keyword_id can be zero if on first run the user does a query |
| // before the TemplateURLModel has finished loading. As the chances of this |
| // occurring are small, we ignore it. |
| if (!keyword_id) |
| return; |
| |
| DCHECK(!prefix.empty()); |
| SQLITE_UNIQUE_STATEMENT(statement, GetStatementCache(), |
| "SELECT DISTINCT kv.term, u.last_visit_time " |
| "FROM keyword_search_terms kv " |
| "JOIN urls u ON kv.url_id = u.id " |
| "WHERE kv.keyword_id = ? AND kv.lower_term >= ? AND kv.lower_term < ? " |
| "ORDER BY u.last_visit_time DESC LIMIT ?"); |
| if (!statement.is_valid()) |
| return; |
| |
| // NOTE: Keep this ToLower() call in sync with search_provider.cc. |
| const std::wstring lower_prefix = l10n_util::ToLower(prefix); |
| // This magic gives us a prefix search. |
| std::wstring next_prefix = lower_prefix; |
| next_prefix[next_prefix.size() - 1] = |
| next_prefix[next_prefix.size() - 1] + 1; |
| statement->bind_int64(0, keyword_id); |
| statement->bind_wstring(1, lower_prefix); |
| statement->bind_wstring(2, next_prefix); |
| statement->bind_int(3, max_count); |
| |
| KeywordSearchTermVisit visit; |
| while (statement->step() == SQLITE_ROW) { |
| visit.term = statement->column_wstring(0); |
| visit.time = Time::FromInternalValue(statement->column_int64(1)); |
| matches->push_back(visit); |
| } |
| } |
| |
| bool URLDatabase::MigrateFromVersion11ToVersion12() { |
| URLRow about_row; |
| if (GetRowForURL(GURL("about:blank"), &about_row)) { |
| about_row.set_favicon_id(0); |
| return UpdateURLRow(about_row.id(), about_row); |
| } |
| return true; |
| } |
| |
| bool URLDatabase::DropStarredIDFromURLs() { |
| if (!DoesSqliteColumnExist(GetDB(), "urls", "starred_id", NULL)) |
| return true; // urls is already updated, no need to continue. |
| |
| // Create a temporary table to contain the new URLs table. |
| if (!CreateTemporaryURLTable()) { |
| NOTREACHED(); |
| return false; |
| } |
| |
| // Copy the contents. |
| const char* insert_statement = |
| "INSERT INTO temp_urls (id, url, title, visit_count, typed_count, " |
| "last_visit_time, hidden, favicon_id) " |
| "SELECT id, url, title, visit_count, typed_count, last_visit_time, " |
| "hidden, favicon_id FROM urls"; |
| if (sqlite3_exec(GetDB(), insert_statement, NULL, NULL, NULL) != SQLITE_OK) { |
| NOTREACHED(); |
| return false; |
| } |
| |
| // Rename/commit the tmp table. |
| CommitTemporaryURLTable(); |
| |
| // This isn't created by CommitTemporaryURLTable. |
| CreateSupplimentaryURLIndices(); |
| |
| return true; |
| } |
| |
| bool URLDatabase::CreateURLTable(bool is_temporary) { |
| const char* name = is_temporary ? "temp_urls" : "urls"; |
| if (DoesSqliteTableExist(GetDB(), name)) |
| return true; |
| |
| std::string sql; |
| sql.append("CREATE TABLE "); |
| sql.append(name); |
| sql.append("(" |
| "id INTEGER PRIMARY KEY," |
| "url LONGVARCHAR," |
| "title LONGVARCHAR," |
| "visit_count INTEGER DEFAULT 0 NOT NULL," |
| "typed_count INTEGER DEFAULT 0 NOT NULL," |
| "last_visit_time INTEGER NOT NULL," |
| "hidden INTEGER DEFAULT 0 NOT NULL," |
| "favicon_id INTEGER DEFAULT 0 NOT NULL)"); |
| |
| return sqlite3_exec(GetDB(), sql.c_str(), NULL, NULL, NULL) == SQLITE_OK; |
| } |
| |
| void URLDatabase::CreateMainURLIndex() { |
| // Index over URLs so we can quickly look up based on URL. Ignore errors as |
| // this likely already exists (and the same below). |
| sqlite3_exec(GetDB(), "CREATE INDEX urls_url_index ON urls (url)", NULL, NULL, |
| NULL); |
| } |
| |
| void URLDatabase::CreateSupplimentaryURLIndices() { |
| // Add a favicon index. This is useful when we delete urls. |
| sqlite3_exec(GetDB(), |
| "CREATE INDEX urls_favicon_id_INDEX ON urls (favicon_id)", |
| NULL, NULL, NULL); |
| } |
| |
| } // namespace history |
| |