Fix invalid collection nesting in DB integrity check
It shouldn't be possible to nest two collections inside each other, but if it happens, fix it in the integrity check. Also detect it from CollectionTreeView::expandToCollection() (used when showing the collections containing an item) and crash Zotero with a flag to run an integrity check after restart. Previously, this would result in an infinite loop. This may be the cause of some of the collection disappearances people have reported. If parentCollectionID never leads to a null, the collection won't appear anywhere in the tree. TODO: - Figure out how this is happening - Detect and fix it automatically for people it's happened to
This commit is contained in:
parent
81739c7a66
commit
0679809735
3 changed files with 132 additions and 7 deletions
|
@ -1039,7 +1039,15 @@ Zotero.CollectionTreeView.prototype.expandToCollection = Zotero.Promise.coroutin
|
|||
}
|
||||
var path = [];
|
||||
var parentID;
|
||||
var seen = new Set([col.id])
|
||||
while (parentID = col.parentID) {
|
||||
// Detect infinite loop due to invalid nesting in DB
|
||||
if (seen.has(parentID)) {
|
||||
yield Zotero.Schema.requireIntegrityCheck();
|
||||
Zotero.crash();
|
||||
return;
|
||||
}
|
||||
seen.add(parentID);
|
||||
path.unshift(parentID);
|
||||
col = yield Zotero.Collections.getAsync(parentID);
|
||||
}
|
||||
|
|
|
@ -139,9 +139,7 @@ Zotero.Schema = new function(){
|
|||
}
|
||||
|
||||
// Check if DB is coming from the DB Repair Tool and should be checked
|
||||
var integrityCheck = await Zotero.DB.valueQueryAsync(
|
||||
"SELECT value FROM settings WHERE setting='db' AND key='integrityCheck'"
|
||||
);
|
||||
var integrityCheck = await this.integrityCheckRequired();
|
||||
|
||||
// Check whether bundled global schema file is newer than DB
|
||||
var bundledGlobalSchema = await _readGlobalSchemaFromFile();
|
||||
|
@ -193,12 +191,9 @@ Zotero.Schema = new function(){
|
|||
await _updateCustomTables();
|
||||
}
|
||||
|
||||
// Auto-repair databases coming from the DB Repair Tool
|
||||
// Auto-repair databases flagged for repair or coming from the DB Repair Tool
|
||||
if (integrityCheck) {
|
||||
await this.integrityCheck(true);
|
||||
await Zotero.DB.queryAsync(
|
||||
"DELETE FROM settings WHERE setting='db' AND key='integrityCheck'"
|
||||
);
|
||||
}
|
||||
|
||||
updated = await _migrateUserDataSchema(userdata, options);
|
||||
|
@ -1664,6 +1659,25 @@ Zotero.Schema = new function(){
|
|||
});
|
||||
|
||||
|
||||
this.integrityCheckRequired = async function () {
|
||||
return !!await Zotero.DB.valueQueryAsync(
|
||||
"SELECT value FROM settings WHERE setting='db' AND key='integrityCheck'"
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
this.setIntegrityCheckRequired = async function (required) {
|
||||
var sql;
|
||||
if (required) {
|
||||
sql = "REPLACE INTO settings VALUES ('db', 'integrityCheck', 1)";
|
||||
}
|
||||
else {
|
||||
sql = "DELETE FROM settings WHERE setting='db' AND key='integrityCheck'";
|
||||
}
|
||||
await Zotero.DB.queryAsync(sql);
|
||||
};
|
||||
|
||||
|
||||
this.integrityCheck = Zotero.Promise.coroutine(function* (fix) {
|
||||
Zotero.debug("Checking database integrity");
|
||||
|
||||
|
@ -1787,6 +1801,74 @@ Zotero.Schema = new function(){
|
|||
let userID = await Zotero.DB.valueQueryAsync("SELECT value FROM settings WHERE setting='account' AND key='userID'");
|
||||
await Zotero.DB.queryAsync("UPDATE settings SET value=? WHERE setting='account' AND key='userID'", parseInt(userID.trim()));
|
||||
}
|
||||
],
|
||||
// Invalid collections nesting
|
||||
[
|
||||
async function () {
|
||||
let rows = await Zotero.DB.queryAsync(
|
||||
"SELECT collectionID, parentCollectionID FROM collections"
|
||||
);
|
||||
let map = new Map();
|
||||
let ids = [];
|
||||
for (let row of rows) {
|
||||
map.set(row.collectionID, row.parentCollectionID);
|
||||
ids.push(row.collectionID);
|
||||
}
|
||||
for (let id of ids) {
|
||||
// Keep track of collections we've seen
|
||||
let seen = new Set([id]);
|
||||
while (true) {
|
||||
let parent = map.get(id);
|
||||
if (!parent) {
|
||||
break;
|
||||
}
|
||||
if (seen.has(parent)) {
|
||||
Zotero.debug(`Collection ${id} parent ${parent} was already seen`, 2);
|
||||
return true;
|
||||
}
|
||||
seen.add(parent);
|
||||
id = parent;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
},
|
||||
async function () {
|
||||
let fix = async function () {
|
||||
let rows = await Zotero.DB.queryAsync(
|
||||
"SELECT collectionID, parentCollectionID FROM collections"
|
||||
);
|
||||
let map = new Map();
|
||||
let ids = [];
|
||||
for (let row of rows) {
|
||||
map.set(row.collectionID, row.parentCollectionID);
|
||||
ids.push(row.collectionID);
|
||||
}
|
||||
for (let id of ids) {
|
||||
let seen = new Set([id]);
|
||||
while (true) {
|
||||
let parent = map.get(id);
|
||||
if (!parent) {
|
||||
break;
|
||||
}
|
||||
if (seen.has(parent)) {
|
||||
await Zotero.DB.queryAsync(
|
||||
"UPDATE collections SET parentCollectionID = NULL "
|
||||
+ "WHERE collectionID = ?",
|
||||
id
|
||||
);
|
||||
// Restart
|
||||
return true;
|
||||
}
|
||||
seen.add(parent);
|
||||
id = parent;
|
||||
}
|
||||
}
|
||||
// Done
|
||||
return false;
|
||||
};
|
||||
|
||||
while (await fix()) {}
|
||||
}
|
||||
]
|
||||
];
|
||||
|
||||
|
@ -1826,12 +1908,20 @@ Zotero.Schema = new function(){
|
|||
}
|
||||
catch (e) {
|
||||
Zotero.logError(e);
|
||||
// Clear flag on failure, to avoid showing an error on every startup if someone
|
||||
// doesn't know how to deal with it
|
||||
await setIntegrityCheckRequired(false);
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Clear flag on success
|
||||
if (fix) {
|
||||
await setIntegrityCheckRequired(false);
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
|
|
|
@ -97,5 +97,32 @@ describe("Zotero.Schema", function() {
|
|||
yield assert.eventually.isTrue(Zotero.Schema.integrityCheck(true));
|
||||
yield assert.eventually.isTrue(Zotero.Schema.integrityCheck());
|
||||
})
|
||||
|
||||
it("should repair invalid nesting between two collections", async function () {
|
||||
var c1 = await createDataObject('collection');
|
||||
var c2 = await createDataObject('collection', { parentID: c1.id });
|
||||
await Zotero.DB.queryAsync(
|
||||
"UPDATE collections SET parentCollectionID=? WHERE collectionID=?",
|
||||
[c2.id, c1.id]
|
||||
);
|
||||
|
||||
await assert.isFalse(await Zotero.Schema.integrityCheck());
|
||||
await assert.isTrue(await Zotero.Schema.integrityCheck(true));
|
||||
await assert.isTrue(await Zotero.Schema.integrityCheck());
|
||||
});
|
||||
|
||||
it("should repair invalid nesting between three collections", async function () {
|
||||
var c1 = await createDataObject('collection');
|
||||
var c2 = await createDataObject('collection', { parentID: c1.id });
|
||||
var c3 = await createDataObject('collection', { parentID: c2.id });
|
||||
await Zotero.DB.queryAsync(
|
||||
"UPDATE collections SET parentCollectionID=? WHERE collectionID=?",
|
||||
[c3.id, c2.id]
|
||||
);
|
||||
|
||||
await assert.isFalse(await Zotero.Schema.integrityCheck());
|
||||
await assert.isTrue(await Zotero.Schema.integrityCheck(true));
|
||||
await assert.isTrue(await Zotero.Schema.integrityCheck());
|
||||
});
|
||||
})
|
||||
})
|
||||
|
|
Loading…
Reference in a new issue