Bug 1172077 - Merge tabs.db into browser.db. r=nalexander a=KWierso
authorAhmed Khalil <ahmedibrahimkhali>
Thu, 18 Jun 2015 15:05:42 -0700
changeset 280596 6b80a1818bf1c0a88015471a36d6d0bb27248ebc
parent 280595 95dc0052f4102c241163005987b8616964941a4c
child 280599 a598e3c30b1afe3e3c1e8ed3d10a656eaa0873c8
push id4932
push userjlund@mozilla.com
push dateMon, 10 Aug 2015 18:23:06 +0000
treeherdermozilla-beta@6dd5a4f5f745 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersnalexander, KWierso
bugs1172077
milestone41.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1172077 - Merge tabs.db into browser.db. r=nalexander a=KWierso This moves the tabs and clients table into the main per-profile browser.db database. It's a code savings but it also makes it easier to query against open tabs when inspecting parts of the browser database, like history and bookmarks.
mobile/android/base/db/BrowserDatabaseHelper.java
mobile/android/base/db/DBUtils.java
mobile/android/base/db/TabsProvider.java
--- a/mobile/android/base/db/BrowserDatabaseHelper.java
+++ b/mobile/android/base/db/BrowserDatabaseHelper.java
@@ -1,26 +1,31 @@
 /* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- */
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 package org.mozilla.gecko.db;
 
+import java.io.File;
+import java.io.IOException;
 import java.util.ArrayList;
 import java.util.List;
 
+import org.mozilla.gecko.GeckoProfile;
 import org.mozilla.gecko.R;
 import org.mozilla.gecko.db.BrowserContract.Bookmarks;
 import org.mozilla.gecko.db.BrowserContract.Combined;
 import org.mozilla.gecko.db.BrowserContract.Favicons;
 import org.mozilla.gecko.db.BrowserContract.History;
 import org.mozilla.gecko.db.BrowserContract.ReadingListItems;
 import org.mozilla.gecko.db.BrowserContract.SearchHistory;
 import org.mozilla.gecko.db.BrowserContract.Thumbnails;
+import org.mozilla.gecko.util.FileUtils;
+
 import static org.mozilla.gecko.db.DBUtils.qualifyColumn;
 
 import android.content.ContentValues;
 import android.content.Context;
 import android.database.Cursor;
 import android.database.DatabaseUtils;
 import android.database.SQLException;
 import android.database.sqlite.SQLiteDatabase;
@@ -29,26 +34,28 @@ import android.database.sqlite.SQLiteOpe
 import android.net.Uri;
 import android.os.Build;
 import android.util.Log;
 
 
 final class BrowserDatabaseHelper extends SQLiteOpenHelper {
     private static final String LOGTAG = "GeckoBrowserDBHelper";
 
-    public static final int DATABASE_VERSION = 23;
+    public static final int DATABASE_VERSION = 24;
     public static final String DATABASE_NAME = "browser.db";
 
     final protected Context mContext;
 
     static final String TABLE_BOOKMARKS = Bookmarks.TABLE_NAME;
     static final String TABLE_HISTORY = History.TABLE_NAME;
     static final String TABLE_FAVICONS = Favicons.TABLE_NAME;
     static final String TABLE_THUMBNAILS = Thumbnails.TABLE_NAME;
     static final String TABLE_READING_LIST = ReadingListItems.TABLE_NAME;
+    static final String TABLE_TABS = TabsProvider.TABLE_TABS;
+    static final String TABLE_CLIENTS = TabsProvider.TABLE_CLIENTS;
 
     static final String VIEW_COMBINED = Combined.VIEW_NAME;
     static final String VIEW_BOOKMARKS_WITH_FAVICONS = Bookmarks.VIEW_WITH_FAVICONS;
     static final String VIEW_HISTORY_WITH_FAVICONS = History.VIEW_WITH_FAVICONS;
     static final String VIEW_COMBINED_WITH_FAVICONS = Combined.VIEW_WITH_FAVICONS;
 
     static final String TABLE_BOOKMARKS_JOIN_FAVICONS = TABLE_BOOKMARKS + " LEFT OUTER JOIN " +
             TABLE_FAVICONS + " ON " + qualifyColumn(TABLE_BOOKMARKS, Bookmarks.FAVICON_ID) + " = " +
@@ -169,16 +176,64 @@ final class BrowserDatabaseHelper extend
 
         db.execSQL("CREATE VIEW IF NOT EXISTS " + VIEW_HISTORY_WITH_FAVICONS + " AS " +
                 "SELECT " + qualifyColumn(TABLE_HISTORY, "*") +
                 ", " + qualifyColumn(TABLE_FAVICONS, Favicons.DATA) + " AS " + History.FAVICON +
                 ", " + qualifyColumn(TABLE_FAVICONS, Favicons.URL) + " AS " + History.FAVICON_URL +
                 " FROM " + TABLE_HISTORY_JOIN_FAVICONS);
     }
 
