Delete child annotations when deleting attachment

And fix a FK definition that could result in orphaned `items` rows
without `itemAnnotations` rows after an attachment was deleted.
This commit is contained in:
Dan Stillman 2021-03-12 06:28:37 -05:00
parent 0bc6b2ccc6
commit 76d504c564
3 changed files with 52 additions and 12 deletions

View file

@ -1440,12 +1440,22 @@ Zotero.Item.prototype._saveData = Zotero.Promise.coroutine(function* (env) {
// Make sure parent is a regular item
if (parentItemID) {
let parentItem = yield Zotero.Items.getAsync(parentItemID);
if (!parentItem.isRegularItem()
// Allow embedded-image attachments under notes
&& !(this.isEmbeddedImageAttachment() && parentItem.isNote())
// Allow annotations under attachments
&& !(this.isAnnotation() && parentItem.isFileAttachment())) {
throw new Error(`Parent item ${parentItem.libraryKey} must be a regular item`);
if (!parentItem.isRegularItem()) {
// Allow embedded-image attachments under notes
if (this.isEmbeddedImageAttachment()) {
if (!parentItem.isNote()) {
throw new Error(`Parent item ${parentItem.libraryKey} must a note`);
}
}
// Allow annotations under attachments
else if (this.isAnnotation()) {
if (!parentItem.isFileAttachment()) {
throw new Error(`Parent item ${parentItem.libraryKey} must be a file attachment`);
}
}
else {
throw new Error(`Parent item ${parentItem.libraryKey} must be a regular item`);
}
}
}
@ -4653,13 +4663,25 @@ Zotero.Item.prototype._eraseData = Zotero.Promise.coroutine(function* (env) {
}
}
// Zotero.Sync.EventListeners.ChangeListener needs to know if this was a storage file
env.notifierData[this.id].storageDeleteLog = this.isStoredFileAttachment();
if (this.isFileAttachment()) {
// Delete child annotations
let sql = "SELECT itemID FROM itemAnnotations WHERE parentItemID=?";
let toDelete = yield Zotero.DB.columnQueryAsync(sql, [this.id]);
for (let i = 0; i < toDelete.length; i++) {
let obj = yield this.ObjectsClass.getAsync(toDelete[i]);
yield obj.erase({
skipParentRefresh: true,
skipEditCheck: env.options.skipEditCheck
});
}
// Delete last page index
let id = this._getLastPageIndexSettingKey();
yield Zotero.SyncedSettings.clear(Zotero.Libraries.userLibraryID, id);
}
// Zotero.Sync.EventListeners.ChangeListener needs to know if this was a storage file
env.notifierData[this.id].storageDeleteLog = this.isStoredFileAttachment();
}
// Delete cached file for image annotation
else if (this.isAnnotation()) {

View file

@ -41,7 +41,7 @@ Zotero.Schema = new function(){
// If updating from this userdata version or later, don't show "Upgrading database…" and don't make
// DB backup first. This should be set to false when breaking compatibility or making major changes.
const minorUpdateFrom = false;
const minorUpdateFrom = 112;
var _dbVersions = [];
var _schemaVersions = [];
@ -3250,6 +3250,24 @@ Zotero.Schema = new function(){
yield Zotero.DB.queryAsync("CREATE INDEX itemAttachments_lastProcessedModificationTime ON itemAttachments(lastProcessedModificationTime)");
}
else if (i == 113) {
yield Zotero.DB.queryAsync("ALTER TABLE itemAnnotations RENAME TO itemAnnotationsOld");
yield Zotero.DB.queryAsync("CREATE TABLE itemAnnotations (\n itemID INTEGER PRIMARY KEY,\n parentItemID INT NOT NULL,\n type INTEGER NOT NULL,\n text TEXT,\n comment TEXT,\n color TEXT,\n pageLabel TEXT,\n sortIndex TEXT NOT NULL,\n position TEXT NOT NULL,\n isExternal INT NOT NULL,\n FOREIGN KEY (itemID) REFERENCES items(itemID) ON DELETE CASCADE,\n FOREIGN KEY (parentItemID) REFERENCES itemAttachments(itemID)\n)");
yield Zotero.DB.queryAsync("INSERT INTO itemAnnotations SELECT * FROM itemAnnotationsOld");
yield Zotero.DB.queryAsync("DROP TABLE itemAnnotationsOld");
yield Zotero.DB.queryAsync("CREATE INDEX itemAnnotations_parentItemID ON itemAnnotations(parentItemID)");
let annotationID = parseInt((yield Zotero.DB.valueQueryAsync(
"SELECT itemTypeID FROM itemTypes WHERE typeName='annotation'"
)) || -1);
let syncObjectTypeID = yield Zotero.DB.valueQueryAsync("SELECT syncObjectTypeID FROM syncObjectTypes WHERE name='item'");
let rows = yield Zotero.DB.queryAsync("SELECT libraryID, key FROM items WHERE itemTypeID=? AND itemID NOT IN (SELECT itemID FROM itemAnnotations)", annotationID);
for (let row of rows) {
yield Zotero.DB.queryAsync("REPLACE INTO syncDeleteLog (syncObjectTypeID, libraryID, key) VALUES (?, ?, ?)", [syncObjectTypeID, row.libraryID, row.key]);
}
yield Zotero.DB.queryAsync("DELETE FROM items WHERE itemTypeID=? AND itemID NOT IN (SELECT itemID FROM itemAnnotations)", annotationID);
}
// If breaking compatibility or doing anything dangerous, clear minorUpdateFrom
}

View file

@ -1,4 +1,4 @@
-- 112
-- 113
-- Copyright (c) 2009 Center for History and New Media
-- George Mason University, Fairfax, Virginia, USA
@ -127,7 +127,7 @@ CREATE TABLE itemAnnotations (
position TEXT NOT NULL,
isExternal INT NOT NULL,
FOREIGN KEY (itemID) REFERENCES items(itemID) ON DELETE CASCADE,
FOREIGN KEY (parentItemID) REFERENCES itemAttachments(itemID) ON DELETE CASCADE
FOREIGN KEY (parentItemID) REFERENCES itemAttachments(itemID)
);
CREATE INDEX itemAnnotations_parentItemID ON itemAnnotations(parentItemID);