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