+    private void createClientsTable(SQLiteDatabase db) {
+        debug("Creating " + TABLE_CLIENTS + " table");
+
+        // Table for client's name-guid mapping.
+        db.execSQL("CREATE TABLE " + TABLE_CLIENTS + "(" +
+                BrowserContract.Clients.GUID + " TEXT PRIMARY KEY," +
+                BrowserContract.Clients.NAME + " TEXT," +
+                BrowserContract.Clients.LAST_MODIFIED + " INTEGER," +
+                BrowserContract.Clients.DEVICE_TYPE + " TEXT" +
+                ");");
+
+        // Index on GUID.
+        db.execSQL("CREATE INDEX " + TabsProvider.INDEX_CLIENTS_GUID +
+                " ON " + TABLE_CLIENTS + "(" + BrowserContract.Clients.GUID + ")");
+    }
+
+    private void createTabsTable(SQLiteDatabase db) {
+        debug("Creating tabs.db: " + db.getPath());
+        debug("Creating " + TABLE_TABS + " table");
+
+        // Table for each tab on any client.
+        db.execSQL("CREATE TABLE " + TABLE_TABS + "(" +
+                BrowserContract.Tabs._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
+                BrowserContract.Tabs.CLIENT_GUID + " TEXT," +
+                BrowserContract.Tabs.TITLE + " TEXT," +
+                BrowserContract.Tabs.URL + " TEXT," +
+                BrowserContract.Tabs.HISTORY + " TEXT," +
+                BrowserContract.Tabs.FAVICON + " TEXT," +
+                BrowserContract.Tabs.LAST_USED + " INTEGER," +
+                BrowserContract.Tabs.POSITION + " INTEGER" +
+                ");");
+
+        // Indices on CLIENT_GUID and POSITION.
+        db.execSQL("CREATE INDEX " + TabsProvider.INDEX_TABS_GUID +
+                " ON " + TABLE_TABS + "(" + BrowserContract.Tabs.CLIENT_GUID + ")");
+        db.execSQL("CREATE INDEX " + TabsProvider.INDEX_TABS_POSITION +
+                " ON " + TABLE_TABS + "(" + BrowserContract.Tabs.POSITION + ")");
+    }
+
+    // Insert a client row for our local Fennec client.
+    private void createLocalClient(SQLiteDatabase db) {
+        debug("Inserting local Fennec client into " + TABLE_CLIENTS + " table");
+
+        ContentValues values = new ContentValues();
+        values.put(BrowserContract.Clients.LAST_MODIFIED, System.currentTimeMillis());
+        db.insertOrThrow(TABLE_CLIENTS, null, values);
+    }
+
     private void createCombinedViewOn19(SQLiteDatabase db) {
         /*
         The v19 combined view removes the redundant subquery from the v16
         combined view and reorders the columns as necessary to prevent this
         from breaking any code that might be referencing columns by index.
 
         The rows in the ensuing view are, in order:
 
@@ -281,31 +336,64 @@ final class BrowserDatabaseHelper extend
         for (Table table : BrowserProvider.sTables) {
             table.onCreate(db);
         }
 
         createBookmarksTable(db);
         createHistoryTable(db);
         createFaviconsTable(db);
         createThumbnailsTable(db);
+        createTabsTable(db);
+        createClientsTable(db);
+        createLocalClient(db);
 
         createBookmarksWithFaviconsView(db);
         createHistoryWithFaviconsView(db);
         createCombinedViewOn19(db);
 
         createOrUpdateSpecialFolder(db, Bookmarks.PLACES_FOLDER_GUID,
             R.string.bookmarks_folder_places, 0);
 
         createOrUpdateAllSpecialFolders(db);
         createSearchHistoryTable(db);
         createReadingListTable(db, TABLE_READING_LIST);
         didCreateCurrentReadingListTable = true;      // Mostly correct, in the absence of transactions.
         createReadingListIndices(db, TABLE_READING_LIST);
     }
 
+    /**
+     * Copies the tabs and clients tables out of the given tabs.db file and into the destinationDB.
+     *
+     * @param tabsDBFile Path to existing tabs.db.
+     * @param destinationDB The destination database.
+     */
+    public void copyTabsDB(File tabsDBFile, SQLiteDatabase destinationDB) {
+        createTabsTable(destinationDB);
+        createClientsTable(destinationDB);
+
+        SQLiteDatabase oldTabsDB = null;
+        try {
+            oldTabsDB = SQLiteDatabase.openDatabase(tabsDBFile.getPath(), null, SQLiteDatabase.OPEN_READONLY);
+
+            if (!DBUtils.copyTable(oldTabsDB, TABLE_CLIENTS, destinationDB, TABLE_CLIENTS)) {
+                Log.e(LOGTAG, "Failed to migrate table clients; ignoring.");
+            }
+            if (!DBUtils.copyTable(oldTabsDB, TABLE_TABS, destinationDB, TABLE_TABS)) {
+                Log.e(LOGTAG, "Failed to migrate table tabs; ignoring.");
+            }
+        } catch (Exception e) {
+            Log.e(LOGTAG, "Exception occurred while trying to copy from " + tabsDBFile.getPath() +
+                    " to " + destinationDB.getPath() + "; ignoring.", e);
+        } finally {
+            if (oldTabsDB != null) {
+                oldTabsDB.close();
+            }
+        }
+    }
+
     private void createSearchHistoryTable(SQLiteDatabase db) {
         debug("Creating " + SearchHistory.TABLE_NAME + " table");
 
         db.execSQL("CREATE TABLE " + SearchHistory.TABLE_NAME + "(" +
                     SearchHistory._ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " +
                     SearchHistory.QUERY + " TEXT UNIQUE NOT NULL, " +
                     SearchHistory.DATE_LAST_VISITED + " INTEGER, " +
                     SearchHistory.VISITS + " INTEGER ) ");
@@ -873,16 +961,36 @@ final class BrowserDatabaseHelper extend
 
         // Now switch these tables over and recreate the indices.
         db.execSQL("DROP TABLE " + TABLE_READING_LIST);
         db.execSQL("ALTER TABLE tmp_rl RENAME TO " + TABLE_READING_LIST);
 
         createReadingListIndices(db, TABLE_READING_LIST);
     }
 
+    private void upgradeDatabaseFrom23to24(SQLiteDatabase db) {
+        // Version 24 consolidates the tabs and clients table into browser.db.  Before, they lived in tabs.db.
+        // It's easier to copy the existing data than to arrange for Sync to re-populate it.
+        try {
+            final File oldTabsDBFile = new File(GeckoProfile.get(mContext).getDir(), "tabs.db");
+            copyTabsDB(oldTabsDBFile, db);
+        } catch (Exception e) {
+            Log.e(LOGTAG, "Got exception copying tabs and clients data from tabs.db to browser.db; ignoring.", e);
+        }
+
+        // Delete the database, the shared memory, and the log.
+        for (String filename : new String[] { "tabs.db", "tabs.db-shm", "tabs.db-wal" }) {
+            final File file = new File(GeckoProfile.get(mContext).getDir(), filename);
+            try {
+                FileUtils.delete(file);
+            } catch (Exception e) {
+                Log.e(LOGTAG, "Exception occurred while trying to delete " + file.getPath() + "; ignoring.", e);
+            }
+        }
+    }
 
     private void createV19CombinedView(SQLiteDatabase db) {
         db.execSQL("DROP VIEW IF EXISTS " + VIEW_COMBINED);
         db.execSQL("DROP VIEW IF EXISTS " + VIEW_COMBINED_WITH_FAVICONS);
 
         createCombinedViewOn19(db);
     }
 
@@ -945,16 +1053,20 @@ final class BrowserDatabaseHelper extend
 
                 case 22:
                     upgradeDatabaseFrom21to22(db);
                     break;
 
                 case 23:
                     upgradeDatabaseFrom22to23(db);
                     break;
+
+                case 24:
+                    upgradeDatabaseFrom23to24(db);
+                    break;
             }
         }
 
         for (Table table : BrowserProvider.sTables) {
             table.onUpgrade(db, oldVersion, newVersion);
         }
 
         // Delete the obsolete favicon database after all other upgrades complete.
