[sql] Allow storing mmap status in a VIEW instead of meta table.

Some databases do not use the sql::MetaTable to handle versioning.
The mmap verification pass uses the [meta] table to store progress for
larger databases, and it uses detection of the [meta] table to
distinguish new/empty databases from existing databases.  For
non-[meta] databases, store the status in an MmapStatus view.

BUG=652359

Review-Url: https://codereview.chromium.org/2397753005
Cr-Commit-Position: refs/heads/master@{#430343}
diff --git a/sql/connection.cc b/sql/connection.cc
index 073b032..788329b 100644
--- a/sql/connection.cc
+++ b/sql/connection.cc
@@ -318,6 +318,7 @@
       needs_rollback_(false),
       in_memory_(false),
       poisoned_(false),
+      mmap_alt_status_(false),
       mmap_disabled_(false),
       mmap_enabled_(false),
       total_changes_at_last_release_(0),
@@ -840,6 +841,49 @@
   return debug_info;
 }
 
+bool Connection::GetMmapAltStatus(int64_t* status) {
+  // The [meta] version uses a missing table as a signal for a fresh database.
+  // That will not work for the view, which would not exist in either a new or
+  // an existing database.  A new database _should_ be only one page long, so
+  // just don't bother optimizing this case (start at offset 0).
+  // TODO(shess): Could the [meta] case also get simpler, then?
+  if (!DoesViewExist("MmapStatus")) {
+    *status = 0;
+    return true;
+  }
+
+  const char* kMmapStatusSql = "SELECT * FROM MmapStatus";
+  Statement s(GetUniqueStatement(kMmapStatusSql));
+  if (s.Step())
+    *status = s.ColumnInt64(0);
+  return s.Succeeded();
+}
+
+bool Connection::SetMmapAltStatus(int64_t status) {
+  if (!BeginTransaction())
+    return false;
+
+  // View may not exist on first run.
+  if (!Execute("DROP VIEW IF EXISTS MmapStatus")) {
+    RollbackTransaction();
+    return false;
+  }
+
+  // Views live in the schema, so they cannot be parameterized.  For an integer
+  // value, this construct should be safe from SQL injection, if the value
+  // becomes more complicated use "SELECT quote(?)" to generate a safe quoted
+  // value.
+  const std::string createViewSql =
+      base::StringPrintf("CREATE VIEW MmapStatus (value) AS SELECT %" PRId64,
+                         status);
+  if (!Execute(createViewSql.c_str())) {
+    RollbackTransaction();
+    return false;
+  }
+
+  return CommitTransaction();
+}
+
 size_t Connection::GetAppropriateMmapSize() {
   AssertIOAllowed();
 
@@ -854,27 +898,27 @@
   // percentile of Chrome databases in the wild, so this should be good.
   const size_t kMmapEverything = 256 * 1024 * 1024;
 
-  // If the database doesn't have a place to track progress, assume the best.
-  // This will happen when new databases are created, or if a database doesn't
-  // use a meta table.  sql::MetaTable::Init() will preload kMmapSuccess.
-  // TODO(shess): Databases not using meta include:
-  //   DOMStorageDatabase (localstorage)
-  //   ActivityDatabase (extensions activity log)
-  //   PredictorDatabase (prefetch and autocomplete predictor data)
-  //   SyncDirectory (sync metadata storage)
-  // For now, these all have mmap disabled to allow other databases to get the
-  // default-enable path.  sqlite-diag could be an alternative for all but
-  // DOMStorageDatabase, which creates many small databases.
-  // http://crbug.com/537742
-  if (!MetaTable::DoesTableExist(this)) {
-    RecordOneEvent(EVENT_MMAP_META_MISSING);
-    return kMmapEverything;
-  }
-
+  // Progress information is tracked in the [meta] table for databases which use
+  // sql::MetaTable, otherwise it is tracked in a special view.
+  // TODO(shess): Move all cases to the view implementation.
   int64_t mmap_ofs = 0;
-  if (!MetaTable::GetMmapStatus(this, &mmap_ofs)) {
-    RecordOneEvent(EVENT_MMAP_META_FAILURE_READ);
-    return 0;
+  if (mmap_alt_status_) {
+    if (!GetMmapAltStatus(&mmap_ofs)) {
+      RecordOneEvent(EVENT_MMAP_STATUS_FAILURE_READ);
+      return 0;
+    }
+  } else {
+    // If [meta] doesn't exist, yet, it's a new database, assume the best.
+    // sql::MetaTable::Init() will preload kMmapSuccess.
+    if (!MetaTable::DoesTableExist(this)) {
+      RecordOneEvent(EVENT_MMAP_META_MISSING);
+      return kMmapEverything;
+    }
+
+    if (!MetaTable::GetMmapStatus(this, &mmap_ofs)) {
+      RecordOneEvent(EVENT_MMAP_META_FAILURE_READ);
+      return 0;
+    }
   }
 
   // Database read failed in the past, don't memory map.
@@ -949,9 +993,16 @@
         event = EVENT_MMAP_FAILED_NEW;
       }
 
-      if (!MetaTable::SetMmapStatus(this, mmap_ofs)) {
-        RecordOneEvent(EVENT_MMAP_META_FAILURE_UPDATE);
-        return 0;
+      if (mmap_alt_status_) {
+        if (!SetMmapAltStatus(mmap_ofs)) {
+          RecordOneEvent(EVENT_MMAP_STATUS_FAILURE_UPDATE);
+          return 0;
+        }
+      } else {
+        if (!MetaTable::SetMmapStatus(this, mmap_ofs)) {
+          RecordOneEvent(EVENT_MMAP_META_FAILURE_UPDATE);
+          return 0;
+        }
       }
 
       RecordOneEvent(event);
@@ -1503,15 +1554,19 @@
   return true;
 }
 
-bool Connection::DoesTableExist(const char* table_name) const {
-  return DoesTableOrIndexExist(table_name, "table");
-}
-
 bool Connection::DoesIndexExist(const char* index_name) const {
-  return DoesTableOrIndexExist(index_name, "index");
+  return DoesSchemaItemExist(index_name, "index");
 }
 
-bool Connection::DoesTableOrIndexExist(
+bool Connection::DoesTableExist(const char* table_name) const {
+  return DoesSchemaItemExist(table_name, "table");
+}
+
+bool Connection::DoesViewExist(const char* view_name) const {
+  return DoesSchemaItemExist(view_name, "view");
+}
+
+bool Connection::DoesSchemaItemExist(
     const char* name, const char* type) const {
   const char* kSql =
       "SELECT name FROM sqlite_master WHERE type=? AND name=? COLLATE NOCASE";