diff options
Diffstat (limited to 'toolkit/components/places/PlacesDBUtils.jsm')
-rw-r--r-- | toolkit/components/places/PlacesDBUtils.jsm | 1138 |
1 files changed, 1138 insertions, 0 deletions
diff --git a/toolkit/components/places/PlacesDBUtils.jsm b/toolkit/components/places/PlacesDBUtils.jsm new file mode 100644 index 000000000..4ac6ea261 --- /dev/null +++ b/toolkit/components/places/PlacesDBUtils.jsm @@ -0,0 +1,1138 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- + * vim: sw=2 ts=2 sts=2 expandtab filetype=javascript + * 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/. */ + +const Cc = Components.classes; +const Ci = Components.interfaces; +const Cr = Components.results; +const Cu = Components.utils; + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/PlacesUtils.jsm"); + +this.EXPORTED_SYMBOLS = [ "PlacesDBUtils" ]; + +// Constants + +const FINISHED_MAINTENANCE_TOPIC = "places-maintenance-finished"; + +const BYTES_PER_MEBIBYTE = 1048576; + +// Smart getters + +XPCOMUtils.defineLazyGetter(this, "DBConn", function() { + return PlacesUtils.history.QueryInterface(Ci.nsPIPlacesDatabase).DBConnection; +}); + +// PlacesDBUtils + +this.PlacesDBUtils = { + /** + * Executes a list of maintenance tasks. + * Once finished it will pass a array log to the callback attached to tasks. + * FINISHED_MAINTENANCE_TOPIC is notified through observer service on finish. + * + * @param aTasks + * Tasks object to execute. + */ + _executeTasks: function PDBU__executeTasks(aTasks) + { + if (PlacesDBUtils._isShuttingDown) { + aTasks.log("- We are shutting down. Will not schedule the tasks."); + aTasks.clear(); + } + + let task = aTasks.pop(); + if (task) { + task.call(PlacesDBUtils, aTasks); + } + else { + // All tasks have been completed. + // Telemetry the time it took for maintenance, if a start time exists. + if (aTasks._telemetryStart) { + Services.telemetry.getHistogramById("PLACES_IDLE_MAINTENANCE_TIME_MS") + .add(Date.now() - aTasks._telemetryStart); + aTasks._telemetryStart = 0; + } + + if (aTasks.callback) { + let scope = aTasks.scope || Cu.getGlobalForObject(aTasks.callback); + aTasks.callback.call(scope, aTasks.messages); + } + + // Notify observers that maintenance finished. + Services.obs.notifyObservers(null, FINISHED_MAINTENANCE_TOPIC, null); + } + }, + + _isShuttingDown : false, + shutdown: function PDBU_shutdown() { + PlacesDBUtils._isShuttingDown = true; + }, + + /** + * Executes integrity check and common maintenance tasks. + * + * @param [optional] aCallback + * Callback to be invoked when done. The callback will get a array + * of log messages. + * @param [optional] aScope + * Scope for the callback. + */ + maintenanceOnIdle: function PDBU_maintenanceOnIdle(aCallback, aScope) + { + let tasks = new Tasks([ + this.checkIntegrity + , this.checkCoherence + , this._refreshUI + ]); + tasks._telemetryStart = Date.now(); + tasks.callback = function() { + Services.prefs.setIntPref("places.database.lastMaintenance", + parseInt(Date.now() / 1000)); + if (aCallback) + aCallback(); + } + tasks.scope = aScope; + this._executeTasks(tasks); + }, + + /** + * Executes integrity check, common and advanced maintenance tasks (like + * expiration and vacuum). Will also collect statistics on the database. + * + * @param [optional] aCallback + * Callback to be invoked when done. The callback will get a array + * of log messages. + * @param [optional] aScope + * Scope for the callback. + */ + checkAndFixDatabase: function PDBU_checkAndFixDatabase(aCallback, aScope) + { + let tasks = new Tasks([ + this.checkIntegrity + , this.checkCoherence + , this.expire + , this.vacuum + , this.stats + , this._refreshUI + ]); + tasks.callback = aCallback; + tasks.scope = aScope; + this._executeTasks(tasks); + }, + + /** + * Forces a full refresh of Places views. + * + * @param [optional] aTasks + * Tasks object to execute. + */ + _refreshUI: function PDBU__refreshUI(aTasks) + { + let tasks = new Tasks(aTasks); + + // Send batch update notifications to update the UI. + PlacesUtils.history.runInBatchMode({ + runBatched: function (aUserData) {} + }, null); + PlacesDBUtils._executeTasks(tasks); + }, + + _handleError: function PDBU__handleError(aError) + { + Cu.reportError("Async statement execution returned with '" + + aError.result + "', '" + aError.message + "'"); + }, + + /** + * Tries to execute a REINDEX on the database. + * + * @param [optional] aTasks + * Tasks object to execute. + */ + reindex: function PDBU_reindex(aTasks) + { + let tasks = new Tasks(aTasks); + tasks.log("> Reindex"); + + let stmt = DBConn.createAsyncStatement("REINDEX"); + stmt.executeAsync({ + handleError: PlacesDBUtils._handleError, + handleResult: function () {}, + + handleCompletion: function (aReason) + { + if (aReason == Ci.mozIStorageStatementCallback.REASON_FINISHED) { + tasks.log("+ The database has been reindexed"); + } + else { + tasks.log("- Unable to reindex database"); + } + + PlacesDBUtils._executeTasks(tasks); + } + }); + stmt.finalize(); + }, + + /** + * Checks integrity but does not try to fix the database through a reindex. + * + * @param [optional] aTasks + * Tasks object to execute. + */ + _checkIntegritySkipReindex: function PDBU__checkIntegritySkipReindex(aTasks) { + return this.checkIntegrity(aTasks, true); + }, + + /** + * Checks integrity and tries to fix the database through a reindex. + * + * @param [optional] aTasks + * Tasks object to execute. + * @param [optional] aSkipdReindex + * Whether to try to reindex database or not. + */ + checkIntegrity: function PDBU_checkIntegrity(aTasks, aSkipReindex) + { + let tasks = new Tasks(aTasks); + tasks.log("> Integrity check"); + + // Run a integrity check, but stop at the first error. + let stmt = DBConn.createAsyncStatement("PRAGMA integrity_check(1)"); + stmt.executeAsync({ + handleError: PlacesDBUtils._handleError, + + _corrupt: false, + handleResult: function (aResultSet) + { + let row = aResultSet.getNextRow(); + this._corrupt = row.getResultByIndex(0) != "ok"; + }, + + handleCompletion: function (aReason) + { + if (aReason == Ci.mozIStorageStatementCallback.REASON_FINISHED) { + if (this._corrupt) { + tasks.log("- The database is corrupt"); + if (aSkipReindex) { + tasks.log("- Unable to fix corruption, database will be replaced on next startup"); + Services.prefs.setBoolPref("places.database.replaceOnStartup", true); + tasks.clear(); + } + else { + // Try to reindex, this often fixed simple indices corruption. + // We insert from the top of the queue, they will run inverse. + tasks.push(PlacesDBUtils._checkIntegritySkipReindex); + tasks.push(PlacesDBUtils.reindex); + } + } + else { + tasks.log("+ The database is sane"); + } + } + else { + tasks.log("- Unable to check database status"); + tasks.clear(); + } + + PlacesDBUtils._executeTasks(tasks); + } + }); + stmt.finalize(); + }, + + /** + * Checks data coherence and tries to fix most common errors. + * + * @param [optional] aTasks + * Tasks object to execute. + */ + checkCoherence: function PDBU_checkCoherence(aTasks) + { + let tasks = new Tasks(aTasks); + tasks.log("> Coherence check"); + + let stmts = PlacesDBUtils._getBoundCoherenceStatements(); + DBConn.executeAsync(stmts, stmts.length, { + handleError: PlacesDBUtils._handleError, + handleResult: function () {}, + + handleCompletion: function (aReason) + { + if (aReason == Ci.mozIStorageStatementCallback.REASON_FINISHED) { + tasks.log("+ The database is coherent"); + } + else { + tasks.log("- Unable to check database coherence"); + tasks.clear(); + } + + PlacesDBUtils._executeTasks(tasks); + } + }); + stmts.forEach(aStmt => aStmt.finalize()); + }, + + _getBoundCoherenceStatements: function PDBU__getBoundCoherenceStatements() + { + let cleanupStatements = []; + + // MOZ_ANNO_ATTRIBUTES + // A.1 remove obsolete annotations from moz_annos. + // The 'weave0' idiom exploits character ordering (0 follows /) to + // efficiently select all annos with a 'weave/' prefix. + let deleteObsoleteAnnos = DBConn.createAsyncStatement( + `DELETE FROM moz_annos + WHERE type = 4 + OR anno_attribute_id IN ( + SELECT id FROM moz_anno_attributes + WHERE name BETWEEN 'weave/' AND 'weave0' + )`); + cleanupStatements.push(deleteObsoleteAnnos); + + // A.2 remove obsolete annotations from moz_items_annos. + let deleteObsoleteItemsAnnos = DBConn.createAsyncStatement( + `DELETE FROM moz_items_annos + WHERE type = 4 + OR anno_attribute_id IN ( + SELECT id FROM moz_anno_attributes + WHERE name = 'sync/children' + OR name = 'placesInternal/GUID' + OR name BETWEEN 'weave/' AND 'weave0' + )`); + cleanupStatements.push(deleteObsoleteItemsAnnos); + + // A.3 remove unused attributes. + let deleteUnusedAnnoAttributes = DBConn.createAsyncStatement( + `DELETE FROM moz_anno_attributes WHERE id IN ( + SELECT id FROM moz_anno_attributes n + WHERE NOT EXISTS + (SELECT id FROM moz_annos WHERE anno_attribute_id = n.id LIMIT 1) + AND NOT EXISTS + (SELECT id FROM moz_items_annos WHERE anno_attribute_id = n.id LIMIT 1) + )`); + cleanupStatements.push(deleteUnusedAnnoAttributes); + + // MOZ_ANNOS + // B.1 remove annos with an invalid attribute + let deleteInvalidAttributeAnnos = DBConn.createAsyncStatement( + `DELETE FROM moz_annos WHERE id IN ( + SELECT id FROM moz_annos a + WHERE NOT EXISTS + (SELECT id FROM moz_anno_attributes + WHERE id = a.anno_attribute_id LIMIT 1) + )`); + cleanupStatements.push(deleteInvalidAttributeAnnos); + + // B.2 remove orphan annos + let deleteOrphanAnnos = DBConn.createAsyncStatement( + `DELETE FROM moz_annos WHERE id IN ( + SELECT id FROM moz_annos a + WHERE NOT EXISTS + (SELECT id FROM moz_places WHERE id = a.place_id LIMIT 1) + )`); + cleanupStatements.push(deleteOrphanAnnos); + + // Bookmarks roots + // C.1 fix missing Places root + // Bug 477739 shows a case where the root could be wrongly removed + // due to an endianness issue. We try to fix broken roots here. + let selectPlacesRoot = DBConn.createStatement( + "SELECT id FROM moz_bookmarks WHERE id = :places_root"); + selectPlacesRoot.params["places_root"] = PlacesUtils.placesRootId; + if (!selectPlacesRoot.executeStep()) { + // We are missing the root, try to recreate it. + let createPlacesRoot = DBConn.createAsyncStatement( + `INSERT INTO moz_bookmarks (id, type, fk, parent, position, title, + guid) + VALUES (:places_root, 2, NULL, 0, 0, :title, :guid)`); + createPlacesRoot.params["places_root"] = PlacesUtils.placesRootId; + createPlacesRoot.params["title"] = ""; + createPlacesRoot.params["guid"] = PlacesUtils.bookmarks.rootGuid; + cleanupStatements.push(createPlacesRoot); + + // Now ensure that other roots are children of Places root. + let fixPlacesRootChildren = DBConn.createAsyncStatement( + `UPDATE moz_bookmarks SET parent = :places_root WHERE guid IN + ( :menuGuid, :toolbarGuid, :unfiledGuid, :tagsGuid )`); + fixPlacesRootChildren.params["places_root"] = PlacesUtils.placesRootId; + fixPlacesRootChildren.params["menuGuid"] = PlacesUtils.bookmarks.menuGuid; + fixPlacesRootChildren.params["toolbarGuid"] = PlacesUtils.bookmarks.toolbarGuid; + fixPlacesRootChildren.params["unfiledGuid"] = PlacesUtils.bookmarks.unfiledGuid; + fixPlacesRootChildren.params["tagsGuid"] = PlacesUtils.bookmarks.tagsGuid; + cleanupStatements.push(fixPlacesRootChildren); + } + selectPlacesRoot.finalize(); + + // C.2 fix roots titles + // some alpha version has wrong roots title, and this also fixes them if + // locale has changed. + let updateRootTitleSql = `UPDATE moz_bookmarks SET title = :title + WHERE id = :root_id AND title <> :title`; + // root + let fixPlacesRootTitle = DBConn.createAsyncStatement(updateRootTitleSql); + fixPlacesRootTitle.params["root_id"] = PlacesUtils.placesRootId; + fixPlacesRootTitle.params["title"] = ""; + cleanupStatements.push(fixPlacesRootTitle); + // bookmarks menu + let fixBookmarksMenuTitle = DBConn.createAsyncStatement(updateRootTitleSql); + fixBookmarksMenuTitle.params["root_id"] = PlacesUtils.bookmarksMenuFolderId; + fixBookmarksMenuTitle.params["title"] = + PlacesUtils.getString("BookmarksMenuFolderTitle"); + cleanupStatements.push(fixBookmarksMenuTitle); + // bookmarks toolbar + let fixBookmarksToolbarTitle = DBConn.createAsyncStatement(updateRootTitleSql); + fixBookmarksToolbarTitle.params["root_id"] = PlacesUtils.toolbarFolderId; + fixBookmarksToolbarTitle.params["title"] = + PlacesUtils.getString("BookmarksToolbarFolderTitle"); + cleanupStatements.push(fixBookmarksToolbarTitle); + // unsorted bookmarks + let fixUnsortedBookmarksTitle = DBConn.createAsyncStatement(updateRootTitleSql); + fixUnsortedBookmarksTitle.params["root_id"] = PlacesUtils.unfiledBookmarksFolderId; + fixUnsortedBookmarksTitle.params["title"] = + PlacesUtils.getString("OtherBookmarksFolderTitle"); + cleanupStatements.push(fixUnsortedBookmarksTitle); + // tags + let fixTagsRootTitle = DBConn.createAsyncStatement(updateRootTitleSql); + fixTagsRootTitle.params["root_id"] = PlacesUtils.tagsFolderId; + fixTagsRootTitle.params["title"] = + PlacesUtils.getString("TagsFolderTitle"); + cleanupStatements.push(fixTagsRootTitle); + + // MOZ_BOOKMARKS + // D.1 remove items without a valid place + // if fk IS NULL we fix them in D.7 + let deleteNoPlaceItems = DBConn.createAsyncStatement( + `DELETE FROM moz_bookmarks WHERE guid NOT IN ( + :rootGuid, :menuGuid, :toolbarGuid, :unfiledGuid, :tagsGuid /* skip roots */ + ) AND id IN ( + SELECT b.id FROM moz_bookmarks b + WHERE fk NOT NULL AND b.type = :bookmark_type + AND NOT EXISTS (SELECT url FROM moz_places WHERE id = b.fk LIMIT 1) + )`); + deleteNoPlaceItems.params["bookmark_type"] = PlacesUtils.bookmarks.TYPE_BOOKMARK; + deleteNoPlaceItems.params["rootGuid"] = PlacesUtils.bookmarks.rootGuid; + deleteNoPlaceItems.params["menuGuid"] = PlacesUtils.bookmarks.menuGuid; + deleteNoPlaceItems.params["toolbarGuid"] = PlacesUtils.bookmarks.toolbarGuid; + deleteNoPlaceItems.params["unfiledGuid"] = PlacesUtils.bookmarks.unfiledGuid; + deleteNoPlaceItems.params["tagsGuid"] = PlacesUtils.bookmarks.tagsGuid; + cleanupStatements.push(deleteNoPlaceItems); + + // D.2 remove items that are not uri bookmarks from tag containers + let deleteBogusTagChildren = DBConn.createAsyncStatement( + `DELETE FROM moz_bookmarks WHERE guid NOT IN ( + :rootGuid, :menuGuid, :toolbarGuid, :unfiledGuid, :tagsGuid /* skip roots */ + ) AND id IN ( + SELECT b.id FROM moz_bookmarks b + WHERE b.parent IN + (SELECT id FROM moz_bookmarks WHERE parent = :tags_folder) + AND b.type <> :bookmark_type + )`); + deleteBogusTagChildren.params["tags_folder"] = PlacesUtils.tagsFolderId; + deleteBogusTagChildren.params["bookmark_type"] = PlacesUtils.bookmarks.TYPE_BOOKMARK; + deleteBogusTagChildren.params["rootGuid"] = PlacesUtils.bookmarks.rootGuid; + deleteBogusTagChildren.params["menuGuid"] = PlacesUtils.bookmarks.menuGuid; + deleteBogusTagChildren.params["toolbarGuid"] = PlacesUtils.bookmarks.toolbarGuid; + deleteBogusTagChildren.params["unfiledGuid"] = PlacesUtils.bookmarks.unfiledGuid; + deleteBogusTagChildren.params["tagsGuid"] = PlacesUtils.bookmarks.tagsGuid; + cleanupStatements.push(deleteBogusTagChildren); + + // D.3 remove empty tags + let deleteEmptyTags = DBConn.createAsyncStatement( + `DELETE FROM moz_bookmarks WHERE guid NOT IN ( + :rootGuid, :menuGuid, :toolbarGuid, :unfiledGuid, :tagsGuid /* skip roots */ + ) AND id IN ( + SELECT b.id FROM moz_bookmarks b + WHERE b.id IN + (SELECT id FROM moz_bookmarks WHERE parent = :tags_folder) + AND NOT EXISTS + (SELECT id from moz_bookmarks WHERE parent = b.id LIMIT 1) + )`); + deleteEmptyTags.params["tags_folder"] = PlacesUtils.tagsFolderId; + deleteEmptyTags.params["rootGuid"] = PlacesUtils.bookmarks.rootGuid; + deleteEmptyTags.params["menuGuid"] = PlacesUtils.bookmarks.menuGuid; + deleteEmptyTags.params["toolbarGuid"] = PlacesUtils.bookmarks.toolbarGuid; + deleteEmptyTags.params["unfiledGuid"] = PlacesUtils.bookmarks.unfiledGuid; + deleteEmptyTags.params["tagsGuid"] = PlacesUtils.bookmarks.tagsGuid; + cleanupStatements.push(deleteEmptyTags); + + // D.4 move orphan items to unsorted folder + let fixOrphanItems = DBConn.createAsyncStatement( + `UPDATE moz_bookmarks SET parent = :unsorted_folder WHERE guid NOT IN ( + :rootGuid, :menuGuid, :toolbarGuid, :unfiledGuid, :tagsGuid /* skip roots */ + ) AND id IN ( + SELECT b.id FROM moz_bookmarks b + WHERE NOT EXISTS + (SELECT id FROM moz_bookmarks WHERE id = b.parent LIMIT 1) + )`); + fixOrphanItems.params["unsorted_folder"] = PlacesUtils.unfiledBookmarksFolderId; + fixOrphanItems.params["rootGuid"] = PlacesUtils.bookmarks.rootGuid; + fixOrphanItems.params["menuGuid"] = PlacesUtils.bookmarks.menuGuid; + fixOrphanItems.params["toolbarGuid"] = PlacesUtils.bookmarks.toolbarGuid; + fixOrphanItems.params["unfiledGuid"] = PlacesUtils.bookmarks.unfiledGuid; + fixOrphanItems.params["tagsGuid"] = PlacesUtils.bookmarks.tagsGuid; + cleanupStatements.push(fixOrphanItems); + + // D.6 fix wrong item types + // Folders and separators should not have an fk. + // If they have a valid fk convert them to bookmarks. Later in D.9 we + // will move eventual children to unsorted bookmarks. + let fixBookmarksAsFolders = DBConn.createAsyncStatement( + `UPDATE moz_bookmarks SET type = :bookmark_type WHERE guid NOT IN ( + :rootGuid, :menuGuid, :toolbarGuid, :unfiledGuid, :tagsGuid /* skip roots */ + ) AND id IN ( + SELECT id FROM moz_bookmarks b + WHERE type IN (:folder_type, :separator_type) + AND fk NOTNULL + )`); + fixBookmarksAsFolders.params["bookmark_type"] = PlacesUtils.bookmarks.TYPE_BOOKMARK; + fixBookmarksAsFolders.params["folder_type"] = PlacesUtils.bookmarks.TYPE_FOLDER; + fixBookmarksAsFolders.params["separator_type"] = PlacesUtils.bookmarks.TYPE_SEPARATOR; + fixBookmarksAsFolders.params["rootGuid"] = PlacesUtils.bookmarks.rootGuid; + fixBookmarksAsFolders.params["menuGuid"] = PlacesUtils.bookmarks.menuGuid; + fixBookmarksAsFolders.params["toolbarGuid"] = PlacesUtils.bookmarks.toolbarGuid; + fixBookmarksAsFolders.params["unfiledGuid"] = PlacesUtils.bookmarks.unfiledGuid; + fixBookmarksAsFolders.params["tagsGuid"] = PlacesUtils.bookmarks.tagsGuid; + cleanupStatements.push(fixBookmarksAsFolders); + + // D.7 fix wrong item types + // Bookmarks should have an fk, if they don't have any, convert them to + // folders. + let fixFoldersAsBookmarks = DBConn.createAsyncStatement( + `UPDATE moz_bookmarks SET type = :folder_type WHERE guid NOT IN ( + :rootGuid, :menuGuid, :toolbarGuid, :unfiledGuid, :tagsGuid /* skip roots */ + ) AND id IN ( + SELECT id FROM moz_bookmarks b + WHERE type = :bookmark_type + AND fk IS NULL + )`); + fixFoldersAsBookmarks.params["bookmark_type"] = PlacesUtils.bookmarks.TYPE_BOOKMARK; + fixFoldersAsBookmarks.params["folder_type"] = PlacesUtils.bookmarks.TYPE_FOLDER; + fixFoldersAsBookmarks.params["rootGuid"] = PlacesUtils.bookmarks.rootGuid; + fixFoldersAsBookmarks.params["menuGuid"] = PlacesUtils.bookmarks.menuGuid; + fixFoldersAsBookmarks.params["toolbarGuid"] = PlacesUtils.bookmarks.toolbarGuid; + fixFoldersAsBookmarks.params["unfiledGuid"] = PlacesUtils.bookmarks.unfiledGuid; + fixFoldersAsBookmarks.params["tagsGuid"] = PlacesUtils.bookmarks.tagsGuid; + cleanupStatements.push(fixFoldersAsBookmarks); + + // D.9 fix wrong parents + // Items cannot have separators or other bookmarks + // as parent, if they have bad parent move them to unsorted bookmarks. + let fixInvalidParents = DBConn.createAsyncStatement( + `UPDATE moz_bookmarks SET parent = :unsorted_folder WHERE guid NOT IN ( + :rootGuid, :menuGuid, :toolbarGuid, :unfiledGuid, :tagsGuid /* skip roots */ + ) AND id IN ( + SELECT id FROM moz_bookmarks b + WHERE EXISTS + (SELECT id FROM moz_bookmarks WHERE id = b.parent + AND type IN (:bookmark_type, :separator_type) + LIMIT 1) + )`); + fixInvalidParents.params["unsorted_folder"] = PlacesUtils.unfiledBookmarksFolderId; + fixInvalidParents.params["bookmark_type"] = PlacesUtils.bookmarks.TYPE_BOOKMARK; + fixInvalidParents.params["separator_type"] = PlacesUtils.bookmarks.TYPE_SEPARATOR; + fixInvalidParents.params["rootGuid"] = PlacesUtils.bookmarks.rootGuid; + fixInvalidParents.params["menuGuid"] = PlacesUtils.bookmarks.menuGuid; + fixInvalidParents.params["toolbarGuid"] = PlacesUtils.bookmarks.toolbarGuid; + fixInvalidParents.params["unfiledGuid"] = PlacesUtils.bookmarks.unfiledGuid; + fixInvalidParents.params["tagsGuid"] = PlacesUtils.bookmarks.tagsGuid; + cleanupStatements.push(fixInvalidParents); + + // D.10 recalculate positions + // This requires multiple related statements. + // We can detect a folder with bad position values comparing the sum of + // all distinct position values (+1 since position is 0-based) with the + // triangular numbers obtained by the number of children (n). + // SUM(DISTINCT position + 1) == (n * (n + 1) / 2). + cleanupStatements.push(DBConn.createAsyncStatement( + `CREATE TEMP TABLE IF NOT EXISTS moz_bm_reindex_temp ( + id INTEGER PRIMARY_KEY + , parent INTEGER + , position INTEGER + )` + )); + cleanupStatements.push(DBConn.createAsyncStatement( + `INSERT INTO moz_bm_reindex_temp + SELECT id, parent, 0 + FROM moz_bookmarks b + WHERE parent IN ( + SELECT parent + FROM moz_bookmarks + GROUP BY parent + HAVING (SUM(DISTINCT position + 1) - (count(*) * (count(*) + 1) / 2)) <> 0 + ) + ORDER BY parent ASC, position ASC, ROWID ASC` + )); + cleanupStatements.push(DBConn.createAsyncStatement( + `CREATE INDEX IF NOT EXISTS moz_bm_reindex_temp_index + ON moz_bm_reindex_temp(parent)` + )); + cleanupStatements.push(DBConn.createAsyncStatement( + `UPDATE moz_bm_reindex_temp SET position = ( + ROWID - (SELECT MIN(t.ROWID) FROM moz_bm_reindex_temp t + WHERE t.parent = moz_bm_reindex_temp.parent) + )` + )); + cleanupStatements.push(DBConn.createAsyncStatement( + `CREATE TEMP TRIGGER IF NOT EXISTS moz_bm_reindex_temp_trigger + BEFORE DELETE ON moz_bm_reindex_temp + FOR EACH ROW + BEGIN + UPDATE moz_bookmarks SET position = OLD.position WHERE id = OLD.id; + END` + )); + cleanupStatements.push(DBConn.createAsyncStatement( + "DELETE FROM moz_bm_reindex_temp " + )); + cleanupStatements.push(DBConn.createAsyncStatement( + "DROP INDEX moz_bm_reindex_temp_index " + )); + cleanupStatements.push(DBConn.createAsyncStatement( + "DROP TRIGGER moz_bm_reindex_temp_trigger " + )); + cleanupStatements.push(DBConn.createAsyncStatement( + "DROP TABLE moz_bm_reindex_temp " + )); + + // D.12 Fix empty-named tags. + // Tags were allowed to have empty names due to a UI bug. Fix them + // replacing their title with "(notitle)". + let fixEmptyNamedTags = DBConn.createAsyncStatement( + `UPDATE moz_bookmarks SET title = :empty_title + WHERE length(title) = 0 AND type = :folder_type + AND parent = :tags_folder` + ); + fixEmptyNamedTags.params["empty_title"] = "(notitle)"; + fixEmptyNamedTags.params["folder_type"] = PlacesUtils.bookmarks.TYPE_FOLDER; + fixEmptyNamedTags.params["tags_folder"] = PlacesUtils.tagsFolderId; + cleanupStatements.push(fixEmptyNamedTags); + + // MOZ_FAVICONS + // E.1 remove orphan icons + let deleteOrphanIcons = DBConn.createAsyncStatement( + `DELETE FROM moz_favicons WHERE id IN ( + SELECT id FROM moz_favicons f + WHERE NOT EXISTS + (SELECT id FROM moz_places WHERE favicon_id = f.id LIMIT 1) + )`); + cleanupStatements.push(deleteOrphanIcons); + + // MOZ_HISTORYVISITS + // F.1 remove orphan visits + let deleteOrphanVisits = DBConn.createAsyncStatement( + `DELETE FROM moz_historyvisits WHERE id IN ( + SELECT id FROM moz_historyvisits v + WHERE NOT EXISTS + (SELECT id FROM moz_places WHERE id = v.place_id LIMIT 1) + )`); + cleanupStatements.push(deleteOrphanVisits); + + // MOZ_INPUTHISTORY + // G.1 remove orphan input history + let deleteOrphanInputHistory = DBConn.createAsyncStatement( + `DELETE FROM moz_inputhistory WHERE place_id IN ( + SELECT place_id FROM moz_inputhistory i + WHERE NOT EXISTS + (SELECT id FROM moz_places WHERE id = i.place_id LIMIT 1) + )`); + cleanupStatements.push(deleteOrphanInputHistory); + + // MOZ_ITEMS_ANNOS + // H.1 remove item annos with an invalid attribute + let deleteInvalidAttributeItemsAnnos = DBConn.createAsyncStatement( + `DELETE FROM moz_items_annos WHERE id IN ( + SELECT id FROM moz_items_annos t + WHERE NOT EXISTS + (SELECT id FROM moz_anno_attributes + WHERE id = t.anno_attribute_id LIMIT 1) + )`); + cleanupStatements.push(deleteInvalidAttributeItemsAnnos); + + // H.2 remove orphan item annos + let deleteOrphanItemsAnnos = DBConn.createAsyncStatement( + `DELETE FROM moz_items_annos WHERE id IN ( + SELECT id FROM moz_items_annos t + WHERE NOT EXISTS + (SELECT id FROM moz_bookmarks WHERE id = t.item_id LIMIT 1) + )`); + cleanupStatements.push(deleteOrphanItemsAnnos); + + // MOZ_KEYWORDS + // I.1 remove unused keywords + let deleteUnusedKeywords = DBConn.createAsyncStatement( + `DELETE FROM moz_keywords WHERE id IN ( + SELECT id FROM moz_keywords k + WHERE NOT EXISTS + (SELECT 1 FROM moz_places h WHERE k.place_id = h.id) + )`); + cleanupStatements.push(deleteUnusedKeywords); + + // MOZ_PLACES + // L.1 fix wrong favicon ids + let fixInvalidFaviconIds = DBConn.createAsyncStatement( + `UPDATE moz_places SET favicon_id = NULL WHERE id IN ( + SELECT id FROM moz_places h + WHERE favicon_id NOT NULL + AND NOT EXISTS + (SELECT id FROM moz_favicons WHERE id = h.favicon_id LIMIT 1) + )`); + cleanupStatements.push(fixInvalidFaviconIds); + + // L.2 recalculate visit_count and last_visit_date + let fixVisitStats = DBConn.createAsyncStatement( + `UPDATE moz_places + SET visit_count = (SELECT count(*) FROM moz_historyvisits + WHERE place_id = moz_places.id AND visit_type NOT IN (0,4,7,8,9)), + last_visit_date = (SELECT MAX(visit_date) FROM moz_historyvisits + WHERE place_id = moz_places.id) + WHERE id IN ( + SELECT h.id FROM moz_places h + WHERE visit_count <> (SELECT count(*) FROM moz_historyvisits v + WHERE v.place_id = h.id AND visit_type NOT IN (0,4,7,8,9)) + OR last_visit_date <> (SELECT MAX(visit_date) FROM moz_historyvisits v + WHERE v.place_id = h.id) + )`); + cleanupStatements.push(fixVisitStats); + + // L.3 recalculate hidden for redirects. + let fixRedirectsHidden = DBConn.createAsyncStatement( + `UPDATE moz_places + SET hidden = 1 + WHERE id IN ( + SELECT h.id FROM moz_places h + JOIN moz_historyvisits src ON src.place_id = h.id + JOIN moz_historyvisits dst ON dst.from_visit = src.id AND dst.visit_type IN (5,6) + LEFT JOIN moz_bookmarks on fk = h.id AND fk ISNULL + GROUP BY src.place_id HAVING count(*) = visit_count + )`); + cleanupStatements.push(fixRedirectsHidden); + + // L.4 recalculate foreign_count. + let fixForeignCount = DBConn.createAsyncStatement( + `UPDATE moz_places SET foreign_count = + (SELECT count(*) FROM moz_bookmarks WHERE fk = moz_places.id ) + + (SELECT count(*) FROM moz_keywords WHERE place_id = moz_places.id )`); + cleanupStatements.push(fixForeignCount); + + // L.5 recalculate missing hashes. + let fixMissingHashes = DBConn.createAsyncStatement( + `UPDATE moz_places SET url_hash = hash(url) WHERE url_hash = 0`); + cleanupStatements.push(fixMissingHashes); + + // MAINTENANCE STATEMENTS SHOULD GO ABOVE THIS POINT! + + return cleanupStatements; + }, + + /** + * Tries to vacuum the database. + * + * @param [optional] aTasks + * Tasks object to execute. + */ + vacuum: function PDBU_vacuum(aTasks) + { + let tasks = new Tasks(aTasks); + tasks.log("> Vacuum"); + + let DBFile = Services.dirsvc.get("ProfD", Ci.nsILocalFile); + DBFile.append("places.sqlite"); + tasks.log("Initial database size is " + + parseInt(DBFile.fileSize / 1024) + " KiB"); + + let stmt = DBConn.createAsyncStatement("VACUUM"); + stmt.executeAsync({ + handleError: PlacesDBUtils._handleError, + handleResult: function () {}, + + handleCompletion: function (aReason) + { + if (aReason == Ci.mozIStorageStatementCallback.REASON_FINISHED) { + tasks.log("+ The database has been vacuumed"); + let vacuumedDBFile = Services.dirsvc.get("ProfD", Ci.nsILocalFile); + vacuumedDBFile.append("places.sqlite"); + tasks.log("Final database size is " + + parseInt(vacuumedDBFile.fileSize / 1024) + " KiB"); + } + else { + tasks.log("- Unable to vacuum database"); + tasks.clear(); + } + + PlacesDBUtils._executeTasks(tasks); + } + }); + stmt.finalize(); + }, + + /** + * Forces a full expiration on the database. + * + * @param [optional] aTasks + * Tasks object to execute. + */ + expire: function PDBU_expire(aTasks) + { + let tasks = new Tasks(aTasks); + tasks.log("> Orphans expiration"); + + let expiration = Cc["@mozilla.org/places/expiration;1"]. + getService(Ci.nsIObserver); + + Services.obs.addObserver(function (aSubject, aTopic, aData) { + Services.obs.removeObserver(arguments.callee, aTopic); + tasks.log("+ Database cleaned up"); + PlacesDBUtils._executeTasks(tasks); + }, PlacesUtils.TOPIC_EXPIRATION_FINISHED, false); + + // Force an orphans expiration step. + expiration.observe(null, "places-debug-start-expiration", 0); + }, + + /** + * Collects statistical data on the database. + * + * @param [optional] aTasks + * Tasks object to execute. + */ + stats: function PDBU_stats(aTasks) + { + let tasks = new Tasks(aTasks); + tasks.log("> Statistics"); + + let DBFile = Services.dirsvc.get("ProfD", Ci.nsILocalFile); + DBFile.append("places.sqlite"); + tasks.log("Database size is " + parseInt(DBFile.fileSize / 1024) + " KiB"); + + [ "user_version" + , "page_size" + , "cache_size" + , "journal_mode" + , "synchronous" + ].forEach(function (aPragma) { + let stmt = DBConn.createStatement("PRAGMA " + aPragma); + stmt.executeStep(); + tasks.log(aPragma + " is " + stmt.getString(0)); + stmt.finalize(); + }); + + // Get maximum number of unique URIs. + try { + let limitURIs = Services.prefs.getIntPref( + "places.history.expiration.transient_current_max_pages"); + tasks.log("History can store a maximum of " + limitURIs + " unique pages"); + } catch (ex) {} + + let stmt = DBConn.createStatement( + "SELECT name FROM sqlite_master WHERE type = :type"); + stmt.params.type = "table"; + while (stmt.executeStep()) { + let tableName = stmt.getString(0); + let countStmt = DBConn.createStatement( + `SELECT count(*) FROM ${tableName}`); + countStmt.executeStep(); + tasks.log("Table " + tableName + " has " + countStmt.getInt32(0) + " records"); + countStmt.finalize(); + } + stmt.reset(); + + stmt.params.type = "index"; + while (stmt.executeStep()) { + tasks.log("Index " + stmt.getString(0)); + } + stmt.reset(); + + stmt.params.type = "trigger"; + while (stmt.executeStep()) { + tasks.log("Trigger " + stmt.getString(0)); + } + stmt.finalize(); + + PlacesDBUtils._executeTasks(tasks); + }, + + /** + * Collects telemetry data and reports it to Telemetry. + * + * @param [optional] aTasks + * Tasks object to execute. + */ + telemetry: function PDBU_telemetry(aTasks) + { + let tasks = new Tasks(aTasks); + + // This will be populated with one integer property for each probe result, + // using the histogram name as key. + let probeValues = {}; + + // The following array contains an ordered list of entries that are + // processed to collect telemetry data. Each entry has these properties: + // + // histogram: Name of the telemetry histogram to update. + // query: This is optional. If present, contains a database command + // that will be executed asynchronously, and whose result will + // be added to the telemetry histogram. + // callback: This is optional. If present, contains a function that must + // return the value that will be added to the telemetry + // histogram. If a query is also present, its result is passed + // as the first argument of the function. If the function + // raises an exception, no data is added to the histogram. + // + // Since all queries are executed in order by the database backend, the + // callbacks can also use the result of previous queries stored in the + // probeValues object. + let probes = [ + { histogram: "PLACES_PAGES_COUNT", + query: "SELECT count(*) FROM moz_places" }, + + { histogram: "PLACES_BOOKMARKS_COUNT", + query: `SELECT count(*) FROM moz_bookmarks b + JOIN moz_bookmarks t ON t.id = b.parent + AND t.parent <> :tags_folder + WHERE b.type = :type_bookmark` }, + + { histogram: "PLACES_TAGS_COUNT", + query: `SELECT count(*) FROM moz_bookmarks + WHERE parent = :tags_folder` }, + + { histogram: "PLACES_KEYWORDS_COUNT", + query: "SELECT count(*) FROM moz_keywords" }, + + { histogram: "PLACES_SORTED_BOOKMARKS_PERC", + query: `SELECT IFNULL(ROUND(( + SELECT count(*) FROM moz_bookmarks b + JOIN moz_bookmarks t ON t.id = b.parent + AND t.parent <> :tags_folder AND t.parent > :places_root + WHERE b.type = :type_bookmark + ) * 100 / ( + SELECT count(*) FROM moz_bookmarks b + JOIN moz_bookmarks t ON t.id = b.parent + AND t.parent <> :tags_folder + WHERE b.type = :type_bookmark + )), 0)` }, + + { histogram: "PLACES_TAGGED_BOOKMARKS_PERC", + query: `SELECT IFNULL(ROUND(( + SELECT count(*) FROM moz_bookmarks b + JOIN moz_bookmarks t ON t.id = b.parent + AND t.parent = :tags_folder + ) * 100 / ( + SELECT count(*) FROM moz_bookmarks b + JOIN moz_bookmarks t ON t.id = b.parent + AND t.parent <> :tags_folder + WHERE b.type = :type_bookmark + )), 0)` }, + + { histogram: "PLACES_DATABASE_FILESIZE_MB", + callback: function () { + let DBFile = Services.dirsvc.get("ProfD", Ci.nsILocalFile); + DBFile.append("places.sqlite"); + return parseInt(DBFile.fileSize / BYTES_PER_MEBIBYTE); + } + }, + + { histogram: "PLACES_DATABASE_PAGESIZE_B", + query: "PRAGMA page_size /* PlacesDBUtils.jsm PAGESIZE_B */" }, + + { histogram: "PLACES_DATABASE_SIZE_PER_PAGE_B", + query: "PRAGMA page_count", + callback: function (aDbPageCount) { + // Note that the database file size would not be meaningful for this + // calculation, because the file grows in fixed-size chunks. + let dbPageSize = probeValues.PLACES_DATABASE_PAGESIZE_B; + let placesPageCount = probeValues.PLACES_PAGES_COUNT; + return Math.round((dbPageSize * aDbPageCount) / placesPageCount); + } + }, + + { histogram: "PLACES_ANNOS_BOOKMARKS_COUNT", + query: "SELECT count(*) FROM moz_items_annos" }, + + { histogram: "PLACES_ANNOS_PAGES_COUNT", + query: "SELECT count(*) FROM moz_annos" }, + + { histogram: "PLACES_MAINTENANCE_DAYSFROMLAST", + callback: function () { + try { + let lastMaintenance = Services.prefs.getIntPref("places.database.lastMaintenance"); + let nowSeconds = parseInt(Date.now() / 1000); + return parseInt((nowSeconds - lastMaintenance) / 86400); + } catch (ex) { + return 60; + } + } + }, + ]; + + let params = { + tags_folder: PlacesUtils.tagsFolderId, + type_folder: PlacesUtils.bookmarks.TYPE_FOLDER, + type_bookmark: PlacesUtils.bookmarks.TYPE_BOOKMARK, + places_root: PlacesUtils.placesRootId + }; + + for (let i = 0; i < probes.length; i++) { + let probe = probes[i]; + + let promiseDone = new Promise((resolve, reject) => { + if (!("query" in probe)) { + resolve([probe]); + return; + } + + let stmt = DBConn.createAsyncStatement(probe.query); + for (let param in params) { + if (probe.query.indexOf(":" + param) > 0) { + stmt.params[param] = params[param]; + } + } + + try { + stmt.executeAsync({ + handleError: reject, + handleResult: function (aResultSet) { + let row = aResultSet.getNextRow(); + resolve([probe, row.getResultByIndex(0)]); + }, + handleCompletion: function () {} + }); + } finally { + stmt.finalize(); + } + }); + + // Report the result of the probe through Telemetry. + // The resulting promise cannot reject. + promiseDone.then( + // On success + ([aProbe, aValue]) => { + let value = aValue; + try { + if ("callback" in aProbe) { + value = aProbe.callback(value); + } + probeValues[aProbe.histogram] = value; + Services.telemetry.getHistogramById(aProbe.histogram).add(value); + } catch (ex) { + Components.utils.reportError("Error adding value " + value + + " to histogram " + aProbe.histogram + + ": " + ex); + } + }, + // On failure + this._handleError); + } + + PlacesDBUtils._executeTasks(tasks); + }, + + /** + * Runs a list of tasks, notifying log messages to the callback. + * + * @param aTasks + * Array of tasks to be executed, in form of pointers to methods in + * this module. + * @param [optional] aCallback + * Callback to be invoked when done. It will receive an array of + * log messages. + */ + runTasks: function PDBU_runTasks(aTasks, aCallback) { + let tasks = new Tasks(aTasks); + tasks.callback = aCallback; + PlacesDBUtils._executeTasks(tasks); + } +}; + +/** + * LIFO tasks stack. + * + * @param [optional] aTasks + * Array of tasks or another Tasks object to clone. + */ +function Tasks(aTasks) +{ + if (aTasks) { + if (Array.isArray(aTasks)) { + this._list = aTasks.slice(0, aTasks.length); + } + // This supports passing in a Tasks-like object, with a "list" property, + // for compatibility reasons. + else if (typeof(aTasks) == "object" && + (Tasks instanceof Tasks || "list" in aTasks)) { + this._list = aTasks.list; + this._log = aTasks.messages; + this.callback = aTasks.callback; + this.scope = aTasks.scope; + this._telemetryStart = aTasks._telemetryStart; + } + } +} + +Tasks.prototype = { + _list: [], + _log: [], + callback: null, + scope: null, + _telemetryStart: 0, + + /** + * Adds a task to the top of the list. + * + * @param aNewElt + * Task to be added. + */ + push: function T_push(aNewElt) + { + this._list.unshift(aNewElt); + }, + + /** + * Returns and consumes next task. + * + * @return next task or undefined if no task is left. + */ + pop: function T_pop() + { + return this._list.shift(); + }, + + /** + * Removes all tasks. + */ + clear: function T_clear() + { + this._list.length = 0; + }, + + /** + * Returns array of tasks ordered from the next to be run to the latest. + */ + get list() + { + return this._list.slice(0, this._list.length); + }, + + /** + * Adds a message to the log. + * + * @param aMsg + * String message to be added. + */ + log: function T_log(aMsg) + { + this._log.push(aMsg); + }, + + /** + * Returns array of log messages ordered from oldest to newest. + */ + get messages() + { + return this._log.slice(0, this._log.length); + }, +} |