393 lines
13 KiB
JavaScript
393 lines
13 KiB
JavaScript
describe("Zotero.DB", function() {
|
|
var tmpTable = "tmpDBTest";
|
|
|
|
before(function* () {
|
|
this.timeout(5000);
|
|
Zotero.debug("Waiting for DB activity to settle");
|
|
yield Zotero.DB.waitForTransaction();
|
|
yield Zotero.Promise.delay(1000);
|
|
});
|
|
beforeEach(function* () {
|
|
yield Zotero.DB.queryAsync("DROP TABLE IF EXISTS " + tmpTable);
|
|
yield Zotero.DB.queryAsync("CREATE TABLE " + tmpTable + " (foo INT)");
|
|
});
|
|
after(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 combination of numbered and unnumbered placeholders", async function () {
|
|
var rows = await Zotero.DB.queryAsync("SELECT a FROM " + tmpTable + " WHERE (a=?1 OR b=?1) 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);
|
|
})
|
|
|
|
it("should throw an error if onRow throws an error", function* () {
|
|
var i = 0;
|
|
var e = Zotero.DB.queryAsync(
|
|
"SELECT * FROM " + tmpTable,
|
|
false,
|
|
{
|
|
onRow: function (row) {
|
|
if (i > 0) {
|
|
throw new Error("Failed");
|
|
}
|
|
i++;
|
|
}
|
|
}
|
|
);
|
|
e = yield getPromiseError(e)
|
|
assert.ok(e);
|
|
assert.equal(e.message, "Failed");
|
|
});
|
|
|
|
it("should stop gracefully if onRow calls cancel()", function* () {
|
|
var i = 0;
|
|
var rows = [];
|
|
yield Zotero.DB.queryAsync(
|
|
"SELECT * FROM " + tmpTable,
|
|
false,
|
|
{
|
|
onRow: function (row, cancel) {
|
|
if (i > 0) {
|
|
cancel();
|
|
return;
|
|
}
|
|
rows.push(row.getResultByIndex(0));
|
|
i++;
|
|
}
|
|
}
|
|
);
|
|
assert.lengthOf(rows, 1);
|
|
});
|
|
})
|
|
|
|
|
|
describe("#executeTransaction()", function () {
|
|
it("should serialize concurrent transactions", Zotero.Promise.coroutine(function* () {
|
|
var resolve1, resolve2, reject1, reject2;
|
|
var promise1 = new Promise(function (resolve, reject) {
|
|
resolve1 = resolve;
|
|
reject1 = reject;
|
|
});
|
|
var promise2 = new Promise(function (resolve, reject) {
|
|
resolve2 = resolve;
|
|
reject2 = reject;
|
|
});
|
|
|
|
Zotero.DB.executeTransaction(async function () {
|
|
await Zotero.Promise.delay(250);
|
|
var num = await Zotero.DB.valueQueryAsync("SELECT COUNT(*) FROM " + tmpTable);
|
|
assert.equal(num, 0);
|
|
await Zotero.DB.queryAsync("INSERT INTO " + tmpTable + " VALUES (1)");
|
|
assert.ok(Zotero.DB.inTransaction());
|
|
})
|
|
.then(resolve1)
|
|
.catch(reject1);
|
|
|
|
Zotero.DB.executeTransaction(async function () {
|
|
var num = await Zotero.DB.valueQueryAsync("SELECT COUNT(*) FROM " + tmpTable);
|
|
assert.equal(num, 1);
|
|
await Zotero.Promise.delay(500);
|
|
await Zotero.DB.queryAsync("INSERT INTO " + tmpTable + " VALUES (2)");
|
|
assert.ok(Zotero.DB.inTransaction());
|
|
})
|
|
.then(resolve2)
|
|
.catch(reject2);
|
|
|
|
yield Zotero.Promise.all([promise1, promise2]);
|
|
}));
|
|
|
|
it("should serialize queued transactions", function* () {
|
|
var resolve1, resolve2, reject1, reject2, resolve3, reject3;
|
|
var promise1 = new Promise(function (resolve, reject) {
|
|
resolve1 = resolve;
|
|
reject1 = reject;
|
|
});
|
|
var promise2 = new Promise(function (resolve, reject) {
|
|
resolve2 = resolve;
|
|
reject2 = reject;
|
|
});
|
|
var promise3 = new Promise(function (resolve, reject) {
|
|
resolve3 = resolve;
|
|
reject3 = reject;
|
|
});
|
|
|
|
// Start a transaction and have it delay
|
|
Zotero.DB.executeTransaction(async function () {
|
|
await Zotero.Promise.delay(100);
|
|
var num = await Zotero.DB.valueQueryAsync("SELECT COUNT(*) FROM " + tmpTable);
|
|
assert.equal(num, 0);
|
|
await Zotero.DB.queryAsync("INSERT INTO " + tmpTable + " VALUES (1)");
|
|
assert.ok(Zotero.DB.inTransaction());
|
|
})
|
|
.then(resolve1)
|
|
.catch(reject1);
|
|
|
|
// Start two more transactions, which should wait on the first
|
|
Zotero.DB.executeTransaction(async function () {
|
|
var num = await Zotero.DB.valueQueryAsync("SELECT COUNT(*) FROM " + tmpTable);
|
|
assert.equal(num, 1);
|
|
await Zotero.DB.queryAsync("INSERT INTO " + tmpTable + " VALUES (2)");
|
|
assert.ok(Zotero.DB.inTransaction());
|
|
})
|
|
.then(resolve2)
|
|
.catch(reject2);
|
|
|
|
Zotero.DB.executeTransaction(async function () {
|
|
var num = await Zotero.DB.valueQueryAsync("SELECT COUNT(*) FROM " + tmpTable);
|
|
assert.equal(num, 2);
|
|
await Zotero.DB.queryAsync("INSERT INTO " + tmpTable + " VALUES (3)");
|
|
// But make sure the second queued transaction doesn't start at the same time,
|
|
// such that the first queued transaction gets closed while the second is still
|
|
// running
|
|
assert.ok(Zotero.DB.inTransaction());
|
|
})
|
|
.then(resolve3)
|
|
.catch(reject3);
|
|
|
|
yield Zotero.Promise.all([promise1, promise2, promise3]);
|
|
})
|
|
|
|
it("should roll back on error", function* () {
|
|
yield Zotero.DB.queryAsync("INSERT INTO " + tmpTable + " VALUES (1)");
|
|
try {
|
|
yield Zotero.DB.executeTransaction(async function () {
|
|
await Zotero.DB.queryAsync("INSERT INTO " + tmpTable + " VALUES (2)");
|
|
throw 'Aborting transaction -- ignore';
|
|
});
|
|
}
|
|
catch (e) {
|
|
if (typeof e != 'string' || !e.startsWith('Aborting transaction')) throw e;
|
|
}
|
|
var count = yield Zotero.DB.valueQueryAsync("SELECT COUNT(*) FROM " + tmpTable + "");
|
|
assert.equal(count, 1);
|
|
|
|
var conn = yield Zotero.DB._getConnectionAsync();
|
|
assert.isFalse(conn.transactionInProgress);
|
|
|
|
yield Zotero.DB.queryAsync("DROP TABLE " + tmpTable);
|
|
});
|
|
|
|
it("should run onRollback callbacks", function* () {
|
|
var callbackRan = false;
|
|
try {
|
|
yield Zotero.DB.executeTransaction(
|
|
async function () {
|
|
await Zotero.DB.queryAsync("INSERT INTO " + tmpTable + " VALUES (1)");
|
|
throw 'Aborting transaction -- ignore';
|
|
},
|
|
{
|
|
onRollback: function () {
|
|
callbackRan = true;
|
|
}
|
|
}
|
|
);
|
|
}
|
|
catch (e) {
|
|
if (typeof e != 'string' || !e.startsWith('Aborting transaction')) throw e;
|
|
}
|
|
assert.ok(callbackRan);
|
|
|
|
yield Zotero.DB.queryAsync("DROP TABLE " + tmpTable);
|
|
});
|
|
|
|
it("should time out on nested transactions", async function () {
|
|
var e;
|
|
await Zotero.DB.executeTransaction(async function () {
|
|
e = await getPromiseError(
|
|
Promise.race([
|
|
Zotero.Promise.delay(250).then(() => {
|
|
var e = new Error;
|
|
e.name = "TimeoutError";
|
|
throw e;
|
|
}),
|
|
Zotero.DB.executeTransaction(async function () {})
|
|
])
|
|
);
|
|
});
|
|
assert.ok(e);
|
|
assert.equal(e.name, "TimeoutError");
|
|
});
|
|
|
|
it("should run onRollback callbacks for timed-out nested transactions", function* () {
|
|
var callback1Ran = false;
|
|
var callback2Ran = false;
|
|
try {
|
|
yield Zotero.DB.executeTransaction(async function () {
|
|
await Zotero.DB.executeTransaction(
|
|
async function () {},
|
|
{
|
|
waitTimeout: 100,
|
|
onRollback: function () {
|
|
callback1Ran = true;
|
|
}
|
|
}
|
|
)
|
|
},
|
|
{
|
|
onRollback: function () {
|
|
callback2Ran = true;
|
|
}
|
|
});
|
|
}
|
|
catch (e) {
|
|
if (e.name != "TimeoutError") throw e;
|
|
}
|
|
assert.ok(callback1Ran);
|
|
assert.ok(callback2Ran);
|
|
});
|
|
})
|
|
|
|
|
|
describe("#columnExists()", function () {
|
|
it("should return true if a column exists", async function () {
|
|
assert.isTrue(await Zotero.DB.columnExists('items', 'itemID'));
|
|
});
|
|
|
|
it("should return false if a column doesn't exists", async function () {
|
|
assert.isFalse(await Zotero.DB.columnExists('items', 'foo'));
|
|
});
|
|
|
|
it("should return false if a table doesn't exists", async function () {
|
|
assert.isFalse(await Zotero.DB.columnExists('foo', 'itemID'));
|
|
});
|
|
});
|
|
|
|
|
|
describe("#indexExists()", function () {
|
|
it("should return true if an index exists", async function () {
|
|
assert.isTrue(await Zotero.DB.indexExists('items_synced'));
|
|
});
|
|
|
|
it("should return false if an index doesn't exists", async function () {
|
|
assert.isFalse(await Zotero.DB.indexExists('foo'));
|
|
});
|
|
});
|
|
|
|
|
|
describe("#parseSQLFile", function () {
|
|
it("should extract tables and indexes from userdata SQL file", async function () {
|
|
var sql = Zotero.File.getResource(`resource://zotero/schema/userdata.sql`);
|
|
var statements = await Zotero.DB.parseSQLFile(sql);
|
|
assert.isTrue(statements.some(x => x.startsWith('CREATE TABLE items')));
|
|
});
|
|
});
|
|
});
|