// Copyright (c) 2012 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 "content/browser/appcache/appcache_database.h"

#include "base/auto_reset.h"
#include "base/bind.h"
#include "base/command_line.h"
#include "base/files/file_util.h"
#include "base/logging.h"
#include "base/macros.h"
#include "base/strings/utf_string_conversions.h"
#include "content/browser/appcache/appcache_entry.h"
#include "content/browser/appcache/appcache_histograms.h"
#include "sql/connection.h"
#include "sql/error_delegate_util.h"
#include "sql/meta_table.h"
#include "sql/statement.h"
#include "sql/transaction.h"

namespace content {

// Schema -------------------------------------------------------------------
namespace {

const int kCurrentVersion = 7;
const int kCompatibleVersion = 7;
const bool kCreateIfNeeded = true;
const bool kDontCreate = false;

// A mechanism to run experiments that may affect in data being persisted
// in different ways such that when the experiment is toggled on/off via
// cmd line flags, the database gets reset. The active flags are stored at
// the time of database creation and compared when reopening. If different
// the database is reset.
const char kExperimentFlagsKey[] = "ExperimentFlags";

const char kGroupsTable[] = "Groups";
const char kCachesTable[] = "Caches";
const char kEntriesTable[] = "Entries";
const char kNamespacesTable[] = "Namespaces";
const char kOnlineWhiteListsTable[] = "OnlineWhiteLists";
const char kDeletableResponseIdsTable[] = "DeletableResponseIds";

struct TableInfo {
  const char* table_name;
  const char* columns;
};

struct IndexInfo {
  const char* index_name;
  const char* table_name;
  const char* columns;
  bool unique;
};

const TableInfo kTables[] = {
  { kGroupsTable,
    "(group_id INTEGER PRIMARY KEY,"
    " origin TEXT,"
    " manifest_url TEXT,"
    " creation_time INTEGER,"
    " last_access_time INTEGER,"
    " last_full_update_check_time INTEGER,"
    " first_evictable_error_time INTEGER)" },

  { kCachesTable,
    "(cache_id INTEGER PRIMARY KEY,"
    " group_id INTEGER,"
    " online_wildcard INTEGER CHECK(online_wildcard IN (0, 1)),"
    " update_time INTEGER,"
    " cache_size INTEGER)" },  // intentionally not normalized

  { kEntriesTable,
    "(cache_id INTEGER,"
    " url TEXT,"
    " flags INTEGER,"
    " response_id INTEGER,"
    " response_size INTEGER)" },

  { kNamespacesTable,
    "(cache_id INTEGER,"
    " origin TEXT,"  // intentionally not normalized
    " type INTEGER,"
    " namespace_url TEXT,"
    " target_url TEXT,"
    " is_pattern INTEGER CHECK(is_pattern IN (0, 1)))" },

  { kOnlineWhiteListsTable,
    "(cache_id INTEGER,"
    " namespace_url TEXT,"
    " is_pattern INTEGER CHECK(is_pattern IN (0, 1)))" },

  { kDeletableResponseIdsTable,
    "(response_id INTEGER NOT NULL)" },
};

const IndexInfo kIndexes[] = {
  { "GroupsOriginIndex",
    kGroupsTable,
    "(origin)",
    false },

  { "GroupsManifestIndex",
    kGroupsTable,
    "(manifest_url)",
    true },

  { "CachesGroupIndex",
    kCachesTable,
    "(group_id)",
    false },

  { "EntriesCacheIndex",
    kEntriesTable,
    "(cache_id)",
    false },

  { "EntriesCacheAndUrlIndex",
    kEntriesTable,
    "(cache_id, url)",
    true },

  { "EntriesResponseIdIndex",
    kEntriesTable,
    "(response_id)",
    true },

  { "NamespacesCacheIndex",
    kNamespacesTable,
    "(cache_id)",
    false },

  { "NamespacesOriginIndex",
    kNamespacesTable,
    "(origin)",
    false },

  { "NamespacesCacheAndUrlIndex",
    kNamespacesTable,
    "(cache_id, namespace_url)",
    true },

  { "OnlineWhiteListCacheIndex",
    kOnlineWhiteListsTable,
    "(cache_id)",
    false },

  { "DeletableResponsesIdIndex",
    kDeletableResponseIdsTable,
    "(response_id)",
    true },
};

const int kTableCount = arraysize(kTables);
const int kIndexCount = arraysize(kIndexes);

bool CreateTable(sql::Connection* db, const TableInfo& info) {
  std::string sql("CREATE TABLE ");
  sql += info.table_name;
  sql += info.columns;
  return db->Execute(sql.c_str());
}

bool CreateIndex(sql::Connection* db, const IndexInfo& info) {
  std::string sql;
  if (info.unique)
    sql += "CREATE UNIQUE INDEX ";
  else
    sql += "CREATE INDEX ";
  sql += info.index_name;
  sql += " ON ";
  sql += info.table_name;
  sql += info.columns;
  return db->Execute(sql.c_str());
}

std::string GetActiveExperimentFlags() {
  return std::string();
}

// GetURL().spec() is used instead of Serialize() to ensure
// backwards compatibility with older data.
std::string SerializeOrigin(const url::Origin& origin) {
  return origin.GetURL().spec();
}

}  // anon namespace

// AppCacheDatabase ----------------------------------------------------------

AppCacheDatabase::GroupRecord::GroupRecord()
    : group_id(0) {
}

AppCacheDatabase::GroupRecord::GroupRecord(const GroupRecord& other) = default;

AppCacheDatabase::GroupRecord::~GroupRecord() {
}

AppCacheDatabase::NamespaceRecord::NamespaceRecord()
    : cache_id(0) {
}

AppCacheDatabase::NamespaceRecord::~NamespaceRecord() {
}


AppCacheDatabase::AppCacheDatabase(const base::FilePath& path)
    : db_file_path_(path),
      is_disabled_(false),
      is_recreating_(false),
      was_corruption_detected_(false) {
}

AppCacheDatabase::~AppCacheDatabase() {
  CommitLazyLastAccessTimes();
}

void AppCacheDatabase::Disable() {
  VLOG(1) << "Disabling appcache database.";
  is_disabled_ = true;
  ResetConnectionAndTables();
}

int64_t AppCacheDatabase::GetOriginUsage(const url::Origin& origin) {
  std::vector<CacheRecord> records;
  if (!FindCachesForOrigin(origin, &records))
    return 0;

  int64_t origin_usage = 0;
  for (const auto& record : records)
    origin_usage += record.cache_size;
  return origin_usage;
}

bool AppCacheDatabase::GetAllOriginUsage(
    std::map<url::Origin, int64_t>* usage_map) {
  std::set<url::Origin> origins;
  if (!FindOriginsWithGroups(&origins))
    return false;
  for (const auto& origin : origins)
    (*usage_map)[origin] = GetOriginUsage(origin);
  return true;
}

bool AppCacheDatabase::FindOriginsWithGroups(std::set<url::Origin>* origins) {
  DCHECK(origins && origins->empty());
  if (!LazyOpen(kDontCreate))
    return false;

  static const char kSql[] = "SELECT DISTINCT(origin) FROM Groups";

  sql::Statement statement(db_->GetUniqueStatement(kSql));

  while (statement.Step())
    origins->insert(url::Origin::Create(GURL(statement.ColumnString(0))));

  return statement.Succeeded();
}

bool AppCacheDatabase::FindLastStorageIds(
    int64_t* last_group_id,
    int64_t* last_cache_id,
    int64_t* last_response_id,
    int64_t* last_deletable_response_rowid) {
  DCHECK(last_group_id && last_cache_id && last_response_id &&
         last_deletable_response_rowid);

  *last_group_id = 0;
  *last_cache_id = 0;
  *last_response_id = 0;
  *last_deletable_response_rowid = 0;

  if (!LazyOpen(kDontCreate))
    return false;

  static const char kMaxGroupIdSql[] = "SELECT MAX(group_id) FROM Groups";
  static const char kMaxCacheIdSql[] = "SELECT MAX(cache_id) FROM Caches";
  static const char kMaxResponseIdFromEntriesSql[] =
      "SELECT MAX(response_id) FROM Entries";
  static const char kMaxResponseIdFromDeletablesSql[] =
      "SELECT MAX(response_id) FROM DeletableResponseIds";
  static const char kMaxDeletableResponseRowIdSql[] =
      "SELECT MAX(rowid) FROM DeletableResponseIds";
  int64_t max_group_id;
  int64_t max_cache_id;
  int64_t max_response_id_from_entries;
  int64_t max_response_id_from_deletables;
  int64_t max_deletable_response_rowid;
  if (!RunUniqueStatementWithInt64Result(kMaxGroupIdSql, &max_group_id) ||
      !RunUniqueStatementWithInt64Result(kMaxCacheIdSql, &max_cache_id) ||
      !RunUniqueStatementWithInt64Result(kMaxResponseIdFromEntriesSql,
                                         &max_response_id_from_entries) ||
      !RunUniqueStatementWithInt64Result(kMaxResponseIdFromDeletablesSql,
                                         &max_response_id_from_deletables) ||
      !RunUniqueStatementWithInt64Result(kMaxDeletableResponseRowIdSql,
                                         &max_deletable_response_rowid)) {
    return false;
  }

  *last_group_id = max_group_id;
  *last_cache_id = max_cache_id;
  *last_response_id = std::max(max_response_id_from_entries,
                               max_response_id_from_deletables);
  *last_deletable_response_rowid = max_deletable_response_rowid;
  return true;
}

bool AppCacheDatabase::FindGroup(int64_t group_id, GroupRecord* record) {
  DCHECK(record);
  if (!LazyOpen(kDontCreate))
    return false;

  static const char kSql[] =
      "SELECT group_id, origin, manifest_url,"
      "       creation_time, last_access_time,"
      "       last_full_update_check_time,"
      "       first_evictable_error_time"
      "  FROM Groups WHERE group_id = ?";

  sql::Statement statement(db_->GetCachedStatement(SQL_FROM_HERE, kSql));

  statement.BindInt64(0, group_id);
  if (!statement.Step())
    return false;

  ReadGroupRecord(statement, record);
  DCHECK(record->group_id == group_id);
  return true;
}

bool AppCacheDatabase::FindGroupForManifestUrl(
    const GURL& manifest_url, GroupRecord* record) {
  DCHECK(record);
  if (!LazyOpen(kDontCreate))
    return false;

  static const char kSql[] =
      "SELECT group_id, origin, manifest_url,"
      "       creation_time, last_access_time,"
      "       last_full_update_check_time,"
      "       first_evictable_error_time"
      "  FROM Groups WHERE manifest_url = ?";

  sql::Statement statement(db_->GetCachedStatement(SQL_FROM_HERE, kSql));
  statement.BindString(0, manifest_url.spec());

  if (!statement.Step())
    return false;

  ReadGroupRecord(statement, record);
  DCHECK(record->manifest_url == manifest_url);
  return true;
}

bool AppCacheDatabase::FindGroupsForOrigin(const url::Origin& origin,
                                           std::vector<GroupRecord>* records) {
  DCHECK(records && records->empty());
  if (!LazyOpen(kDontCreate))
    return false;

  static const char kSql[] =
      "SELECT group_id, origin, manifest_url,"
      "       creation_time, last_access_time,"
      "       last_full_update_check_time,"
      "       first_evictable_error_time"
      "   FROM Groups WHERE origin = ?";

  sql::Statement statement(db_->GetCachedStatement(SQL_FROM_HERE, kSql));
  statement.BindString(0, SerializeOrigin(origin));

  while (statement.Step()) {
    records->push_back(GroupRecord());
    ReadGroupRecord(statement, &records->back());
    DCHECK(records->back().origin == origin);
  }

  return statement.Succeeded();
}

bool AppCacheDatabase::FindGroupForCache(int64_t cache_id,
                                         GroupRecord* record) {
  DCHECK(record);
  if (!LazyOpen(kDontCreate))
    return false;

  static const char kSql[] =
      "SELECT g.group_id, g.origin, g.manifest_url,"
      "       g.creation_time, g.last_access_time,"
      "       g.last_full_update_check_time,"
      "       g.first_evictable_error_time"
      "  FROM Groups g, Caches c"
      "  WHERE c.cache_id = ? AND c.group_id = g.group_id";

  sql::Statement statement(db_->GetCachedStatement(SQL_FROM_HERE, kSql));
  statement.BindInt64(0, cache_id);

  if (!statement.Step())
    return false;

  ReadGroupRecord(statement, record);
  return true;
}

bool AppCacheDatabase::InsertGroup(const GroupRecord* record) {
  if (!LazyOpen(kCreateIfNeeded))
    return false;

  static const char kSql[] =
      "INSERT INTO Groups"
      "  (group_id, origin, manifest_url, creation_time, last_access_time,"
      "   last_full_update_check_time, first_evictable_error_time)"
      "  VALUES(?, ?, ?, ?, ?, ?, ?)";
  sql::Statement statement(db_->GetCachedStatement(SQL_FROM_HERE, kSql));
  statement.BindInt64(0, record->group_id);
  statement.BindString(1, SerializeOrigin(record->origin));
  statement.BindString(2, record->manifest_url.spec());
  statement.BindInt64(3, record->creation_time.ToInternalValue());
  statement.BindInt64(4, record->last_access_time.ToInternalValue());
  statement.BindInt64(5, record->last_full_update_check_time.ToInternalValue());
  statement.BindInt64(6, record->first_evictable_error_time.ToInternalValue());
  return statement.Run();
}

bool AppCacheDatabase::DeleteGroup(int64_t group_id) {
  if (!LazyOpen(kDontCreate))
    return false;

  static const char kSql[] = "DELETE FROM Groups WHERE group_id = ?";
  sql::Statement statement(db_->GetCachedStatement(SQL_FROM_HERE, kSql));
  statement.BindInt64(0, group_id);
  return statement.Run();
}

bool AppCacheDatabase::UpdateLastAccessTime(int64_t group_id, base::Time time) {
  if (!LazyUpdateLastAccessTime(group_id, time))
    return false;
  return CommitLazyLastAccessTimes();
}

bool AppCacheDatabase::LazyUpdateLastAccessTime(int64_t group_id,
                                                base::Time time) {
  if (!LazyOpen(kCreateIfNeeded))
    return false;
  lazy_last_access_times_[group_id] = time;
  return true;
}

bool AppCacheDatabase::CommitLazyLastAccessTimes() {
  if (lazy_last_access_times_.empty())
    return true;
  if (!LazyOpen(kDontCreate))
    return false;

  sql::Transaction transaction(db_.get());
  if (!transaction.Begin())
    return false;
  for (const auto& pair : lazy_last_access_times_) {
    static const char kSql[] =
        "UPDATE Groups SET last_access_time = ? WHERE group_id = ?";
    sql::Statement statement(db_->GetCachedStatement(SQL_FROM_HERE, kSql));
    statement.BindInt64(0, pair.second.ToInternalValue());  // time
    statement.BindInt64(1, pair.first);  // group_id
    statement.Run();
  }
  lazy_last_access_times_.clear();
  return transaction.Commit();
}

bool AppCacheDatabase::UpdateEvictionTimes(
    int64_t group_id,
    base::Time last_full_update_check_time,
    base::Time first_evictable_error_time) {
  if (!LazyOpen(kCreateIfNeeded))
    return false;

  static const char kSql[] =
      "UPDATE Groups"
      " SET last_full_update_check_time = ?, first_evictable_error_time = ?"
      " WHERE group_id = ?";
  sql::Statement statement(db_->GetCachedStatement(SQL_FROM_HERE, kSql));
  statement.BindInt64(0, last_full_update_check_time.ToInternalValue());
  statement.BindInt64(1, first_evictable_error_time.ToInternalValue());
  statement.BindInt64(2, group_id);
  return statement.Run();  // Will succeed even if group_id is invalid.
}

bool AppCacheDatabase::FindCache(int64_t cache_id, CacheRecord* record) {
  DCHECK(record);
  if (!LazyOpen(kDontCreate))
    return false;

  static const char kSql[] =
      "SELECT cache_id, group_id, online_wildcard, update_time, cache_size"
      " FROM Caches WHERE cache_id = ?";

  sql::Statement statement(db_->GetCachedStatement(SQL_FROM_HERE, kSql));
  statement.BindInt64(0, cache_id);

  if (!statement.Step())
    return false;

  ReadCacheRecord(statement, record);
  return true;
}

bool AppCacheDatabase::FindCacheForGroup(int64_t group_id,
                                         CacheRecord* record) {
  DCHECK(record);
  if (!LazyOpen(kDontCreate))
    return false;

  static const char kSql[] =
      "SELECT cache_id, group_id, online_wildcard, update_time, cache_size"
      "  FROM Caches WHERE group_id = ?";

  sql::Statement statement(db_->GetCachedStatement(SQL_FROM_HERE, kSql));
  statement.BindInt64(0, group_id);

  if (!statement.Step())
    return false;

  ReadCacheRecord(statement, record);
  return true;
}

bool AppCacheDatabase::FindCachesForOrigin(const url::Origin& origin,
                                           std::vector<CacheRecord>* records) {
  DCHECK(records);
  std::vector<GroupRecord> group_records;
  if (!FindGroupsForOrigin(origin, &group_records))
    return false;

  CacheRecord cache_record;
  for (const auto& record : group_records) {
    if (FindCacheForGroup(record.group_id, &cache_record))
      records->push_back(cache_record);
  }
  return true;
}

bool AppCacheDatabase::InsertCache(const CacheRecord* record) {
  if (!LazyOpen(kCreateIfNeeded))
    return false;

  static const char kSql[] =
      "INSERT INTO Caches (cache_id, group_id, online_wildcard,"
      "                    update_time, cache_size)"
      "  VALUES(?, ?, ?, ?, ?)";

  sql::Statement statement(db_->GetCachedStatement(SQL_FROM_HERE, kSql));
  statement.BindInt64(0, record->cache_id);
  statement.BindInt64(1, record->group_id);
  statement.BindBool(2, record->online_wildcard);
  statement.BindInt64(3, record->update_time.ToInternalValue());
  statement.BindInt64(4, record->cache_size);

  return statement.Run();
}

bool AppCacheDatabase::DeleteCache(int64_t cache_id) {
  if (!LazyOpen(kDontCreate))
    return false;

  static const char kSql[] = "DELETE FROM Caches WHERE cache_id = ?";

  sql::Statement statement(db_->GetCachedStatement(SQL_FROM_HERE, kSql));
  statement.BindInt64(0, cache_id);

  return statement.Run();
}

bool AppCacheDatabase::FindEntriesForCache(int64_t cache_id,
                                           std::vector<EntryRecord>* records) {
  DCHECK(records && records->empty());
  if (!LazyOpen(kDontCreate))
    return false;

  static const char kSql[] =
      "SELECT cache_id, url, flags, response_id, response_size FROM Entries"
      "  WHERE cache_id = ?";

  sql::Statement statement(db_->GetCachedStatement(SQL_FROM_HERE, kSql));
  statement.BindInt64(0, cache_id);

  while (statement.Step()) {
    records->push_back(EntryRecord());
    ReadEntryRecord(statement, &records->back());
    DCHECK(records->back().cache_id == cache_id);
  }

  return statement.Succeeded();
}

bool AppCacheDatabase::FindEntriesForUrl(
    const GURL& url, std::vector<EntryRecord>* records) {
  DCHECK(records && records->empty());
  if (!LazyOpen(kDontCreate))
    return false;

  static const char kSql[] =
      "SELECT cache_id, url, flags, response_id, response_size FROM Entries"
      "  WHERE url = ?";

  sql::Statement statement(db_->GetCachedStatement(SQL_FROM_HERE, kSql));
  statement.BindString(0, url.spec());

  while (statement.Step()) {
    records->push_back(EntryRecord());
    ReadEntryRecord(statement, &records->back());
    DCHECK(records->back().url == url);
  }

  return statement.Succeeded();
}

bool AppCacheDatabase::FindEntry(int64_t cache_id,
                                 const GURL& url,
                                 EntryRecord* record) {
  DCHECK(record);
  if (!LazyOpen(kDontCreate))
    return false;

  static const char kSql[] =
      "SELECT cache_id, url, flags, response_id, response_size FROM Entries"
      "  WHERE cache_id = ? AND url = ?";

  sql::Statement statement(db_->GetCachedStatement(SQL_FROM_HERE, kSql));
  statement.BindInt64(0, cache_id);
  statement.BindString(1, url.spec());

  if (!statement.Step())
    return false;

  ReadEntryRecord(statement, record);
  DCHECK(record->cache_id == cache_id);
  DCHECK(record->url == url);
  return true;
}

bool AppCacheDatabase::InsertEntry(const EntryRecord* record) {
  if (!LazyOpen(kCreateIfNeeded))
    return false;

  static const char kSql[] =
      "INSERT INTO Entries (cache_id, url, flags, response_id, response_size)"
      "  VALUES(?, ?, ?, ?, ?)";

  sql::Statement statement(db_->GetCachedStatement(SQL_FROM_HERE, kSql));
  statement.BindInt64(0, record->cache_id);
  statement.BindString(1, record->url.spec());
  statement.BindInt(2, record->flags);
  statement.BindInt64(3, record->response_id);
  statement.BindInt64(4, record->response_size);

  return statement.Run();
}

bool AppCacheDatabase::InsertEntryRecords(
    const std::vector<EntryRecord>& records) {
  if (records.empty())
    return true;
  sql::Transaction transaction(db_.get());
  if (!transaction.Begin())
    return false;
  for (const auto& record : records) {
    if (!InsertEntry(&record))
      return false;
  }
  return transaction.Commit();
}

bool AppCacheDatabase::DeleteEntriesForCache(int64_t cache_id) {
  if (!LazyOpen(kDontCreate))
    return false;

  static const char kSql[] = "DELETE FROM Entries WHERE cache_id = ?";

  sql::Statement statement(db_->GetCachedStatement(SQL_FROM_HERE, kSql));
  statement.BindInt64(0, cache_id);

  return statement.Run();
}

bool AppCacheDatabase::AddEntryFlags(const GURL& entry_url,
                                     int64_t cache_id,
                                     int additional_flags) {
  if (!LazyOpen(kDontCreate))
    return false;

  static const char kSql[] =
      "UPDATE Entries SET flags = flags | ? WHERE cache_id = ? AND url = ?";

  sql::Statement statement(db_->GetCachedStatement(SQL_FROM_HERE, kSql));
  statement.BindInt(0, additional_flags);
  statement.BindInt64(1, cache_id);
  statement.BindString(2, entry_url.spec());

  return statement.Run() && db_->GetLastChangeCount();
}

bool AppCacheDatabase::FindNamespacesForOrigin(
    const url::Origin& origin,
    std::vector<NamespaceRecord>* intercepts,
    std::vector<NamespaceRecord>* fallbacks) {
  DCHECK(intercepts && intercepts->empty());
  DCHECK(fallbacks && fallbacks->empty());
  if (!LazyOpen(kDontCreate))
    return false;

  static const char kSql[] =
      "SELECT cache_id, origin, type, namespace_url, target_url, is_pattern"
      "  FROM Namespaces WHERE origin = ?";

  sql::Statement statement(db_->GetCachedStatement(SQL_FROM_HERE, kSql));
  statement.BindString(0, SerializeOrigin(origin));

  ReadNamespaceRecords(&statement, intercepts, fallbacks);

  return statement.Succeeded();
}

bool AppCacheDatabase::FindNamespacesForCache(
    int64_t cache_id,
    std::vector<NamespaceRecord>* intercepts,
    std::vector<NamespaceRecord>* fallbacks) {
  DCHECK(intercepts && intercepts->empty());
  DCHECK(fallbacks && fallbacks->empty());
  if (!LazyOpen(kDontCreate))
    return false;

  static const char kSql[] =
      "SELECT cache_id, origin, type, namespace_url, target_url, is_pattern"
      "  FROM Namespaces WHERE cache_id = ?";

  sql::Statement statement(db_->GetCachedStatement(SQL_FROM_HERE, kSql));
  statement.BindInt64(0, cache_id);

  ReadNamespaceRecords(&statement, intercepts, fallbacks);

  return statement.Succeeded();
}

bool AppCacheDatabase::InsertNamespace(
    const NamespaceRecord* record) {
  if (!LazyOpen(kCreateIfNeeded))
    return false;

  static const char kSql[] =
      "INSERT INTO Namespaces"
      "  (cache_id, origin, type, namespace_url, target_url, is_pattern)"
      "  VALUES (?, ?, ?, ?, ?, ?)";

  sql::Statement statement(db_->GetCachedStatement(SQL_FROM_HERE, kSql));
  statement.BindInt64(0, record->cache_id);
  statement.BindString(1, SerializeOrigin(record->origin));
  statement.BindInt(2, record->namespace_.type);
  statement.BindString(3, record->namespace_.namespace_url.spec());
  statement.BindString(4, record->namespace_.target_url.spec());
  statement.BindBool(5, record->namespace_.is_pattern);
  return statement.Run();
}

bool AppCacheDatabase::InsertNamespaceRecords(
    const std::vector<NamespaceRecord>& records) {
  if (records.empty())
    return true;
  sql::Transaction transaction(db_.get());
  if (!transaction.Begin())
    return false;
  for (const auto& record : records) {
    if (!InsertNamespace(&record))
      return false;
  }
  return transaction.Commit();
}

bool AppCacheDatabase::DeleteNamespacesForCache(int64_t cache_id) {
  if (!LazyOpen(kDontCreate))
    return false;

  static const char kSql[] = "DELETE FROM Namespaces WHERE cache_id = ?";

  sql::Statement statement(db_->GetCachedStatement(SQL_FROM_HERE, kSql));
  statement.BindInt64(0, cache_id);

  return statement.Run();
}

bool AppCacheDatabase::FindOnlineWhiteListForCache(
    int64_t cache_id,
    std::vector<OnlineWhiteListRecord>* records) {
  DCHECK(records && records->empty());
  if (!LazyOpen(kDontCreate))
    return false;

  static const char kSql[] =
      "SELECT cache_id, namespace_url, is_pattern FROM OnlineWhiteLists"
      "  WHERE cache_id = ?";

  sql::Statement statement(db_->GetCachedStatement(SQL_FROM_HERE, kSql));
  statement.BindInt64(0, cache_id);

  while (statement.Step()) {
    records->push_back(OnlineWhiteListRecord());
    this->ReadOnlineWhiteListRecord(statement, &records->back());
    DCHECK(records->back().cache_id == cache_id);
  }
  return statement.Succeeded();
}

bool AppCacheDatabase::InsertOnlineWhiteList(
    const OnlineWhiteListRecord* record) {
  if (!LazyOpen(kCreateIfNeeded))
    return false;

  static const char kSql[] =
      "INSERT INTO OnlineWhiteLists (cache_id, namespace_url, is_pattern)"
      "  VALUES (?, ?, ?)";

  sql::Statement statement(db_->GetCachedStatement(SQL_FROM_HERE, kSql));
  statement.BindInt64(0, record->cache_id);
  statement.BindString(1, record->namespace_url.spec());
  statement.BindBool(2, record->is_pattern);

  return statement.Run();
}

bool AppCacheDatabase::InsertOnlineWhiteListRecords(
    const std::vector<OnlineWhiteListRecord>& records) {
  if (records.empty())
    return true;
  sql::Transaction transaction(db_.get());
  if (!transaction.Begin())
    return false;
  for (const auto& record : records) {
    if (!InsertOnlineWhiteList(&record))
      return false;
  }
  return transaction.Commit();
}

bool AppCacheDatabase::DeleteOnlineWhiteListForCache(int64_t cache_id) {
  if (!LazyOpen(kDontCreate))
    return false;

  static const char kSql[] = "DELETE FROM OnlineWhiteLists WHERE cache_id = ?";

  sql::Statement statement(db_->GetCachedStatement(SQL_FROM_HERE, kSql));
  statement.BindInt64(0, cache_id);

  return statement.Run();
}

bool AppCacheDatabase::GetDeletableResponseIds(
    std::vector<int64_t>* response_ids,
    int64_t max_rowid,
    int limit) {
  if (!LazyOpen(kDontCreate))
    return false;

  static const char kSql[] =
      "SELECT response_id FROM DeletableResponseIds "
      "  WHERE rowid <= ?"
      "  LIMIT ?";

  sql::Statement statement(db_->GetCachedStatement(SQL_FROM_HERE, kSql));
  statement.BindInt64(0, max_rowid);
  statement.BindInt64(1, limit);

  while (statement.Step())
    response_ids->push_back(statement.ColumnInt64(0));
  return statement.Succeeded();
}

bool AppCacheDatabase::InsertDeletableResponseIds(
    const std::vector<int64_t>& response_ids) {
  static const char kSql[] =
      "INSERT INTO DeletableResponseIds (response_id) VALUES (?)";
  return RunCachedStatementWithIds(SQL_FROM_HERE, kSql, response_ids);
}

bool AppCacheDatabase::DeleteDeletableResponseIds(
    const std::vector<int64_t>& response_ids) {
  static const char kSql[] =
      "DELETE FROM DeletableResponseIds WHERE response_id = ?";
  return RunCachedStatementWithIds(SQL_FROM_HERE, kSql, response_ids);
}

bool AppCacheDatabase::RunCachedStatementWithIds(
    sql::StatementID statement_id,
    const char* sql,
    const std::vector<int64_t>& ids) {
  DCHECK(sql);
  if (!LazyOpen(kCreateIfNeeded))
    return false;

  sql::Transaction transaction(db_.get());
  if (!transaction.Begin())
    return false;

  sql::Statement statement(db_->GetCachedStatement(statement_id, sql));

  for (const auto& id : ids) {
    statement.BindInt64(0, id);
    if (!statement.Run())
      return false;
    statement.Reset(true);
  }

  return transaction.Commit();
}

bool AppCacheDatabase::RunUniqueStatementWithInt64Result(const char* sql,
                                                         int64_t* result) {
  DCHECK(sql);
  sql::Statement statement(db_->GetUniqueStatement(sql));
  if (!statement.Step()) {
    return false;
  }
  *result = statement.ColumnInt64(0);
  return true;
}

bool AppCacheDatabase::FindResponseIdsForCacheHelper(
    int64_t cache_id,
    std::vector<int64_t>* ids_vector,
    std::set<int64_t>* ids_set) {
  DCHECK(ids_vector || ids_set);
  DCHECK(!(ids_vector && ids_set));
  if (!LazyOpen(kDontCreate))
    return false;

  static const char kSql[] =
      "SELECT response_id FROM Entries WHERE cache_id = ?";

  sql::Statement statement(db_->GetCachedStatement(SQL_FROM_HERE, kSql));

  statement.BindInt64(0, cache_id);
  while (statement.Step()) {
    int64_t id = statement.ColumnInt64(0);
    if (ids_set)
      ids_set->insert(id);
    else
      ids_vector->push_back(id);
  }

  return statement.Succeeded();
}

void AppCacheDatabase::ReadGroupRecord(
    const sql::Statement& statement, GroupRecord* record) {
  record->group_id = statement.ColumnInt64(0);
  record->origin = url::Origin::Create(GURL(statement.ColumnString(1)));
  record->manifest_url = GURL(statement.ColumnString(2));
  record->creation_time =
      base::Time::FromInternalValue(statement.ColumnInt64(3));

  const auto found = lazy_last_access_times_.find(record->group_id);
  if (found != lazy_last_access_times_.end()) {
    record->last_access_time = found->second;
  } else {
    record->last_access_time =
        base::Time::FromInternalValue(statement.ColumnInt64(4));
  }

  record->last_full_update_check_time =
      base::Time::FromInternalValue(statement.ColumnInt64(5));
  record->first_evictable_error_time =
      base::Time::FromInternalValue(statement.ColumnInt64(6));
}

void AppCacheDatabase::ReadCacheRecord(
    const sql::Statement& statement, CacheRecord* record) {
  record->cache_id = statement.ColumnInt64(0);
  record->group_id = statement.ColumnInt64(1);
  record->online_wildcard = statement.ColumnBool(2);
  record->update_time =
      base::Time::FromInternalValue(statement.ColumnInt64(3));
  record->cache_size = statement.ColumnInt64(4);
}

void AppCacheDatabase::ReadEntryRecord(
    const sql::Statement& statement, EntryRecord* record) {
  record->cache_id = statement.ColumnInt64(0);
  record->url = GURL(statement.ColumnString(1));
  record->flags = statement.ColumnInt(2);
  record->response_id = statement.ColumnInt64(3);
  record->response_size = statement.ColumnInt64(4);
}

void AppCacheDatabase::ReadNamespaceRecords(
      sql::Statement* statement,
      NamespaceRecordVector* intercepts,
      NamespaceRecordVector* fallbacks) {
  while (statement->Step()) {
    AppCacheNamespaceType type = static_cast<AppCacheNamespaceType>(
        statement->ColumnInt(2));
    NamespaceRecordVector* records =
        (type == APPCACHE_FALLBACK_NAMESPACE) ? fallbacks : intercepts;
    records->push_back(NamespaceRecord());
    ReadNamespaceRecord(statement, &records->back());
  }
}

void AppCacheDatabase::ReadNamespaceRecord(
    const sql::Statement* statement, NamespaceRecord* record) {
  record->cache_id = statement->ColumnInt64(0);
  record->origin = url::Origin::Create(GURL(statement->ColumnString(1)));
  record->namespace_.type =
      static_cast<AppCacheNamespaceType>(statement->ColumnInt(2));
  record->namespace_.namespace_url = GURL(statement->ColumnString(3));
  record->namespace_.target_url = GURL(statement->ColumnString(4));
  record->namespace_.is_pattern = statement->ColumnBool(5);
  DCHECK(record->namespace_.type == APPCACHE_FALLBACK_NAMESPACE ||
         record->namespace_.type == APPCACHE_INTERCEPT_NAMESPACE);
  // The APPCACHE_NETWORK_NAMESPACE are stored as OnlineWhiteListRecords.
}

void AppCacheDatabase::ReadOnlineWhiteListRecord(
    const sql::Statement& statement, OnlineWhiteListRecord* record) {
  record->cache_id = statement.ColumnInt64(0);
  record->namespace_url = GURL(statement.ColumnString(1));
  record->is_pattern = statement.ColumnBool(2);
}

bool AppCacheDatabase::LazyOpen(bool create_if_needed) {
  if (db_)
    return true;

  // If we tried and failed once, don't try again in the same session
  // to avoid creating an incoherent mess on disk.
  if (is_disabled_)
    return false;

  // Avoid creating a database at all if we can.
  bool use_in_memory_db = db_file_path_.empty();
  if (!create_if_needed &&
      (use_in_memory_db || !base::PathExists(db_file_path_))) {
    return false;
  }

  db_.reset(new sql::Connection);
  meta_table_.reset(new sql::MetaTable);

  db_->set_histogram_tag("AppCache");

  bool opened = false;
  if (use_in_memory_db) {
    opened = db_->OpenInMemory();
  } else if (!base::CreateDirectory(db_file_path_.DirName())) {
    LOG(ERROR) << "Failed to create appcache directory.";
  } else {
    opened = db_->Open(db_file_path_);
    if (opened)
      db_->Preload();
  }

  if (!opened || !db_->QuickIntegrityCheck() || !EnsureDatabaseVersion()) {
    LOG(ERROR) << "Failed to open the appcache database.";
    AppCacheHistograms::CountInitResult(
        AppCacheHistograms::SQL_DATABASE_ERROR);

    // We're unable to open the database. This is a fatal error
    // which we can't recover from. We try to handle it by deleting
    // the existing appcache data and starting with a clean slate in
    // this browser session.
    if (!use_in_memory_db && DeleteExistingAndCreateNewDatabase())
      return true;

    Disable();
    return false;
  }

  AppCacheHistograms::CountInitResult(AppCacheHistograms::INIT_OK);
  was_corruption_detected_ = false;
  db_->set_error_callback(
      base::Bind(&AppCacheDatabase::OnDatabaseError, base::Unretained(this)));
  return true;
}

bool AppCacheDatabase::EnsureDatabaseVersion() {
  if (!sql::MetaTable::DoesTableExist(db_.get()))
    return CreateSchema();

  if (!meta_table_->Init(db_.get(), kCurrentVersion, kCompatibleVersion))
    return false;

  if (meta_table_->GetCompatibleVersionNumber() > kCurrentVersion) {
    LOG(WARNING) << "AppCache database is too new.";
    return false;
  }

  std::string stored_flags;
  meta_table_->GetValue(kExperimentFlagsKey, &stored_flags);
  if (stored_flags != GetActiveExperimentFlags())
    return false;

  if (meta_table_->GetVersionNumber() < kCurrentVersion)
    return UpgradeSchema();

#ifndef NDEBUG
  DCHECK(sql::MetaTable::DoesTableExist(db_.get()));
  for (int i = 0; i < kTableCount; ++i) {
    DCHECK(db_->DoesTableExist(kTables[i].table_name));
  }
  for (int i = 0; i < kIndexCount; ++i) {
    DCHECK(db_->DoesIndexExist(kIndexes[i].index_name));
  }
#endif

  return true;
}

bool AppCacheDatabase::CreateSchema() {
  sql::Transaction transaction(db_.get());
  if (!transaction.Begin())
    return false;

  if (!meta_table_->Init(db_.get(), kCurrentVersion, kCompatibleVersion))
    return false;

  if (!meta_table_->SetValue(kExperimentFlagsKey,
                             GetActiveExperimentFlags())) {
    return false;
  }

  for (int i = 0; i < kTableCount; ++i) {
    if (!CreateTable(db_.get(), kTables[i]))
      return false;
  }

  for (int i = 0; i < kIndexCount; ++i) {
    if (!CreateIndex(db_.get(), kIndexes[i]))
      return false;
  }

  return transaction.Commit();
}

bool AppCacheDatabase::UpgradeSchema() {
#if defined(APPCACHE_USE_SIMPLE_CACHE)
  if (meta_table_->GetVersionNumber() < 6)
    return DeleteExistingAndCreateNewDatabase();
#endif
  if (meta_table_->GetVersionNumber() == 3) {
    // version 3 was pre 12/17/2011
    DCHECK_EQ(strcmp(kNamespacesTable, kTables[3].table_name), 0);
    DCHECK_EQ(strcmp(kNamespacesTable, kIndexes[6].table_name), 0);
    DCHECK_EQ(strcmp(kNamespacesTable, kIndexes[7].table_name), 0);
    DCHECK_EQ(strcmp(kNamespacesTable, kIndexes[8].table_name), 0);

    const TableInfo kNamespaceTable_v4 = {
        kNamespacesTable,
        "(cache_id INTEGER,"
        " origin TEXT,"  // intentionally not normalized
        " type INTEGER,"
        " namespace_url TEXT,"
        " target_url TEXT)"
    };

    // Migrate from the old FallbackNameSpaces to the newer Namespaces table,
    // but without the is_pattern column added in v5.
    sql::Transaction transaction(db_.get());
    if (!transaction.Begin() ||
        !CreateTable(db_.get(), kNamespaceTable_v4)) {
      return false;
    }

    // Move data from the old table to the new table, setting the
    // 'type' for all current records to the value for
    // APPCACHE_FALLBACK_NAMESPACE.
    DCHECK_EQ(0, static_cast<int>(APPCACHE_FALLBACK_NAMESPACE));
    if (!db_->Execute(
            "INSERT INTO Namespaces"
            "  SELECT cache_id, origin, 0, namespace_url, fallback_entry_url"
            "  FROM FallbackNameSpaces")) {
      return false;
    }

    // Drop the old table, indexes on that table are also removed by this.
    if (!db_->Execute("DROP TABLE FallbackNameSpaces"))
      return false;

    // Create new indexes.
    if (!CreateIndex(db_.get(), kIndexes[6]) ||
        !CreateIndex(db_.get(), kIndexes[7]) ||
        !CreateIndex(db_.get(), kIndexes[8])) {
      return false;
    }

    meta_table_->SetVersionNumber(4);
    meta_table_->SetCompatibleVersionNumber(4);
    if (!transaction.Commit())
      return false;
  }

  if (meta_table_->GetVersionNumber() == 4) {
    // version 4 pre 3/30/2013
    // Add the is_pattern column to the Namespaces and OnlineWhitelists tables.
    DCHECK_EQ(strcmp(kNamespacesTable, "Namespaces"), 0);
    sql::Transaction transaction(db_.get());
    if (!transaction.Begin())
      return false;
    if (!db_->Execute(
            "ALTER TABLE Namespaces ADD COLUMN"
            "  is_pattern INTEGER CHECK(is_pattern IN (0, 1))")) {
      return false;
    }
    if (!db_->Execute(
            "ALTER TABLE OnlineWhitelists ADD COLUMN"
            "  is_pattern INTEGER CHECK(is_pattern IN (0, 1))")) {
      return false;
    }
    meta_table_->SetVersionNumber(5);
    meta_table_->SetCompatibleVersionNumber(5);
    if (!transaction.Commit())
      return false;
  }

#if defined(APPCACHE_USE_SIMPLE_CACHE)
  // The schema version number was increased to 6 when we switched to the
  // SimpleCache for Android, but the SQL part of the schema is identical
  // to v5 on desktop chrome.
  if (meta_table_->GetVersionNumber() == 6) {
#else
  if (meta_table_->GetVersionNumber() == 5) {
#endif
    // Versions 5 and 6 were pre-July 2015.
    // Version 7 adds support for expiring caches that are failing to update.
    sql::Transaction transaction(db_.get());
    if (!transaction.Begin() ||
        !db_->Execute(
            "ALTER TABLE Groups ADD COLUMN"
            " last_full_update_check_time INTEGER") ||
        !db_->Execute(
            "ALTER TABLE Groups ADD COLUMN"
            " first_evictable_error_time INTEGER") ||
        !db_->Execute(
            "UPDATE Groups"
            " SET last_full_update_check_time ="
            "   (SELECT update_time FROM Caches"
            "    WHERE Caches.group_id = Groups.group_id)")) {
      return false;
    }
    meta_table_->SetVersionNumber(7);
    meta_table_->SetCompatibleVersionNumber(7);
    return transaction.Commit();
  }

  // If there is no upgrade path for the version on disk to the current
  // version, nuke everything and start over.
  return DeleteExistingAndCreateNewDatabase();
}

void AppCacheDatabase::ResetConnectionAndTables() {
  meta_table_.reset();
  db_.reset();
}

bool AppCacheDatabase::DeleteExistingAndCreateNewDatabase() {
  DCHECK(!db_file_path_.empty());
  DCHECK(base::PathExists(db_file_path_));
  VLOG(1) << "Deleting existing appcache data and starting over.";

  ResetConnectionAndTables();

  // This also deletes the disk cache data.
  base::FilePath directory = db_file_path_.DirName();
  if (!base::DeleteFile(directory, true))
    return false;

  // Make sure the steps above actually deleted things.
  if (base::PathExists(directory))
    return false;

  if (!base::CreateDirectory(directory))
    return false;

  // So we can't go recursive.
  if (is_recreating_)
    return false;

  base::AutoReset<bool> auto_reset(&is_recreating_, true);
  return LazyOpen(kCreateIfNeeded);
}

void AppCacheDatabase::OnDatabaseError(int err, sql::Statement* stmt) {
  was_corruption_detected_ |= sql::IsErrorCatastrophic(err);
  if (!db_->IsExpectedSqliteError(err))
    DLOG(ERROR) << db_->GetErrorMessage();
  // TODO: Maybe use non-catostrophic errors to trigger a full integrity check?
}

}  // namespace content
