Bug 888784 - Make FormHistory.update use Sqlite.jsm database backend. r=mak

MozReview-Commit-ID: 7Ku1kFtTYZK

--HG--
extra : rebase_source : e03ebfa4e724c0d6b0a9eeb2c5ab0d8191e7fcb0
This commit is contained in:
Mike Conley 2017-11-30 18:09:54 -05:00
Родитель 95819f2108
Коммит b5fae64640
1 изменённых файлов: 158 добавлений и 96 удалений

Просмотреть файл

@ -621,14 +621,48 @@ function dbClose(aShutdown) {
} }
} }
/**
* @typedef {Object} InsertQueryData
* @property {Object} updatedChange
* A change requested by FormHistory.
* @property {String} query
* The insert query string.
*/
/**
* Prepares a query and some default parameters when inserting an entry
* to the database.
*
* @param {Object} change
* The change requested by FormHistory.
* @param {number} now
* The current timestamp in microseconds.
* @returns {InsertQueryData}
* The query information needed to pass along to the database.
*/
function prepareInsertQuery(change, now) {
let updatedChange = Object.assign({}, change);
let query = "INSERT INTO moz_formhistory " +
"(fieldname, value, timesUsed, firstUsed, lastUsed, guid) " +
"VALUES (:fieldname, :value, :timesUsed, :firstUsed, :lastUsed, :guid)";
updatedChange.timesUsed = updatedChange.timesUsed || 1;
updatedChange.firstUsed = updatedChange.firstUsed || now;
updatedChange.lastUsed = updatedChange.lastUsed || now;
return {
updatedChange,
query,
};
}
/** /**
* Constructs and executes database statements from a pre-processed list of * Constructs and executes database statements from a pre-processed list of
* inputted changes. * inputted changes.
* *
* @param {Array.<Object>} aChanges changes to form history * @param {Array.<Object>} aChanges changes to form history
* @param {Object} aCallbacks * @param {Object} aPreparedHandlers
*/ */
async function updateFormHistoryWrite(aChanges, aCallbacks) { async function updateFormHistoryWrite(aChanges, aPreparedHandlers) {
log("updateFormHistoryWrite " + aChanges.length); log("updateFormHistoryWrite " + aChanges.length);
// pass 'now' down so that every entry in the batch has the same timestamp // pass 'now' down so that every entry in the batch has the same timestamp
@ -638,51 +672,65 @@ async function updateFormHistoryWrite(aChanges, aCallbacks) {
// stmts or bind a new set of parameters to an existing storage statement. // stmts or bind a new set of parameters to an existing storage statement.
// stmts and bindingArrays are updated when makeXXXStatement eventually // stmts and bindingArrays are updated when makeXXXStatement eventually
// calls dbCreateAsyncStatement. // calls dbCreateAsyncStatement.
let stmts = []; let queries = [];
let notifications = []; let notifications = [];
let bindingArrays = new Map(); let conn = await FormHistory.db;
for (let change of aChanges) { for (let change of aChanges) {
let operation = change.op; let operation = change.op;
delete change.op; delete change.op;
let stmt;
switch (operation) { switch (operation) {
case "remove": case "remove": {
log("Remove from form history " + change); log("Remove from form history " + change);
let delStmt = makeMoveToDeletedStatement(change.guid, now, change, bindingArrays); let queryTerms = makeQueryPredicates(change);
if (delStmt && !stmts.includes(delStmt)) {
stmts.push(delStmt);
}
if ("timeDeleted" in change) {
delete change.timeDeleted;
}
stmt = makeRemoveStatement(change, bindingArrays);
// Fetch the GUIDs we are going to delete. // Fetch the GUIDs we are going to delete.
try { try {
await new Promise((res, rej) => { let query = "SELECT guid FROM moz_formhistory";
let selectStmt = makeSearchStatement(change, ["guid"]); if (queryTerms) {
let selectHandlers = { query += " WHERE " + queryTerms;
handleCompletion() { }
res();
}, await conn.executeCached(query, change, row => {
handleError() { notifications.push(["formhistory-remove", row.getResultByName("guid")]);
log("remove select guids failure");
},
handleResult(aResultSet) {
for (let row = aResultSet.getNextRow(); row; row = aResultSet.getNextRow()) {
notifications.push(["formhistory-remove", row.getResultByName("guid")]);
}
},
};
dbConnection.executeAsync([selectStmt], 1, selectHandlers);
}); });
} catch (e) { } catch (e) {
log("Error in select statement: " + e); log("Error getting guids from moz_formhistory: " + e);
} }
if (supportsDeletedTable) {
log("Moving to deleted table " + change);
let query = "INSERT INTO moz_deleted_formhistory (guid, timeDeleted)";
// TODO: Add these items to the deleted items table once we've sorted
// out the issues from bug 756701
if (change.guid || queryTerms) {
query +=
change.guid ? " VALUES (:guid, :timeDeleted)"
: " SELECT guid, :timeDeleted FROM moz_formhistory WHERE " + queryTerms;
change.timeDeleted = now;
queries.push({ query, params: Object.assign({}, change) });
}
if ("timeDeleted" in change) {
delete change.timeDeleted;
}
}
let query = "DELETE FROM moz_formhistory";
if (queryTerms) {
log("removeEntries");
query += " WHERE " + queryTerms;
} else {
log("removeAllEntries");
// Not specifying any fields means we should remove all entries. We
// won't need to modify the query in this case.
}
queries.push({ query, params: change });
break; break;
case "update": }
case "update": {
log("Update form history " + change); log("Update form history " + change);
let guid = change.guid; let guid = change.guid;
delete change.guid; delete change.guid;
@ -692,70 +740,90 @@ async function updateFormHistoryWrite(aChanges, aCallbacks) {
change.guid = change.newGuid; change.guid = change.newGuid;
delete change.newGuid; delete change.newGuid;
} }
stmt = makeUpdateStatement(guid, change, bindingArrays);
let query = "UPDATE moz_formhistory SET ";
let queryTerms = makeQueryPredicates(change, ", ");
if (!queryTerms) {
throw Components.Exception("Update query must define fields to modify.",
Cr.NS_ERROR_ILLEGAL_VALUE);
}
query += queryTerms + " WHERE guid = :existing_guid";
change.existing_guid = guid;
queries.push({ query, params: change });
notifications.push(["formhistory-update", guid]); notifications.push(["formhistory-update", guid]);
break; break;
case "bump": }
case "bump": {
log("Bump form history " + change); log("Bump form history " + change);
if (change.guid) { if (change.guid) {
stmt = makeBumpStatement(change.guid, now, bindingArrays); let query = "UPDATE moz_formhistory " +
"SET timesUsed = timesUsed + 1, lastUsed = :lastUsed WHERE guid = :guid";
let queryParams = {
lastUsed: now,
guid: change.guid,
};
queries.push({ query, params: queryParams });
notifications.push(["formhistory-update", change.guid]); notifications.push(["formhistory-update", change.guid]);
} else { } else {
change.guid = generateGUID(); change.guid = generateGUID();
stmt = makeAddStatement(change, now, bindingArrays); let { query, updatedChange } = prepareInsertQuery(change, now);
notifications.push(["formhistory-add", change.guid]); queries.push({ query, params: updatedChange });
notifications.push(["formhistory-add", updatedChange.guid]);
} }
break; break;
case "add": }
case "add": {
log("Add to form history " + change); log("Add to form history " + change);
if (!change.guid) { if (!change.guid) {
change.guid = generateGUID(); change.guid = generateGUID();
} }
stmt = makeAddStatement(change, now, bindingArrays);
notifications.push(["formhistory-add", change.guid]); let { query, updatedChange } = prepareInsertQuery(change, now);
queries.push({ query, params: updatedChange });
notifications.push(["formhistory-add", updatedChange.guid]);
break; break;
default: }
default: {
// We should've already guaranteed that change.op is one of the above // We should've already guaranteed that change.op is one of the above
throw Components.Exception("Invalid operation " + operation, throw Components.Exception("Invalid operation " + operation,
Cr.NS_ERROR_ILLEGAL_VALUE); Cr.NS_ERROR_ILLEGAL_VALUE);
} }
// As identical statements are reused, only add statements if they aren't already present.
if (stmt && !stmts.includes(stmt)) {
stmts.push(stmt);
} }
} }
for (let stmt of stmts) { try {
stmt.bindParameters(bindingArrays.get(stmt)); await runUpdateQueries(conn, queries);
} catch (e) {
aPreparedHandlers.handleError(e);
aPreparedHandlers.handleCompletion(1);
return;
} }
let handlers = { for (let [notification, param] of notifications) {
handleCompletion(aReason) { // We're either sending a GUID or nothing at all.
if (aReason == Ci.mozIStorageStatementCallback.REASON_FINISHED) { sendNotification(notification, param);
for (let [notification, param] of notifications) { }
// We're either sending a GUID or nothing at all.
sendNotification(notification, param);
}
}
if (aCallbacks && aCallbacks.handleCompletion) { aPreparedHandlers.handleCompletion(0);
aCallbacks.handleCompletion( }
aReason == Ci.mozIStorageStatementCallback.REASON_FINISHED ?
0 :
1
);
}
},
handleError(aError) {
if (aCallbacks && aCallbacks.handleError) {
aCallbacks.handleError(aError);
}
},
handleResult: NOOP,
};
dbConnection.executeAsync(stmts, stmts.length, handlers); /**
* Runs queries for an update operation to the database. This
* is separated out from updateFormHistoryWrite to avoid shutdown
* leaks where the handlers passed to updateFormHistoryWrite would
* leak from the closure around the executeTransaction function.
*
* @param {SqliteConnection} conn the database connection
* @param {Object} queries query string and param pairs generated
* by updateFormHistoryWrite
*/
async function runUpdateQueries(conn, queries) {
await conn.executeTransaction(async () => {
for (let { query, params } of queries) {
await conn.executeCached(query, params);
}
});
} }
/** /**
@ -1170,7 +1238,7 @@ this.FormHistory = {
}); });
}, },
update(aChanges, aCallbacks) { update(aChanges, aHandlers) {
// Used to keep track of how many searches have been started. When that number // Used to keep track of how many searches have been started. When that number
// are finished, updateFormHistoryWrite can be called. // are finished, updateFormHistoryWrite can be called.
let numSearches = 0; let numSearches = 0;
@ -1187,17 +1255,15 @@ this.FormHistory = {
aChanges = [aChanges]; aChanges = [aChanges];
} }
let handlers = this._prepareHandlers(aHandlers);
let isRemoveOperation = aChanges.every(change => change && change.op && change.op == "remove"); let isRemoveOperation = aChanges.every(change => change && change.op && change.op == "remove");
if (!Prefs.enabled && !isRemoveOperation) { if (!Prefs.enabled && !isRemoveOperation) {
if (aCallbacks && aCallbacks.handleError) { handlers.handleError({
aCallbacks.handleError({ message: "Form history is disabled, only remove operations are allowed",
message: "Form history is disabled, only remove operations are allowed", result: Ci.mozIStorageError.MISUSE,
result: Ci.mozIStorageError.MISUSE, });
}); handlers.handleCompletion(1);
}
if (aCallbacks && aCallbacks.handleCompletion) {
aCallbacks.handleCompletion(1);
}
return; return;
} }
@ -1257,13 +1323,11 @@ this.FormHistory = {
handleResult(aResult) { handleResult(aResult) {
if (this.foundResult) { if (this.foundResult) {
log("Database contains multiple entries with the same fieldname/value pair."); log("Database contains multiple entries with the same fieldname/value pair.");
if (aCallbacks && aCallbacks.handleError) { handlers.handleError({
aCallbacks.handleError({ message:
message: "Database contains multiple entries with the same fieldname/value pair.",
"Database contains multiple entries with the same fieldname/value pair.", result: 19, // Constraint violation
result: 19, // Constraint violation });
});
}
searchFailed = true; searchFailed = true;
return; return;
@ -1274,18 +1338,16 @@ this.FormHistory = {
}, },
handleError(aError) { handleError(aError) {
if (aCallbacks && aCallbacks.handleError) { handlers.handleError(aError);
aCallbacks.handleError(aError);
}
}, },
handleCompletion(aReason) { handleCompletion(aReason) {
completedSearches++; completedSearches++;
if (completedSearches == numSearches) { if (completedSearches == numSearches) {
if (!aReason && !searchFailed) { if (!aReason && !searchFailed) {
updateFormHistoryWrite(aChanges, aCallbacks); updateFormHistoryWrite(aChanges, handlers);
} else if (aCallbacks && aCallbacks.handleCompletion) { } else {
aCallbacks.handleCompletion(1); handlers.handleCompletion(1);
} }
} }
}, },
@ -1294,7 +1356,7 @@ this.FormHistory = {
if (numSearches == 0) { if (numSearches == 0) {
// We don't have to wait for any statements to return. // We don't have to wait for any statements to return.
updateFormHistoryWrite(aChanges, aCallbacks); updateFormHistoryWrite(aChanges, handlers);
} }
}, },