www

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | Submodules | README | LICENSE

commit 4e1dd6f5b6c8e12747773328af2227523db8478b
parent 1e7c822ab020c69929320847710267bac0b0ebb7
Author: Dan Stillman <dstillman@zotero.org>
Date:   Thu, 28 May 2015 15:38:39 -0400

Restore DB parameter checking and add tests

Some parameter situations also weren't being properly handled

Diffstat:
Mchrome/content/zotero/xpcom/db.js | 66++++++++++++++++++++++++++++++++++++------------------------------
Mtest/tests/dbTest.js | 119+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 155 insertions(+), 30 deletions(-)

diff --git a/chrome/content/zotero/xpcom/db.js b/chrome/content/zotero/xpcom/db.js @@ -23,6 +23,8 @@ ***** END LICENSE BLOCK ***** */ +"use strict"; + // Exclusive locking mode (default) prevents access to Zotero database while Zotero is open // and speeds up DB access (http://www.sqlite.org/pragma.html#pragma_locking_mode). // Normal mode is more convenient for development, but risks database corruption, particularly if @@ -122,7 +124,7 @@ Zotero.DBConnection.prototype.getAsyncStatement = Zotero.Promise.coroutine(funct }); -Zotero.DBConnection.prototype.parseQueryAndParams = function (sql, params, options) { +Zotero.DBConnection.prototype.parseQueryAndParams = function (sql, params) { // If single scalar value, wrap in an array if (!Array.isArray(params)) { if (typeof params == 'string' || typeof params == 'number' || params === null) { @@ -138,31 +140,43 @@ Zotero.DBConnection.prototype.parseQueryAndParams = function (sql, params, optio } // Find placeholders - var placeholderRE = /\s*[=,(]\s*\?/g; - if (params.length) { - if (options && options.checkParams) { - let matches = sql.match(placeholderRE); - - if (!matches) { - throw new Error("Parameters provided for query without placeholders " - + "[QUERY: " + sql + "]"); + let matches = sql.match(/\?\d*/g); + if (!matches) { + throw new Error("Parameters provided for query without placeholders " + + "[QUERY: " + sql + "]"); + } + else { + // Count numbered parameters (?1) properly + let num = 0; + let numbered = {}; + for (let i = 0; i < matches.length; i++) { + let match = matches[i]; + if (match == '?') { + num++; + } + else { + numbered[match] = true; + } } - else if (matches.length != params.length) { + num += Object.keys(numbered).length; + + if (params.length != num) { throw new Error("Incorrect number of parameters provided for query " - + "(" + params.length + ", expecting " + matches.length + ") " + + "(" + params.length + ", expecting " + num + ") " + "[QUERY: " + sql + "]"); } } // First, determine the type of query using first word - var matches = sql.match(/^[^\s\(]*/); - var queryMethod = matches[0].toLowerCase(); + let queryMethod = sql.match(/^[^\s\(]*/)[0].toLowerCase(); // Reset lastIndex, since regexp isn't recompiled dynamically - placeholderRE.lastIndex = 0; - var lastNullParamIndex = -1; + let placeholderRE = /\s*[=,(]\s*\?/g; for (var i=0; i<params.length; i++) { + // Find index of this parameter, skipping previous ones + matches = placeholderRE.exec(sql); + if (typeof params[i] == 'boolean') { throw new Error("Invalid boolean parameter " + i + " '" + params[i] + "' " + "[QUERY: " + sql + "]"); @@ -193,23 +207,17 @@ Zotero.DBConnection.prototype.parseQueryAndParams = function (sql, params, optio // // Replace NULL bound parameters with hard-coded NULLs // - - // Find index of this parameter, skipping previous ones - do { - var matches = placeholderRE.exec(sql); - lastNullParamIndex++; - } - while (lastNullParamIndex < i); - lastNullParamIndex = i; - if (!matches) { throw new Error("Null parameter provided for a query without placeholders " + "-- use false or undefined [QUERY: " + sql + "]"); } - if (matches[0].indexOf('=') == -1) { - // mozStorage supports null bound parameters in value lists (e.g., "(?,?)") natively - continue; + if (matches[0].trim().indexOf('=') == -1) { + if (queryMethod == 'select') { + throw new Error("NULL cannot be used for parenthesized placeholders " + + "in SELECT queries [QUERY: " + sql + "]"); + } + var repl = matches[0].replace('?', 'NULL'); } else if (queryMethod == 'select') { var repl = ' IS NULL'; @@ -226,14 +234,12 @@ Zotero.DBConnection.prototype.parseQueryAndParams = function (sql, params, optio params.splice(i, 1); i--; - lastNullParamIndex--; - continue; } if (!params.length) { params = []; } } - else if (options && options.checkParams && placeholderRE.test(sql)) { + else if (/\?/g.test(sql)) { throw new Error("Parameters not provided for query containing placeholders " + "[QUERY: " + sql + "]"); } diff --git a/test/tests/dbTest.js b/test/tests/dbTest.js @@ -15,6 +15,125 @@ describe("Zotero.DB", function() { yield Zotero.DB.queryAsync("DROP TABLE IF EXISTS " + tmpTable); }); + + describe("#queryAsync()", function () { + var tmpTable; + + before(function* () { + tmpTable = "tmp_queryAsync"; + yield Zotero.DB.queryAsync("CREATE TEMPORARY TABLE " + tmpTable + " (a, b)"); + yield Zotero.DB.queryAsync("INSERT INTO " + tmpTable + " VALUES (1, 2)"); + yield Zotero.DB.queryAsync("INSERT INTO " + tmpTable + " VALUES (3, 4)"); + yield Zotero.DB.queryAsync("INSERT INTO " + tmpTable + " VALUES (5, NULL)"); + }) + after(function* () { + if (tmpTable) { + yield Zotero.DB.queryAsync("DROP TABLE IF EXISTS " + tmpTable); + } + }) + + it("should throw an error if no parameters are passed for a query with placeholders", function* () { + var e = yield getPromiseError(Zotero.DB.queryAsync("SELECT itemID FROM items WHERE itemID=?")); + assert.ok(e); + assert.include(e.message, "for query containing placeholders"); + }) + + it("should throw an error if too few parameters are passed", function* () { + var e = yield getPromiseError(Zotero.DB.queryAsync("SELECT itemID FROM items WHERE itemID=? OR itemID=?", [1])); + assert.ok(e); + assert.include(e.message, "Incorrect number of parameters provided for query"); + }) + + it("should throw an error if too many parameters are passed", function* () { + var e = yield getPromiseError(Zotero.DB.queryAsync("SELECT itemID FROM items WHERE itemID=?", [1, 2])); + assert.ok(e); + assert.include(e.message, "Incorrect number of parameters provided for query"); + }) + + it("should throw an error if too many parameters are passed for numbered placeholders", function* () { + var e = yield getPromiseError(Zotero.DB.queryAsync("SELECT itemID FROM items WHERE itemID=?1 OR itemID=?1", [1, 2])); + assert.ok(e); + assert.include(e.message, "Incorrect number of parameters provided for query"); + }) + + it("should accept a single placeholder given as a value", function* () { + var rows = yield Zotero.DB.queryAsync("SELECT a FROM " + tmpTable + " WHERE b=?", 2); + assert.lengthOf(rows, 1); + assert.equal(rows[0].a, 1); + }) + + it("should accept a single placeholder given as an array", function* () { + var rows = yield Zotero.DB.queryAsync("SELECT a FROM " + tmpTable + " WHERE b=?", [2]); + assert.lengthOf(rows, 1); + assert.equal(rows[0].a, 1); + }) + + it("should accept multiple placeholders", function* () { + var rows = yield Zotero.DB.queryAsync("SELECT a FROM " + tmpTable + " WHERE b=? OR b=?", [2, 4]); + assert.lengthOf(rows, 2); + assert.equal(rows[0].a, 1); + assert.equal(rows[1].a, 3); + }) + + it("should accept a single placeholder within parentheses", function* () { + var rows = yield Zotero.DB.queryAsync("SELECT a FROM " + tmpTable + " WHERE b IN (?)", 2); + assert.lengthOf(rows, 1); + assert.equal(rows[0].a, 1); + }) + + it("should accept multiple placeholders within parentheses", function* () { + var rows = yield Zotero.DB.queryAsync("SELECT a FROM " + tmpTable + " WHERE b IN (?, ?)", [2, 4]); + assert.lengthOf(rows, 2); + assert.equal(rows[0].a, 1); + assert.equal(rows[1].a, 3); + }) + + it("should replace =? with IS NULL if NULL is passed as a value", function* () { + var rows = yield Zotero.DB.queryAsync("SELECT a FROM " + tmpTable + " WHERE b=?", null); + assert.lengthOf(rows, 1); + assert.equal(rows[0].a, 5); + }) + + it("should replace =? with IS NULL if NULL is passed in an array", function* () { + var rows = yield Zotero.DB.queryAsync("SELECT a FROM " + tmpTable + " WHERE b=?", [null]); + assert.lengthOf(rows, 1); + assert.equal(rows[0].a, 5); + }) + + it("should replace ? with NULL for placeholders within parentheses in INSERT statements", function* () { + yield Zotero.DB.queryAsync("CREATE TEMPORARY TABLE tmp_srqwnfpwpinss (a, b)"); + // Replace ", ?" + yield Zotero.DB.queryAsync("INSERT INTO tmp_srqwnfpwpinss (a, b) VALUES (?, ?)", [1, null]); + assert.equal( + (yield Zotero.DB.valueQueryAsync("SELECT a FROM tmp_srqwnfpwpinss WHERE b IS NULL")), + 1 + ); + // Replace "(?" + yield Zotero.DB.queryAsync("DELETE FROM tmp_srqwnfpwpinss"); + yield Zotero.DB.queryAsync("INSERT INTO tmp_srqwnfpwpinss (a, b) VALUES (?, ?)", [null, 2]); + assert.equal( + (yield Zotero.DB.valueQueryAsync("SELECT b FROM tmp_srqwnfpwpinss WHERE a IS NULL")), + 2 + ); + yield Zotero.DB.queryAsync("DROP TABLE tmp_srqwnfpwpinss"); + }) + + it("should throw an error if NULL is passed for placeholder within parentheses in a SELECT statement", function* () { + var e = yield getPromiseError(Zotero.DB.queryAsync("SELECT a FROM " + tmpTable + " WHERE b IN (?)", null)); + assert.ok(e); + assert.include(e.message, "NULL cannot be used for parenthesized placeholders in SELECT queries"); + }) + + it("should handle numbered parameters", function* () { + var rows = yield Zotero.DB.queryAsync("SELECT a FROM " + tmpTable + " WHERE b=?1 " + + "UNION SELECT b FROM " + tmpTable + " WHERE b=?1", 2); + assert.lengthOf(rows, 2); + assert.equal(rows[0].a, 1); + assert.equal(rows[1].a, 2); + }) + }) + + describe("#executeTransaction()", function () { it("should serialize concurrent transactions", Zotero.Promise.coroutine(function* () { this.timeout(1000);