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