--- a/mobile/android/base/db/DBUtils.java
+++ b/mobile/android/base/db/DBUtils.java
@@ -1,15 +1,16 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 package org.mozilla.gecko.db;
 
 import android.annotation.TargetApi;
+import android.database.DatabaseUtils;
 import android.database.sqlite.SQLiteDatabase;
 import android.database.sqlite.SQLiteStatement;
 import android.os.Build;
 import org.mozilla.gecko.AppConstants;
 import org.mozilla.gecko.GeckoAppShell;
 import org.mozilla.gecko.GeckoProfile;
 
 import android.content.ContentValues;
@@ -110,16 +111,60 @@ public class DBUtils {
         // If we needed to retry, but we succeeded, report that in telemetry.
         // Failures are indicated by a lower frequency of UNLOCKED than LOCKED.
         if (attempt > 1) {
             Telemetry.addToHistogram(HISTOGRAM_DATABASE_UNLOCKED, attempt - 1);
         }
     }
 
     /**
+     * Copies a table <b>between</b> database files.
+     *
+     * This method assumes that the source table and destination table already exist in the
+     * source and destination databases, respectively.
+     *
+     * The table is copied row-by-row in a single transaction.
+     *
+     * @param source The source database that the table will be copied from.
+     * @param sourceTableName The name of the source table.
+     * @param destination The destination database that the table will be copied to.
+     * @param destinationTableName The name of the destination table.
+     * @return true if all rows were copied; false otherwise.
+     */
+    public static boolean copyTable(SQLiteDatabase source, String sourceTableName,
+                                    SQLiteDatabase destination, String destinationTableName) {
+        Cursor cursor = null;
+        try {
+            destination.beginTransaction();
+
+            cursor = source.query(sourceTableName, null, null, null, null, null, null);
+            Log.d(LOGTAG, "Trying to copy " + cursor.getCount() + " rows from " + sourceTableName + " to " + destinationTableName);
+
+            final ContentValues contentValues = new ContentValues();
+            while (cursor.moveToNext()) {
+                contentValues.clear();
+                DatabaseUtils.cursorRowToContentValues(cursor, contentValues);
+                destination.insert(destinationTableName, null, contentValues);
+            }
+
+            destination.setTransactionSuccessful();
+            Log.d(LOGTAG, "Successfully copied " + cursor.getCount() + " rows from " + sourceTableName + " to " + destinationTableName);
+            return true;
+        } catch (Exception e) {
+            Log.w(LOGTAG, "Got exception copying rows from " + sourceTableName + " to " + destinationTableName + "; ignoring.", e);
+            return false;
+        } finally {
+            destination.endTransaction();
+            if (cursor != null) {
+                cursor.close();
+            }
+        }
+    }
+
+    /**
      * Verifies that 0-byte arrays aren't added as favicon or thumbnail data.
      * @param values        ContentValues of query
      * @param columnName    Name of data column to verify
      */
     public static void stripEmptyByteArray(ContentValues values, String columnName) {
         if (values.containsKey(columnName)) {
             byte[] data = values.getAsByteArray(columnName);
             if (data == null || data.length == 0) {
--- a/mobile/android/base/db/TabsProvider.java
+++ b/mobile/android/base/db/TabsProvider.java
@@ -18,21 +18,17 @@ import android.content.Context;
 import android.content.UriMatcher;
 import android.database.Cursor;
 import android.database.sqlite.SQLiteDatabase;
 import android.database.sqlite.SQLiteOpenHelper;
 import android.database.sqlite.SQLiteQueryBuilder;
 import android.net.Uri;
 import android.text.TextUtils;
 
-public class TabsProvider extends PerProfileDatabaseProvider<TabsProvider.TabsDatabaseHelper> {
-    static final String DATABASE_NAME = "tabs.db";
-
-    static final int DATABASE_VERSION = 3;
-
+public class TabsProvider extends SharedBrowserDatabaseProvider {
     static final String TABLE_TABS = "tabs";
     static final String TABLE_CLIENTS = "clients";
 
     static final int TABS = 600;
     static final int TABS_ID = 601;
     static final int CLIENTS = 602;
     static final int CLIENTS_ID = 603;
     static final int CLIENTS_RECENCY = 604;
@@ -95,118 +91,16 @@ public class TabsProvider extends PerPro
     private static final String projectColumn(String table, String column) {
         return table + "." + column;
     }
 
     private static final String selectColumn(String table, String column) {
         return projectColumn(table, column) + " = ?";
     }
 
-    final class TabsDatabaseHelper extends SQLiteOpenHelper {
-        public TabsDatabaseHelper(Context context, String databasePath) {
-            super(context, databasePath, null, DATABASE_VERSION);
-        }
-
-        @Override
-        public void onCreate(SQLiteDatabase db) {
-            debug("Creating tabs.db: " + db.getPath());
-            debug("Creating " + TABLE_TABS + " table");
-
-            // Table for each tab on any client.
-            db.execSQL("CREATE TABLE " + TABLE_TABS + "(" +
-                       Tabs._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
-                       Tabs.CLIENT_GUID + " TEXT," +
-                       Tabs.TITLE + " TEXT," +
-                       Tabs.URL + " TEXT," +
-                       Tabs.HISTORY + " TEXT," +
-                       Tabs.FAVICON + " TEXT," +
-                       Tabs.LAST_USED + " INTEGER," +
-                       Tabs.POSITION + " INTEGER" +
-                       ");");
-
-            // Indices on CLIENT_GUID and POSITION.
-            db.execSQL("CREATE INDEX " + INDEX_TABS_GUID +
-                       " ON " + TABLE_TABS + "(" + Tabs.CLIENT_GUID + ")");
-            db.execSQL("CREATE INDEX " + INDEX_TABS_POSITION +
-                       " ON " + TABLE_TABS + "(" + Tabs.POSITION + ")");
-
-            debug("Creating " + TABLE_CLIENTS + " table");
-
-            // Table for client's name-guid mapping.
-            db.execSQL("CREATE TABLE " + TABLE_CLIENTS + "(" +
-                       Clients.GUID + " TEXT PRIMARY KEY," +
-                       Clients.NAME + " TEXT," +
-                       Clients.LAST_MODIFIED + " INTEGER," +
-                       Clients.DEVICE_TYPE + " TEXT" +
-                       ");");
-
-            // Index on GUID.
-            db.execSQL("CREATE INDEX " + INDEX_CLIENTS_GUID +
-                       " ON " + TABLE_CLIENTS + "(" + Clients.GUID + ")");
-
-            createLocalClient(db);
-        }
-
-        // Insert a client row for our local Fennec client.
-        private void createLocalClient(SQLiteDatabase db) {
-            debug("Inserting local Fennec client into " + TABLE_CLIENTS + " table");
-
-            ContentValues values = new ContentValues();
-            values.put(BrowserContract.Clients.LAST_MODIFIED, System.currentTimeMillis());
-            db.insertOrThrow(TABLE_CLIENTS, null, values);
-        }
-
-        protected void upgradeDatabaseFrom2to3(SQLiteDatabase db) {
-            debug("Setting remote client device types to 'mobile' in " + TABLE_CLIENTS + " table");
-
-            // Add type to client, defaulting to mobile. This is correct for our
-            // local client; all remote clients will be updated by Sync.
-            db.execSQL("ALTER TABLE " + TABLE_CLIENTS + " ADD COLUMN " + BrowserContract.Clients.DEVICE_TYPE + " TEXT DEFAULT 'mobile'");
-        }
-
-        @Override
-        public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
-            debug("Upgrading tabs.db: " + db.getPath() + " from " +
-                  oldVersion + " to " + newVersion);
-
-            // We have to do incremental upgrades until we reach the current
-            // database schema version.
-            for (int v = oldVersion + 1; v <= newVersion; v++) {
-                switch(v) {
-                    case 2:
-                        createLocalClient(db);
-                        break;
-
-                    case 3:
-                        upgradeDatabaseFrom2to3(db);
-                        break;
-                 }
-             }
-        }
-
-        @Override
-        public void onOpen(SQLiteDatabase db) {
-            debug("Opening tabs.db: " + db.getPath());
-            db.rawQuery("PRAGMA synchronous=OFF", null).close();
-
-            if (shouldUseTransactions()) {
-                // Modern Android allows WAL to be enabled through a mode flag.
-                if (Versions.preJB) {
-                    db.enableWriteAheadLogging();
-                }
-                db.setLockingEnabled(false);
-                return;
-            }
-
-            // If we're not using transactions (in particular, prior to
-            // Honeycomb), then we can do some lesser optimizations.
-            db.rawQuery("PRAGMA journal_mode=PERSIST", null).close();
-        }
-    }
-
     @Override
     public String getType(Uri uri) {
         final int match = URI_MATCHER.match(uri);
 
         trace("Getting URI type: " + uri);
 
         switch (match) {
             case TABS:
@@ -429,19 +323,9 @@ public class TabsProvider extends PerPro
 
     int deleteValues(Uri uri, String selection, String[] selectionArgs, String table) {
         debug("Deleting tabs for URI: " + uri);
 
         final SQLiteDatabase db = getWritableDatabase(uri);
         beginWrite(db);
         return db.delete(table, selection, selectionArgs);
     }
-
-    @Override
-    protected TabsDatabaseHelper createDatabaseHelper(Context context, String databasePath) {
-        return new TabsDatabaseHelper(context, databasePath);
-    }
-
-    @Override
-    protected String getDatabaseName() {
-        return DATABASE_NAME;
-    }
 }