www

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

schema.js (127018B)


      1 /*
      2     ***** BEGIN LICENSE BLOCK *****
      3     
      4     Copyright © 2009 Center for History and New Media
      5                      George Mason University, Fairfax, Virginia, USA
      6                      http://zotero.org
      7     
      8     This file is part of Zotero.
      9     
     10     Zotero is free software: you can redistribute it and/or modify
     11     it under the terms of the GNU Affero General Public License as published by
     12     the Free Software Foundation, either version 3 of the License, or
     13     (at your option) any later version.
     14     
     15     Zotero is distributed in the hope that it will be useful,
     16     but WITHOUT ANY WARRANTY; without even the implied warranty of
     17     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
     18     GNU Affero General Public License for more details.
     19     
     20     You should have received a copy of the GNU Affero General Public License
     21     along with Zotero.  If not, see <http://www.gnu.org/licenses/>.
     22     
     23     ***** END LICENSE BLOCK *****
     24 */
     25 
     26 Zotero.Schema = new function(){
     27 	this.dbInitialized = false;
     28 	this.goToChangeLog = false;
     29 	
     30 	this.REPO_UPDATE_MANUAL = 1;
     31 	this.REPO_UPDATE_UPGRADE = 2;
     32 	this.REPO_UPDATE_STARTUP = 3;
     33 	this.REPO_UPDATE_NOTIFICATION = 4;
     34 	
     35 	var _schemaUpdateDeferred = Zotero.Promise.defer();
     36 	this.schemaUpdatePromise = _schemaUpdateDeferred.promise;
     37 	
     38 	// If updating from this userdata version or later, don't show "Upgrading database…" and don't make
     39 	// DB backup first. This should be set to false when breaking compatibility or making major changes.
     40 	const minorUpdateFrom = 95;
     41 	
     42 	var _dbVersions = [];
     43 	var _schemaVersions = [];
     44 	// Update when adding _updateCompatibility() line to schema update step
     45 	var _maxCompatibility = 5;
     46 	
     47 	var _repositoryTimerID;
     48 	var _repositoryNotificationTimerID;
     49 	var _nextRepositoryUpdate;
     50 	var _remoteUpdateInProgress = false;
     51 	var _localUpdateInProgress = false;
     52 	
     53 	var self = this;
     54 	
     55 	/*
     56 	 * Retrieve the DB schema version
     57 	 */
     58 	this.getDBVersion = function (schema) {
     59 		if (_dbVersions[schema]){
     60 			return Zotero.Promise.resolve(_dbVersions[schema]);
     61 		}
     62 		
     63 		var sql = "SELECT version FROM version WHERE schema='" + schema + "'";
     64 		return Zotero.DB.valueQueryAsync(sql)
     65 		.then(function (dbVersion) {
     66 			if (dbVersion) {
     67 				dbVersion = parseInt(dbVersion);
     68 				_dbVersions[schema] = dbVersion;
     69 			}
     70 			return dbVersion;
     71 		})
     72 		.catch(function (e) {
     73 			return Zotero.DB.tableExists('version')
     74 			.then(function (exists) {
     75 				if (exists) {
     76 					throw e;
     77 				}
     78 				return false;
     79 			});
     80 		});
     81 	}
     82 	
     83 	
     84 	/*
     85 	 * Checks if the DB schema exists and is up-to-date, updating if necessary
     86 	 */
     87 	this.updateSchema = Zotero.Promise.coroutine(function* (options = {}) {
     88 		// TODO: Check database integrity first with Zotero.DB.integrityCheck()
     89 		
     90 		// 'userdata' is the last upgrade step run in _migrateUserDataSchema() based on the
     91 		// version in the schema file. Upgrade steps may or may not break DB compatibility.
     92 		//
     93 		// 'compatibility' is incremented manually by upgrade steps in order to break DB
     94 		// compatibility with older versions.
     95 		var versions = yield Zotero.Promise.all([
     96 			this.getDBVersion('userdata'), this.getDBVersion('compatibility')
     97 		]);
     98 		var [userdata, compatibility] = versions;
     99 		if (!userdata) {
    100 			Zotero.debug('Database does not exist -- creating\n');
    101 			return _initializeSchema()
    102 			.then(function() {
    103 				(Zotero.isStandalone ? Zotero.uiReadyPromise : Zotero.initializationPromise)
    104 				.then(1000)
    105 				.then(async function () {
    106 					await this.updateBundledFiles();
    107 					if (Zotero.Prefs.get('automaticScraperUpdates')) {
    108 						try {
    109 							await this.updateFromRepository(this.REPO_UPDATE_UPGRADE);
    110 						}
    111 						catch (e) {
    112 							Zotero.logError(e);
    113 						}
    114 					}
    115 					_schemaUpdateDeferred.resolve(true);
    116 				}.bind(this))
    117 			}.bind(this));
    118 		}
    119 		
    120 		// We don't handle upgrades from pre-Zotero 2.1 databases
    121 		if (userdata < 76) {
    122 			let msg = Zotero.getString('upgrade.nonupgradeableDB1')
    123 				+ "\n\n" + Zotero.getString('upgrade.nonupgradeableDB2', "4.0");
    124 			throw new Error(msg);
    125 		}
    126 		
    127 		if (compatibility > _maxCompatibility) {
    128 			let dbClientVersion = yield Zotero.DB.valueQueryAsync(
    129 				"SELECT value FROM settings "
    130 				+ "WHERE setting='client' AND key='lastCompatibleVersion'"
    131 			);
    132 			let msg = "Database is incompatible with this Zotero version "
    133 				+ `(${compatibility} > ${_maxCompatibility})`
    134 			throw new Zotero.DB.IncompatibleVersionException(msg, dbClientVersion);
    135 		}
    136 		
    137 		// Check if DB is coming from the DB Repair Tool and should be checked
    138 		var integrityCheck = yield Zotero.DB.valueQueryAsync(
    139 			"SELECT value FROM settings WHERE setting='db' AND key='integrityCheck'"
    140 		);
    141 		
    142 		var schemaVersion = yield _getSchemaSQLVersion('userdata');
    143 		options.minor = minorUpdateFrom && userdata >= minorUpdateFrom;
    144 		
    145 		// If non-minor userdata upgrade, make backup of database first
    146 		if (userdata < schemaVersion && !options.minor) {
    147 			yield Zotero.DB.backupDatabase(userdata, true);
    148 		}
    149 		else if (integrityCheck) {
    150 			yield Zotero.DB.backupDatabase(false, true);
    151 		}
    152 		
    153 		yield Zotero.DB.queryAsync("PRAGMA foreign_keys = false");
    154 		try {
    155 			var updated = yield Zotero.DB.executeTransaction(function* (conn) {
    156 				var updated = yield _updateSchema('system');
    157 				
    158 				// Update custom tables if they exist so that changes are in
    159 				// place before user data migration
    160 				if (Zotero.DB.tableExists('customItemTypes')) {
    161 					yield _updateCustomTables(updated);
    162 				}
    163 				
    164 				// Auto-repair databases coming from the DB Repair Tool
    165 				if (integrityCheck) {
    166 					yield this.integrityCheck(true);
    167 					yield Zotero.DB.queryAsync(
    168 						"DELETE FROM settings WHERE setting='db' AND key='integrityCheck'"
    169 					);
    170 				}
    171 				
    172 				updated = yield _migrateUserDataSchema(userdata, options);
    173 				yield _updateSchema('triggers');
    174 				
    175 				// Populate combined tables for custom types and fields -- this is likely temporary
    176 				//
    177 				// We do this again in case custom fields were changed during user data migration
    178 				yield _updateCustomTables()
    179 				
    180 				return updated;
    181 			}.bind(this));
    182 		}
    183 		finally {
    184 			yield Zotero.DB.queryAsync("PRAGMA foreign_keys = true");
    185 		}
    186 		
    187 		if (updated) {
    188 			// Upgrade seems to have been a success -- delete any previous backups
    189 			var maxPrevious = userdata - 1;
    190 			var file = Zotero.File.pathToFile(Zotero.DataDirectory.dir);
    191 			var toDelete = [];
    192 			try {
    193 				var files = file.directoryEntries;
    194 				while (files.hasMoreElements()) {
    195 					var file = files.getNext();
    196 					file.QueryInterface(Components.interfaces.nsIFile);
    197 					if (file.isDirectory()) {
    198 						continue;
    199 					}
    200 					var matches = file.leafName.match(/zotero\.sqlite\.([0-9]{2,})\.bak/);
    201 					if (!matches) {
    202 						continue;
    203 					}
    204 					if (matches[1]>=28 && matches[1]<=maxPrevious) {
    205 						toDelete.push(file);
    206 					}
    207 				}
    208 				for (let file of toDelete) {
    209 					Zotero.debug('Removing previous backup file ' + file.leafName);
    210 					file.remove(false);
    211 				}
    212 			}
    213 			catch (e) {
    214 				Zotero.debug(e);
    215 			}
    216 		}
    217 		
    218 		// Reset sync queue tries if new version
    219 		yield _checkClientVersion();
    220 		
    221 		// In Standalone, don't load bundled files until after UI is ready. In Firefox, load them as
    222 		// soon initialization is done so that translation works before the Zotero pane is opened.
    223 		(Zotero.isStandalone ? Zotero.uiReadyPromise : Zotero.initializationPromise)
    224 		.then(1000)
    225 		.then(async function () {
    226 			await this.updateBundledFiles();
    227 			if (Zotero.Prefs.get('automaticScraperUpdates')) {
    228 				try {
    229 					await this.updateFromRepository(this.REPO_UPDATE_STARTUP);
    230 				}
    231 				catch (e) {
    232 					Zotero.logError(e);
    233 				}
    234 			}
    235 			_schemaUpdateDeferred.resolve(true);
    236 		}.bind(this));
    237 		
    238 		return updated;
    239 	});
    240 	
    241 	
    242 	// https://www.zotero.org/support/nsf
    243 	//
    244 	// This is mostly temporary
    245 	// TEMP - NSF
    246 	this.importSchema = Zotero.Promise.coroutine(function* (str, uri) {
    247 		var ps = Components.classes["@mozilla.org/embedcomp/prompt-service;1"]
    248 								.getService(Components.interfaces.nsIPromptService);
    249 		
    250 		if (!uri.match(/https?:\/\/([^\.]+\.)?zotero.org\//)) {
    251 			Zotero.debug("Ignoring schema file from non-zotero.org domain");
    252 			return;
    253 		}
    254 		
    255 		str = str.trim();
    256 		
    257 		Zotero.debug(str);
    258 		
    259 		if (str == "%%%ZOTERO_NSF_TEMP_INSTALL%%%") {
    260 			Zotero.debug(Zotero.ItemTypes.getID("nsfReviewer"));
    261 			if (Zotero.ItemTypes.getID("nsfReviewer")) {
    262 				ps.alert(null, "Zotero Item Type Already Exists", "The 'NSF Reviewer' item type already exists in Zotero.");
    263 				Zotero.debug("nsfReviewer item type already exists");
    264 				return;
    265 			}
    266 			
    267 			Zotero.debug("Installing nsfReviewer item type");
    268 			
    269 			var itemTypeID = Zotero.ID.get('customItemTypes');
    270 			
    271 			yield Zotero.DB.executeTransaction(function* () {
    272 				yield Zotero.DB.queryAsync("INSERT INTO customItemTypes VALUES (?, 'nsfReviewer', 'NSF Reviewer', 1, 'chrome://zotero/skin/report_user.png')", itemTypeID);
    273 				
    274 				var fields = [
    275 					['name', 'Name'],
    276 					['institution', 'Institution'],
    277 					['address', 'Address'],
    278 					['telephone', 'Telephone'],
    279 					['email', 'Email'],
    280 					['homepage', 'Webpage'],
    281 					['discipline', 'Discipline'],
    282 					['nsfID', 'NSF ID'],
    283 					['dateSent', 'Date Sent'],
    284 					['dateDue', 'Date Due'],
    285 					['accepted', 'Accepted'],
    286 					['programDirector', 'Program Director']
    287 				];
    288 				for (var i=0; i<fields.length; i++) {
    289 					var fieldID = Zotero.ItemFields.getID(fields[i][0]);
    290 					if (!fieldID) {
    291 						var fieldID = Zotero.ID.get('customFields');
    292 						yield Zotero.DB.queryAsync("INSERT INTO customFields VALUES (?, ?, ?)", [fieldID, fields[i][0], fields[i][1]]);
    293 						yield Zotero.DB.queryAsync("INSERT INTO customItemTypeFields VALUES (?, NULL, ?, 1, ?)", [itemTypeID, fieldID, i+1]);
    294 					}
    295 					else {
    296 						yield Zotero.DB.queryAsync("INSERT INTO customItemTypeFields VALUES (?, ?, NULL, 1, ?)", [itemTypeID, fieldID, i+1]);
    297 					}
    298 					
    299 					switch (fields[i][0]) {
    300 						case 'name':
    301 							var baseFieldID = 110; // title
    302 							break;
    303 						
    304 						case 'dateSent':
    305 							var baseFieldID = 14; // date
    306 							break;
    307 						
    308 						case 'homepage':
    309 							var baseFieldID = 1; // URL
    310 							break;
    311 						
    312 						default:
    313 							var baseFieldID = null;
    314 					}
    315 					
    316 					if (baseFieldID) {
    317 						yield Zotero.DB.queryAsync("INSERT INTO customBaseFieldMappings VALUES (?, ?, ?)", [itemTypeID, baseFieldID, fieldID]);
    318 					}
    319 				}
    320 				
    321 				yield _reloadSchema();
    322 			});
    323 			
    324 			var s = new Zotero.Search;
    325 			s.name = "Overdue NSF Reviewers";
    326 			s.addCondition('itemType', 'is', 'nsfReviewer');
    327 			s.addCondition('dateDue', 'isBefore', 'today');
    328 			s.addCondition('tag', 'isNot', 'Completed');
    329 			yield s.saveTx();
    330 			
    331 			ps.alert(null, "Zotero Item Type Added", "The 'NSF Reviewer' item type and 'Overdue NSF Reviewers' saved search have been installed.");
    332 		}
    333 		else if (str == "%%%ZOTERO_NSF_TEMP_UNINSTALL%%%") {
    334 			var itemTypeID = Zotero.ItemTypes.getID('nsfReviewer');
    335 			if (!itemTypeID) {
    336 				ps.alert(null, "Zotero Item Type Does Not Exist", "The 'NSF Reviewer' item type does not exist in Zotero.");
    337 				Zotero.debug("nsfReviewer item types doesn't exist", 2);
    338 				return;
    339 			}
    340 			
    341 			var s = new Zotero.Search;
    342 			s.addCondition('itemType', 'is', 'nsfReviewer');
    343 			var s2 = new Zotero.Search;
    344 			s2.addCondition('itemType', 'is', 'nsfReviewer');
    345 			s2.addCondition('deleted', 'true');
    346 			if ((yield s.search()).length || (yield s2.search()).length) {
    347 				ps.alert(null, "Error", "All 'NSF Reviewer' items must be deleted before the item type can be removed from Zotero.");
    348 				return;
    349 			}
    350 			
    351 			Zotero.debug("Uninstalling nsfReviewer item type");
    352 			yield Zotero.DB.executeTransaction(function* () {
    353 				yield Zotero.DB.queryAsync("DELETE FROM customItemTypeFields WHERE customItemTypeID=?", itemTypeID - Zotero.ItemTypes.customIDOffset);
    354 				yield Zotero.DB.queryAsync("DELETE FROM customBaseFieldMappings WHERE customItemTypeID=?", itemTypeID - Zotero.ItemTypes.customIDOffset);
    355 				var fields = Zotero.ItemFields.getItemTypeFields(itemTypeID);
    356 				for (let fieldID of fields) {
    357 					if (Zotero.ItemFields.isCustom(fieldID)) {
    358 						yield Zotero.DB.queryAsync("DELETE FROM customFields WHERE customFieldID=?", fieldID - Zotero.ItemTypes.customIDOffset);
    359 					}
    360 				}
    361 				yield Zotero.DB.queryAsync("DELETE FROM customItemTypes WHERE customItemTypeID=?", itemTypeID - Zotero.ItemTypes.customIDOffset);
    362 				
    363 				var searches = Zotero.Searches.getByLibrary(Zotero.Libraries.userLibraryID);
    364 				for (let search of searches) {
    365 					if (search.name == 'Overdue NSF Reviewers') {
    366 						yield search.erase();
    367 					}
    368 				}
    369 				
    370 				yield _reloadSchema();
    371 			}.bind(this));
    372 			
    373 			ps.alert(null, "Zotero Item Type Removed", "The 'NSF Reviewer' item type has been uninstalled.");
    374 		}
    375 	});
    376 	
    377 	var _reloadSchema = Zotero.Promise.coroutine(function* () {
    378 		yield _updateCustomTables();
    379 		yield Zotero.ItemTypes.init();
    380 		yield Zotero.ItemFields.init();
    381 		yield Zotero.SearchConditions.init();
    382 		
    383 		// Update item type menus in every open window
    384 		var wm = Components.classes["@mozilla.org/appshell/window-mediator;1"]
    385 					.getService(Components.interfaces.nsIWindowMediator);
    386 		var enumerator = wm.getEnumerator("navigator:browser");
    387 		while (enumerator.hasMoreElements()) {
    388 			var win = enumerator.getNext();
    389 			win.ZoteroPane.buildItemTypeSubMenu();
    390 			win.document.getElementById('zotero-editpane-item-box').buildItemTypeMenu();
    391 		}
    392 	});
    393 	
    394 	
    395 	var _updateCustomTables = Zotero.Promise.coroutine(function* (skipDelete, skipSystem) {
    396 		Zotero.debug("Updating custom tables");
    397 		
    398 		Zotero.DB.requireTransaction();
    399 		
    400 		if (!skipDelete) {
    401 			yield Zotero.DB.queryAsync("DELETE FROM itemTypesCombined");
    402 			yield Zotero.DB.queryAsync("DELETE FROM fieldsCombined WHERE fieldID NOT IN (SELECT fieldID FROM itemData)");
    403 			yield Zotero.DB.queryAsync("DELETE FROM itemTypeFieldsCombined");
    404 			yield Zotero.DB.queryAsync("DELETE FROM baseFieldMappingsCombined");
    405 		}
    406 		
    407 		var offset = Zotero.ItemTypes.customIDOffset;
    408 		yield Zotero.DB.queryAsync(
    409 			"INSERT INTO itemTypesCombined "
    410 				+ (
    411 					skipSystem
    412 					? ""
    413 					: "SELECT itemTypeID, typeName, display, 0 AS custom FROM itemTypes UNION "
    414 				)
    415 				+ "SELECT customItemTypeID + " + offset + " AS itemTypeID, typeName, display, 1 AS custom FROM customItemTypes"
    416 		);
    417 		yield Zotero.DB.queryAsync(
    418 			"INSERT OR IGNORE INTO fieldsCombined "
    419 				+ (
    420 					skipSystem
    421 					? ""
    422 					: "SELECT fieldID, fieldName, NULL AS label, fieldFormatID, 0 AS custom FROM fields UNION "
    423 				)
    424 				+ "SELECT customFieldID + " + offset + " AS fieldID, fieldName, label, NULL, 1 AS custom FROM customFields"
    425 		);
    426 		yield Zotero.DB.queryAsync(
    427 			"INSERT INTO itemTypeFieldsCombined "
    428 				+ (
    429 					skipSystem
    430 					? ""
    431 					: "SELECT itemTypeID, fieldID, hide, orderIndex FROM itemTypeFields UNION "
    432 				)
    433 				+ "SELECT customItemTypeID + " + offset + " AS itemTypeID, "
    434 					+ "COALESCE(fieldID, customFieldID + " + offset + ") AS fieldID, hide, orderIndex FROM customItemTypeFields"
    435 		);
    436 		yield Zotero.DB.queryAsync(
    437 			"INSERT INTO baseFieldMappingsCombined "
    438 				+ (
    439 					skipSystem
    440 					? ""
    441 					: "SELECT itemTypeID, baseFieldID, fieldID FROM baseFieldMappings UNION "
    442 				)
    443 				+ "SELECT customItemTypeID + " + offset + " AS itemTypeID, baseFieldID, "
    444 					+ "customFieldID + " + offset + " AS fieldID FROM customBaseFieldMappings"
    445 		);
    446 	});
    447 	
    448 	
    449 	/**
    450 	 * Update styles and translators in data directory with versions from
    451 	 * ZIP file (XPI) or directory (source) in extension directory
    452 	 *
    453 	 * @param {String} [mode] - 'translators' or 'styles'
    454 	 * @return {Promise}
    455 	 */
    456 	this.updateBundledFiles = Zotero.Promise.coroutine(function* (mode) {
    457 		if (Zotero.skipBundledFiles) {
    458 			Zotero.debug("Skipping bundled file installation");
    459 			return;
    460 		}
    461 		
    462 		if (_localUpdateInProgress) {
    463 			Zotero.debug("Bundled file update already in progress", 2);
    464 			return;
    465 		}
    466 		
    467 		_localUpdateInProgress = true;
    468 		
    469 		try {
    470 			yield Zotero.proxyAuthComplete.delay(1000);
    471 			
    472 			Zotero.debug("Updating bundled " + (mode || "files"));
    473 			
    474 			// Get path to add-on
    475 			
    476 			// Synchronous in Standalone
    477 			if (Zotero.isStandalone) {
    478 				var installLocation = Components.classes["@mozilla.org/file/directory_service;1"]
    479 					.getService(Components.interfaces.nsIProperties)
    480 					.get("AChrom", Components.interfaces.nsIFile).parent;
    481 				installLocation.append("zotero.jar");
    482 			}
    483 			// Asynchronous in Firefox
    484 			else {
    485 				let resolve, reject;
    486 				let promise = new Zotero.Promise(function () {
    487 					resolve = arguments[0];
    488 					reject = arguments[1];
    489 				});
    490 				Components.utils.import("resource://gre/modules/AddonManager.jsm");
    491 				AddonManager.getAddonByID(
    492 					ZOTERO_CONFIG.GUID,
    493 					function (addon) {
    494 						try {
    495 							installLocation = addon.getResourceURI()
    496 								.QueryInterface(Components.interfaces.nsIFileURL).file;
    497 						}
    498 						catch (e) {
    499 							reject(e);
    500 							return;
    501 						}
    502 						resolve();
    503 					}
    504 				);
    505 				yield promise;
    506 			}
    507 			installLocation = installLocation.path;
    508 			
    509 			let initOpts = { fromSchemaUpdate: true };
    510 			
    511 			// Update files
    512 			switch (mode) {
    513 			case 'styles':
    514 				yield Zotero.Styles.init(initOpts);
    515 				var updated = yield _updateBundledFilesAtLocation(installLocation, mode);
    516 				break;
    517 			
    518 			case 'translators':
    519 				yield Zotero.Translators.init(initOpts);
    520 				var updated = yield _updateBundledFilesAtLocation(installLocation, mode);
    521 				break;
    522 			
    523 			default:
    524 				yield Zotero.Translators.init(initOpts);
    525 				let up1 = yield _updateBundledFilesAtLocation(installLocation, 'translators', true);
    526 				yield Zotero.Styles.init(initOpts);
    527 				let up2 = yield _updateBundledFilesAtLocation(installLocation, 'styles');
    528 				var updated = up1 || up2;
    529 			}
    530 		}
    531 		finally {
    532 			_localUpdateInProgress = false;
    533 		}
    534 		
    535 		return updated;
    536 	});
    537 	
    538 	/**
    539 	 * Update bundled files in a given location
    540 	 *
    541 	 * @param {String} installLocation - Path to XPI or source dir
    542 	 * @param {'translators','styles'} mode
    543 	 * @param {Boolean} [skipVersionUpdates=false]
    544 	 */
    545 	var _updateBundledFilesAtLocation = Zotero.Promise.coroutine(function* (installLocation, mode, skipVersionUpdates) {
    546 		Components.utils.import("resource://gre/modules/FileUtils.jsm");
    547 		
    548 		var isUnpacked = (yield OS.File.stat(installLocation)).isDir;
    549 		if(!isUnpacked) {
    550 			var xpiZipReader = Components.classes["@mozilla.org/libjar/zip-reader;1"]
    551 					.createInstance(Components.interfaces.nsIZipReader);
    552 			xpiZipReader.open(new FileUtils.File(installLocation));
    553 		}
    554 		
    555 		switch (mode) {
    556 			case "translators":
    557 				var titleField = 'label';
    558 				var fileExt = ".js";
    559 				var destDir = Zotero.getTranslatorsDirectory().path;
    560 				break;
    561 			
    562 			case "styles":
    563 				var titleField = 'title';
    564 				var fileExt = ".csl";
    565 				var destDir = Zotero.getStylesDirectory().path;
    566 				var hiddenDir = OS.Path.join(destDir, 'hidden');
    567 				break;
    568 			
    569 			default:
    570 				throw new Error("Invalid mode '" + mode + "'");
    571 		}
    572 		
    573 		var modeType = mode.substr(0, mode.length - 1);
    574 		var ModeType = Zotero.Utilities.capitalize(modeType);
    575 		var Mode = Zotero.Utilities.capitalize(mode);
    576 		
    577 		var repotime = yield Zotero.File.getContentsFromURLAsync("resource://zotero/schema/repotime.txt");
    578 		var date = Zotero.Date.sqlToDate(repotime.trim(), true);
    579 		repotime = Zotero.Date.toUnixTimestamp(date);
    580 		
    581 		var fileNameRE = new RegExp("^[^\.].+\\" + fileExt + "$");
    582 		
    583 		// If directory is empty, force reinstall
    584 		var forceReinstall = true;
    585 		let iterator = new OS.File.DirectoryIterator(destDir);
    586 		try {
    587 			outer:
    588 			while (true) {
    589 				let entries = yield iterator.nextBatch(10);
    590 				if (!entries.length) break;
    591 				for (let i = 0; i < entries.length; i++) {
    592 					let entry = entries[i];
    593 					if (!entry.name.match(fileNameRE) || entry.isDir) {
    594 						continue;
    595 					}
    596 					// Not empty
    597 					forceReinstall = false;
    598 					break outer;
    599 				}
    600 			}
    601 		}
    602 		finally {
    603 			iterator.close();
    604 		}
    605 		
    606 		//
    607 		// Delete obsolete files
    608 		//
    609 		var sql = "SELECT version FROM version WHERE schema='delete'";
    610 		var lastVersion = yield Zotero.DB.valueQueryAsync(sql);
    611 		
    612 		if(isUnpacked) {
    613 			var deleted = OS.Path.join(installLocation, 'deleted.txt');
    614 			// In source builds, deleted.txt is in the translators directory
    615 			if (!(yield OS.File.exists(deleted))) {
    616 				deleted = OS.Path.join(installLocation, 'translators', 'deleted.txt');
    617 				if (!(yield OS.File.exists(deleted))) {
    618 					deleted = false;
    619 				}
    620 			}
    621 		} else {
    622 			var deleted = xpiZipReader.getInputStream("deleted.txt");
    623 		}
    624 		
    625 		let deletedVersion;
    626 		if (deleted) {
    627 			deleted = yield Zotero.File.getContentsAsync(deleted);
    628 			deleted = deleted.match(/^([^\s]+)/gm);
    629 			deletedVersion = deleted.shift();
    630 		}
    631 		
    632 		if (!lastVersion || lastVersion < deletedVersion) {
    633 			var toDelete = [];
    634 			let iterator = new OS.File.DirectoryIterator(destDir);
    635 			try {
    636 				while (true) {
    637 					let entries = yield iterator.nextBatch(10);
    638 					if (!entries.length) break;
    639 					for (let i = 0; i < entries.length; i++) {
    640 						let entry = entries[i];
    641 						
    642 						if ((entry.isSymLink && !(yield OS.File.exists(entry.path))) // symlink to non-existent file
    643 								|| entry.isDir) {
    644 							continue;
    645 						}
    646 						
    647 						if (mode == 'styles') {
    648 							switch (entry.name) {
    649 								// Remove update script (included with 3.0 accidentally)
    650 								case 'update':
    651 								
    652 								// Delete renamed/obsolete files
    653 								case 'chicago-note.csl':
    654 								case 'mhra_note_without_bibliography.csl':
    655 								case 'mhra.csl':
    656 								case 'mla.csl':
    657 									toDelete.push(entry.path);
    658 									continue;
    659 								
    660 								// Be a little more careful with this one, in case someone
    661 								// created a custom 'aaa' style
    662 								case 'aaa.csl':
    663 									let str = yield Zotero.File.getContentsAsync(entry.path, false, 300);
    664 									if (str.indexOf("<title>American Anthropological Association</title>") != -1) {
    665 										toDelete.push(entry.path);
    666 									}
    667 									continue;
    668 							}
    669 						}
    670 						
    671 						if (forceReinstall || !entry.name.match(fileNameRE)) {
    672 							continue;
    673 						}
    674 						
    675 						if (mode == 'translators') {
    676 							// TODO: Change if the APIs change
    677 							let newObj = new Zotero[Mode].loadFromFile(entry.path);
    678 							if (deleted.indexOf(newObj[modeType + "ID"]) == -1) {
    679 								continue;
    680 							}
    681 							toDelete.push(entry.path);
    682 						}
    683 					}
    684 				}
    685 			}
    686 			finally {
    687 				iterator.close();
    688 			}
    689 			
    690 			for (let i = 0; i < toDelete.length; i++) {
    691 				let path = toDelete[i];
    692 				Zotero.debug("Deleting " + path);
    693 				try {
    694 					yield OS.File.remove(path);
    695 				}
    696 				catch (e) {
    697 					Components.utils.reportError(e);
    698 					Zotero.debug(e, 1);
    699 				}
    700 			}
    701 			
    702 			if (!skipVersionUpdates) {
    703 				let sql = "REPLACE INTO version (schema, version) VALUES ('delete', ?)";
    704 				yield Zotero.DB.queryAsync(sql, deletedVersion);
    705 			}
    706 		}
    707 		
    708 		//
    709 		// Update files
    710 		//
    711 		var sql = "SELECT version FROM version WHERE schema=?";
    712 		var lastModTime = yield Zotero.DB.valueQueryAsync(sql, mode);
    713 		// Fix millisecond times (possible in 4.0?)
    714 		if (lastModTime > 9999999999) {
    715 			lastModTime = Math.round(lastModTime / 1000);
    716 		}
    717 		var cache = {};
    718 		
    719 		// XPI installation
    720 		if (!isUnpacked) {
    721 			var modTime = Math.round(
    722 				(yield OS.File.stat(installLocation)).lastModificationDate.getTime() / 1000
    723 			);
    724 			
    725 			if (!forceReinstall && lastModTime && modTime <= lastModTime) {
    726 				Zotero.debug("Installed " + mode + " are up-to-date with XPI");
    727 				return false;
    728 			}
    729 			
    730 			Zotero.debug("Updating installed " + mode + " from XPI");
    731 			
    732 			let tmpDir = Zotero.getTempDirectory().path;
    733 			
    734 			if (mode == 'translators') {
    735 				// Parse translators.json
    736 				if (!xpiZipReader.hasEntry("translators.json")) {
    737 					Zotero.logError("translators.json not found");
    738 					return false;
    739 				}
    740 				let index = JSON.parse(yield Zotero.File.getContentsAsync(
    741 					xpiZipReader.getInputStream("translators.json"))
    742 				);
    743 				for (let id in index) {
    744 					index[id].extract = true;
    745 				}
    746 				
    747 				let sql = "SELECT rowid, fileName, metadataJSON FROM translatorCache";
    748 				let rows = yield Zotero.DB.queryAsync(sql);
    749 				// If there's anything in the cache, see what we actually need to extract
    750 				for (let i = 0; i < rows.length; i++) {
    751 					let json = rows[i].metadataJSON;
    752 					let metadata;
    753 					try {
    754 						metadata = JSON.parse(json);
    755 					}
    756 					catch (e) {
    757 						Zotero.logError(e);
    758 						Zotero.debug(json, 1);
    759 						
    760 						// If JSON is invalid, clear from cache
    761 						yield Zotero.DB.queryAsync(
    762 							"DELETE FROM translatorCache WHERE rowid=?",
    763 							rows[i].rowid
    764 						);
    765 						continue;
    766 					}
    767 					let id = metadata.translatorID;
    768 					if (index[id] && index[id].lastUpdated <= metadata.lastUpdated) {
    769 						index[id].extract = false;
    770 					}
    771 				}
    772 				
    773 				for (let translatorID in index) {
    774 					// Use index file and DB cache for translator entries,
    775 					// extracting only what's necessary
    776 					let entry = index[translatorID];
    777 					if (!entry.extract) {
    778 						//Zotero.debug("Not extracting '" + entry.label + "' -- same version already in cache");
    779 						continue;
    780 					}
    781 					
    782 					let tmpFile = OS.Path.join(tmpDir, entry.fileName)
    783 					yield Zotero.File.removeIfExists(tmpFile);
    784 					xpiZipReader.extract("translators/" + entry.fileName, new FileUtils.File(tmpFile));
    785 					
    786 					var existingObj = Zotero.Translators.get(translatorID);
    787 					if (!existingObj) {
    788 						Zotero.debug("Installing translator '" + entry.label + "'");
    789 					}
    790 					else {
    791 						Zotero.debug("Updating translator '" + existingObj.label + "'");
    792 						yield Zotero.File.removeIfExists(existingObj.path);
    793 					}
    794 					
    795 					let destFile = OS.Path.join(destDir, entry.fileName);
    796 					try {
    797 						yield OS.File.move(tmpFile, destFile, {
    798 							noOverwrite: true
    799 						});
    800 					}
    801 					catch (e) {
    802 						if (e instanceof OS.File.Error && e.becauseExists) {
    803 							// Could overwrite automatically, but we want to log this
    804 							Zotero.warn("Overwriting translator with same filename '"
    805 								+ entry.fileName + "'");
    806 							yield OS.File.move(tmpFile, destFile);
    807 						}
    808 						else {
    809 							throw e;
    810 						}
    811 					}
    812 				}
    813 			}
    814 			// Styles
    815 			else {
    816 				let entries = xpiZipReader.findEntries('styles/*.csl');
    817 				while (entries.hasMore()) {
    818 					let entry = entries.getNext();
    819 					let fileName = entry.substr(7); // strip 'styles/'
    820 					
    821 					let tmpFile = OS.Path.join(tmpDir, fileName);
    822 					yield Zotero.File.removeIfExists(tmpFile);
    823 					xpiZipReader.extract(entry, new FileUtils.File(tmpFile));
    824 					let code = yield Zotero.File.getContentsAsync(tmpFile);
    825 					let newObj = new Zotero.Style(code);
    826 					
    827 					let existingObj = Zotero.Styles.get(newObj[modeType + "ID"]);
    828 					if (!existingObj) {
    829 						Zotero.debug("Installing style '" + newObj[titleField] + "'");
    830 					}
    831 					else {
    832 						Zotero.debug("Updating "
    833 							+ (existingObj.hidden ? "hidden " : "")
    834 							+ "style '" + existingObj[titleField] + "'");
    835 						yield Zotero.File.removeIfExists(existingObj.path);
    836 					}
    837 					
    838 					if (!existingObj || !existingObj.hidden) {
    839 						yield OS.File.move(tmpFile, OS.Path.join(destDir, fileName));
    840 					}
    841 					else {
    842 						yield OS.File.move(tmpFile, OS.Path.join(hiddenDir, fileName));
    843 					}
    844 				}
    845 			}
    846 			
    847 			if(xpiZipReader) xpiZipReader.close();
    848 		}
    849 		// Source installation
    850 		else {
    851 			let sourceDir = OS.Path.join(installLocation, mode);
    852 			
    853 			var modTime = 0;
    854 			let sourceFilesExist = false;
    855 			let iterator;
    856 			try {
    857 				iterator = new OS.File.DirectoryIterator(sourceDir);
    858 			}
    859 			catch (e) {
    860 				if (e instanceof OS.File.Error && e.becauseNoSuchFile) {
    861 					let msg = "No " + mode + " directory";
    862 					Zotero.debug(msg, 1);
    863 					Components.utils.reportError(msg);
    864 					return false;
    865 				}
    866 				throw e;
    867 			}
    868 			try {
    869 				while (true) {
    870 					let entries = yield iterator.nextBatch(10); // TODO: adjust as necessary
    871 					if (!entries.length) break;
    872 					for (let i = 0; i < entries.length; i++) {
    873 						let entry = entries[i];
    874 						if (!entry.name.match(fileNameRE) || entry.isDir) {
    875 							continue;
    876 						}
    877 						sourceFilesExist = true;
    878 						let d;
    879 						if ('winLastWriteDate' in entry) {
    880 							d = entry.winLastWriteDate;
    881 						}
    882 						else {
    883 							d = (yield OS.File.stat(entry.path)).lastModificationDate;
    884 						}
    885 						let fileModTime = Math.round(d.getTime() / 1000);
    886 						if (fileModTime > modTime) {
    887 							modTime = fileModTime;
    888 						}
    889 					}
    890 				}
    891 			}
    892 			finally {
    893 				iterator.close();
    894 			}
    895 			
    896 			// Don't attempt installation for source build with missing styles
    897 			if (!sourceFilesExist) {
    898 				Zotero.debug("No source " + modeType + " files exist -- skipping update");
    899 				return false;
    900 			}
    901 			
    902 			if (!forceReinstall && lastModTime && modTime <= lastModTime) {
    903 				Zotero.debug("Installed " + mode + " are up-to-date with " + mode + " directory");
    904 				return false;
    905 			}
    906 			
    907 			Zotero.debug("Updating installed " + mode + " from " + mode + " directory");
    908 			
    909 			iterator = new OS.File.DirectoryIterator(sourceDir);
    910 			try {
    911 				while (true) {
    912 					let entries = yield iterator.nextBatch(10); // TODO: adjust as necessary
    913 					if (!entries.length) break;
    914 					
    915 					for (let i = 0; i < entries.length; i++) {
    916 						let entry = entries[i];
    917 						if (!entry.name.match(fileNameRE) || entry.isDir) {
    918 							continue;
    919 						}
    920 						let newObj;
    921 						if (mode == 'styles') {
    922 							let code = yield Zotero.File.getContentsAsync(entry.path);
    923 							newObj = new Zotero.Style(code);
    924 						}
    925 						else if (mode == 'translators') {
    926 							newObj = yield Zotero.Translators.loadFromFile(entry.path);
    927 						}
    928 						else {
    929 							throw new Error("Invalid mode '" + mode + "'");
    930 						}
    931 						let existingObj = Zotero[Mode].get(newObj[modeType + "ID"]);
    932 						if (!existingObj) {
    933 							Zotero.debug("Installing " + modeType + " '" + newObj[titleField] + "'");
    934 						}
    935 						else {
    936 							Zotero.debug("Updating "
    937 								+ (existingObj.hidden ? "hidden " : "")
    938 								+ modeType + " '" + existingObj[titleField] + "'");
    939 							yield Zotero.File.removeIfExists(existingObj.path);
    940 						}
    941 						
    942 						let fileName;
    943 						if (mode == 'translators') {
    944 							fileName = Zotero.Translators.getFileNameFromLabel(
    945 								newObj[titleField], newObj.translatorID
    946 							);
    947 						}
    948 						else if (mode == 'styles') {
    949 							fileName = entry.name;
    950 						}
    951 						
    952 						try {
    953 							let destFile;
    954 							if (!existingObj || !existingObj.hidden) {
    955 								destFile = OS.Path.join(destDir, fileName);
    956 							}
    957 							else {
    958 								destFile = OS.Path.join(hiddenDir, fileName)
    959 							}
    960 							
    961 							try {
    962 								yield OS.File.copy(entry.path, destFile, { noOverwrite: true });
    963 							}
    964 							catch (e) {
    965 								if (e instanceof OS.File.Error && e.becauseExists) {
    966 									// Could overwrite automatically, but we want to log this
    967 									Zotero.warn("Overwriting " + modeType + " with same filename "
    968 										+ "'" + fileName + "'", 1);
    969 									yield OS.File.copy(entry.path, destFile);
    970 								}
    971 								else {
    972 									throw e;
    973 								}
    974 							}
    975 							
    976 							if (mode == 'translators') {
    977 								cache[fileName] = newObj.metadata;
    978 							}
    979 						}
    980 						catch (e) {
    981 							Components.utils.reportError("Error copying file " + fileName + ": " + e);
    982 						}
    983 					}
    984 				}
    985 			}
    986 			finally {
    987 				iterator.close();
    988 			}
    989 		}
    990 		
    991 		yield Zotero.DB.executeTransaction(function* () {
    992 			var sql = "REPLACE INTO version VALUES (?, ?)";
    993 			yield Zotero.DB.queryAsync(sql, [mode, modTime]);
    994 			
    995 			if (!skipVersionUpdates) {
    996 				sql = "REPLACE INTO version VALUES ('repository', ?)";
    997 				yield Zotero.DB.queryAsync(sql, repotime);
    998 			}
    999 		});
   1000 		
   1001 		yield Zotero[Mode].reinit({
   1002 			metadataCache: cache,
   1003 			fromSchemaUpdate: true
   1004 		});
   1005 		
   1006 		return true;
   1007 	});
   1008 	
   1009 	
   1010 	this.onUpdateNotification = async function (delay) {
   1011 		if (!Zotero.Prefs.get('automaticScraperUpdates')) {
   1012 			return;
   1013 		}
   1014 		
   1015 		// If another repository check -- either from notification or daily check -- is scheduled
   1016 		// before delay, just wait for that one
   1017 		if (_nextRepositoryUpdate) {
   1018 			if (_nextRepositoryUpdate <= (Date.now() + delay)) {
   1019 				Zotero.debug("Next scheduled update from repository is in "
   1020 					+ Math.round((_nextRepositoryUpdate - Date.now()) / 1000) + " seconds "
   1021 					+ "-- ignoring notification");
   1022 				return;
   1023 			}
   1024 			if (_repositoryNotificationTimerID) {
   1025 				clearTimeout(_repositoryNotificationTimerID);
   1026 			}
   1027 		}
   1028 		
   1029 		_nextRepositoryUpdate = Date.now() + delay;
   1030 		Zotero.debug(`Updating from repository in ${Math.round(delay / 1000)} seconds`);
   1031 		_repositoryNotificationTimerID = setTimeout(() => {
   1032 			this.updateFromRepository(this.REPO_UPDATE_NOTIFICATION)
   1033 		}, delay);
   1034 	};
   1035 	
   1036 	
   1037 	/**
   1038 	 * Send XMLHTTP request for updated translators and styles to the central repository
   1039 	 *
   1040 	 * @param {Integer} [force=0]	 - If non-zero, force a repository query regardless of how long it's
   1041 	 *     been since the last check. Should be a REPO_UPDATE_* constant.
   1042 	 */
   1043 	this.updateFromRepository = Zotero.Promise.coroutine(function* (force = 0) {
   1044 		if (Zotero.skipBundledFiles) {
   1045 			Zotero.debug("No bundled files -- skipping repository update");
   1046 			return;
   1047 		}
   1048 		
   1049 		if (_remoteUpdateInProgress) {
   1050 			Zotero.debug("A remote update is already in progress -- not checking repository");
   1051 			return false;
   1052 		}
   1053 		
   1054 		if (!force) {
   1055 			// Check user preference for automatic updates
   1056 			if (!Zotero.Prefs.get('automaticScraperUpdates')) {
   1057 				Zotero.debug('Automatic repository updating disabled -- not checking repository', 4);
   1058 				return false;
   1059 			}
   1060 			
   1061 			// Determine the earliest local time that we'd query the repository again
   1062 			let lastCheck = yield this.getDBVersion('lastcheck');
   1063 			let nextCheck = new Date();
   1064 			nextCheck.setTime((lastCheck + ZOTERO_CONFIG.REPOSITORY_CHECK_INTERVAL) * 1000);
   1065 			
   1066 			// If enough time hasn't passed, don't update
   1067 			var now = new Date();
   1068 			if (now < nextCheck) {
   1069 				Zotero.debug('Not enough time since last update -- not checking repository', 4);
   1070 				// Set the repository timer to the remaining time
   1071 				_setRepositoryTimer(Math.round((nextCheck.getTime() - now.getTime()) / 1000));
   1072 				return false;
   1073 			}
   1074 		}
   1075 		
   1076 		if (_localUpdateInProgress) {
   1077 			Zotero.debug('A local update is already in progress -- delaying repository check', 4);
   1078 			_setRepositoryTimer(600);
   1079 			return false;
   1080 		}
   1081 		
   1082 		if (Zotero.locked) {
   1083 			Zotero.debug('Zotero is locked -- delaying repository check', 4);
   1084 			_setRepositoryTimer(600);
   1085 			return false;
   1086 		}
   1087 		
   1088 		// If an update from a notification is queued, stop it, since we're updating now
   1089 		if (_repositoryNotificationTimerID) {
   1090 			clearTimeout(_repositoryNotificationTimerID);
   1091 			_repositoryNotificationTimerID = null;
   1092 			_nextRepositoryUpdate = null;
   1093 		}
   1094 		
   1095 		if (Zotero.DB.inTransaction()) {
   1096 			yield Zotero.DB.waitForTransaction();
   1097 		}
   1098 		
   1099 		// Get the last timestamp we got from the server
   1100 		var lastUpdated = yield this.getDBVersion('repository');
   1101 		var updated = false;
   1102 		
   1103 		try {
   1104 			var url = ZOTERO_CONFIG.REPOSITORY_URL + 'updated?'
   1105 				+ (lastUpdated ? 'last=' + lastUpdated + '&' : '')
   1106 				+ 'version=' + Zotero.version;
   1107 			
   1108 			Zotero.debug('Checking repository for updates');
   1109 			
   1110 			_remoteUpdateInProgress = true;
   1111 			
   1112 			if (force) {
   1113 				url += '&m=' + force;
   1114 			}
   1115 			
   1116 			// Send list of installed styles
   1117 			var styles = Zotero.Styles.getAll();
   1118 			var styleTimestamps = [];
   1119 			for (let id in styles) {
   1120 				let styleUpdated = Zotero.Date.sqlToDate(styles[id].updated);
   1121 				styleUpdated = styleUpdated ? styleUpdated.getTime() / 1000 : 0;
   1122 				var selfLink = styles[id].url;
   1123 				var data = {
   1124 					id: id,
   1125 					updated: styleUpdated
   1126 				};
   1127 				if (selfLink) {
   1128 					data.url = selfLink;
   1129 				}
   1130 				styleTimestamps.push(data);
   1131 			}
   1132 			var body = 'styles=' + encodeURIComponent(JSON.stringify(styleTimestamps));
   1133 			
   1134 			try {
   1135 				var xmlhttp = yield Zotero.HTTP.request("POST", url, { body: body });
   1136 				updated = yield _handleRepositoryResponse(xmlhttp, force);
   1137 			}
   1138 			catch (e) {
   1139 				if (!force) {
   1140 					if (e instanceof Zotero.HTTP.UnexpectedStatusException
   1141 							|| e instanceof Zotero.HTTP.BrowserOfflineException) {
   1142 						let msg = " -- retrying in " + ZOTERO_CONFIG.REPOSITORY_RETRY_INTERVAL
   1143 						if (e instanceof Zotero.HTTP.BrowserOfflineException) {
   1144 							Zotero.debug("Browser is offline" + msg, 2);
   1145 						}
   1146 						else {
   1147 							Zotero.logError(e);
   1148 							Zotero.debug(e.status, 1);
   1149 							Zotero.debug(e.xmlhttp.responseText, 1);
   1150 							Zotero.debug("Error updating from repository " + msg, 1);
   1151 						}
   1152 						// TODO: instead, add an observer to start and stop timer on online state change
   1153 						_setRepositoryTimer(ZOTERO_CONFIG.REPOSITORY_RETRY_INTERVAL);
   1154 						return;
   1155 					}
   1156 				}
   1157 				if (xmlhttp) {
   1158 					Zotero.debug(xmlhttp.status, 1);
   1159 					Zotero.debug(xmlhttp.responseText, 1);
   1160 				}
   1161 				throw e;
   1162 			};
   1163 		}
   1164 		finally {
   1165 			if (!force) {
   1166 				_setRepositoryTimer(ZOTERO_CONFIG.REPOSITORY_RETRY_INTERVAL);
   1167 			}
   1168 			_remoteUpdateInProgress = false;
   1169 		}
   1170 		
   1171 		return updated;
   1172 	});
   1173 	
   1174 	
   1175 	this.stopRepositoryTimer = function () {
   1176 		if (_repositoryTimerID) {
   1177 			Zotero.debug('Stopping repository check timer');
   1178 			clearTimeout(_repositoryTimerID);
   1179 			_repositoryTimerID = null;
   1180 		}
   1181 		if (_repositoryNotificationTimerID) {
   1182 			Zotero.debug('Stopping repository notification update timer');
   1183 			clearTimeout(_repositoryNotificationTimerID);
   1184 			_repositoryNotificationTimerID = null
   1185 		}
   1186 		_nextRepositoryUpdate = null;
   1187 	}
   1188 	
   1189 	
   1190 	this.resetTranslatorsAndStyles = Zotero.Promise.coroutine(function* () {
   1191 		Zotero.debug("Resetting translators and styles");
   1192 		
   1193 		var sql = "DELETE FROM version WHERE schema IN "
   1194 			+ "('translators', 'styles', 'repository', 'lastcheck')";
   1195 		yield Zotero.DB.queryAsync(sql);
   1196 		_dbVersions.repository = null;
   1197 		_dbVersions.lastcheck = null;
   1198 		
   1199 		var translatorsDir = Zotero.getTranslatorsDirectory();
   1200 		var stylesDir = Zotero.getStylesDirectory();
   1201 		
   1202 		translatorsDir.remove(true);
   1203 		stylesDir.remove(true);
   1204 		
   1205 		// Recreate directories
   1206 		Zotero.getTranslatorsDirectory();
   1207 		Zotero.getStylesDirectory();
   1208 		
   1209 		yield Zotero.Promise.all(Zotero.Translators.reinit(), Zotero.Styles.reinit());
   1210 		var updated = yield this.updateBundledFiles();
   1211 		if (updated && Zotero.Prefs.get('automaticScraperUpdates')) {
   1212 			yield Zotero.Schema.updateFromRepository(this.REPO_UPDATE_MANUAL);
   1213 		}
   1214 		return updated;
   1215 	});
   1216 	
   1217 	
   1218 	this.resetTranslators = Zotero.Promise.coroutine(function* () {
   1219 		Zotero.debug("Resetting translators");
   1220 		
   1221 		var sql = "DELETE FROM version WHERE schema IN "
   1222 			+ "('translators', 'repository', 'lastcheck')";
   1223 		yield Zotero.DB.queryAsync(sql);
   1224 		_dbVersions.repository = null;
   1225 		_dbVersions.lastcheck = null;
   1226 		
   1227 		var translatorsDir = Zotero.getTranslatorsDirectory();
   1228 		translatorsDir.remove(true);
   1229 		Zotero.getTranslatorsDirectory(); // recreate directory
   1230 		yield Zotero.Translators.reinit();
   1231 		var updated = yield this.updateBundledFiles('translators');
   1232 		if (updated && Zotero.Prefs.get('automaticScraperUpdates')) {
   1233 			yield Zotero.Schema.updateFromRepository(this.REPO_UPDATE_MANUAL);
   1234 		}
   1235 		return updated;
   1236 	});
   1237 	
   1238 	
   1239 	this.resetStyles = Zotero.Promise.coroutine(function* () {
   1240 		Zotero.debug("Resetting styles");
   1241 		
   1242 		var sql = "DELETE FROM version WHERE schema IN "
   1243 			+ "('styles', 'repository', 'lastcheck')";
   1244 		yield Zotero.DB.queryAsync(sql);
   1245 		_dbVersions.repository = null;
   1246 		_dbVersions.lastcheck = null;
   1247 		
   1248 		var stylesDir = Zotero.getStylesDirectory();
   1249 		stylesDir.remove(true);
   1250 		Zotero.getStylesDirectory(); // recreate directory
   1251 		yield Zotero.Styles.reinit()
   1252 		var updated = yield this.updateBundledFiles('styles');
   1253 		if (updated && Zotero.Prefs.get('automaticScraperUpdates')) {
   1254 			yield Zotero.Schema.updateFromRepository(this.REPO_UPDATE_MANUAL);
   1255 		}
   1256 		return updated;
   1257 	});
   1258 	
   1259 	
   1260 	this.integrityCheck = Zotero.Promise.coroutine(function* (fix) {
   1261 		Zotero.debug("Checking database integrity");
   1262 		
   1263 		// Just as a sanity check, make sure combined field tables are populated,
   1264 		// so that we don't try to wipe out all data
   1265 		if (!(yield Zotero.DB.valueQueryAsync("SELECT COUNT(*) FROM fieldsCombined"))
   1266 				|| !(yield Zotero.DB.valueQueryAsync("SELECT COUNT(*) FROM itemTypeFieldsCombined"))) {
   1267 			return false;
   1268 		}
   1269 		
   1270 		// Check foreign keys
   1271 		var rows = yield Zotero.DB.queryAsync("PRAGMA foreign_key_check");
   1272 		if (rows.length && !fix) {
   1273 			let suffix1 = rows.length == 1 ? '' : 's';
   1274 			let suffix2 = rows.length == 1 ? 's' : '';
   1275 			Zotero.debug(`Found ${rows.length} row${suffix1} that violate${suffix2} foreign key constraints`, 1);
   1276 			return false;
   1277 		}
   1278 		// If fixing, delete rows that violate FK constraints
   1279 		for (let row of rows) {
   1280 			try {
   1281 				yield Zotero.DB.queryAsync(`DELETE FROM ${row.table} WHERE ROWID=?`, row.rowid);
   1282 			}
   1283 			catch (e) {
   1284 				Zotero.logError(e);
   1285 			}
   1286 		}
   1287 		
   1288 		
   1289 		// Non-foreign key checks
   1290 		//
   1291 		// The first position is for testing and the second is for repairing. Can be either SQL
   1292 		// statements or promise-returning functions. For statements, the repair entry can be either a
   1293 		// string or an array with multiple statements. Functions should avoid assuming any global state
   1294 		// (e.g., loaded data).
   1295 		var checks = [
   1296 			// Can't be a FK with itemTypesCombined
   1297 			[
   1298 				"SELECT COUNT(*) > 0 FROM items WHERE itemTypeID IS NULL",
   1299 				"DELETE FROM items WHERE itemTypeID IS NULL",
   1300 			],
   1301 			// Attachments row with itemTypeID != 14
   1302 			[
   1303 				"SELECT COUNT(*) > 0 FROM itemAttachments JOIN items USING (itemID) WHERE itemTypeID != 14",
   1304 				"UPDATE items SET itemTypeID=14, clientDateModified=CURRENT_TIMESTAMP WHERE itemTypeID != 14 AND itemID IN (SELECT itemID FROM itemAttachments)",
   1305 			],
   1306 			// Fields not in type
   1307 			[
   1308 				"SELECT COUNT(*) > 0 FROM itemData WHERE fieldID NOT IN (SELECT fieldID FROM itemTypeFieldsCombined WHERE itemTypeID=(SELECT itemTypeID FROM items WHERE itemID=itemData.itemID))",
   1309 				"DELETE FROM itemData WHERE fieldID NOT IN (SELECT fieldID FROM itemTypeFieldsCombined WHERE itemTypeID=(SELECT itemTypeID FROM items WHERE itemID=itemData.itemID))",
   1310 			],
   1311 			// Missing itemAttachments row
   1312 			[
   1313 				"SELECT COUNT(*) > 0 FROM items WHERE itemTypeID=14 AND itemID NOT IN (SELECT itemID FROM itemAttachments)",
   1314 				"INSERT INTO itemAttachments (itemID, linkMode) SELECT itemID, 0 FROM items WHERE itemTypeID=14 AND itemID NOT IN (SELECT itemID FROM itemAttachments)",
   1315 			],
   1316 			// Note/child parents
   1317 			[
   1318 				"SELECT COUNT(*) > 0 FROM itemAttachments WHERE parentItemID IN (SELECT itemID FROM items WHERE itemTypeID IN (1,14))",
   1319 				"UPDATE itemAttachments SET parentItemID=NULL WHERE parentItemID IN (SELECT itemID FROM items WHERE itemTypeID IN (1,14))",
   1320 			],
   1321 			[
   1322 				"SELECT COUNT(*) > 0 FROM itemNotes WHERE parentItemID IN (SELECT itemID FROM items WHERE itemTypeID IN (1,14))",
   1323 				"UPDATE itemNotes SET parentItemID=NULL WHERE parentItemID IN (SELECT itemID FROM items WHERE itemTypeID IN (1,14))",
   1324 			],
   1325 			
   1326 			// Delete empty creators
   1327 			// This may cause itemCreator gaps, but that's better than empty creators
   1328 			[
   1329 				"SELECT COUNT(*) > 0 FROM creators WHERE firstName='' AND lastName=''",
   1330 				"DELETE FROM creators WHERE firstName='' AND lastName=''"
   1331 			],
   1332 			
   1333 			// Non-attachment items in the full-text index
   1334 			[
   1335 				"SELECT COUNT(*) > 0 FROM fulltextItemWords WHERE itemID NOT IN (SELECT itemID FROM items WHERE itemTypeID=14)",
   1336 				"DELETE FROM fulltextItemWords WHERE itemID NOT IN (SELECT itemID FROM items WHERE itemTypeID=14)"
   1337 			],
   1338 			// Full-text items must be attachments
   1339 			[
   1340 				"SELECT COUNT(*) > 0 FROM fulltextItems WHERE itemID NOT IN (SELECT itemID FROM items WHERE itemTypeID=14)",
   1341 				"DELETE FROM fulltextItems WHERE itemID NOT IN (SELECT itemID FROM items WHERE itemTypeID=14)"
   1342 			],
   1343 			// Invalid link mode -- set to imported url
   1344 			[
   1345 				"SELECT COUNT(*) > 0 FROM itemAttachments WHERE linkMode NOT IN (0,1,2,3)",
   1346 				"UPDATE itemAttachments SET linkMode=1 WHERE linkMode NOT IN (0,1,2,3)"
   1347 			],
   1348 			// Creators with first name can't be fieldMode 1
   1349 			[
   1350 				"SELECT COUNT(*) > 0 FROM creators WHERE fieldMode = 1 AND firstName != ''",
   1351 				function () {
   1352 					return Zotero.DB.executeTransaction(function* () {
   1353 						var rows = yield Zotero.DB.queryAsync("SELECT * FROM creators WHERE fieldMode = 1 AND firstName != ''");
   1354 						for (let row of rows) {
   1355 							// Find existing fieldMode 0 row and use that if available
   1356 							let newID = yield Zotero.DB.valueQueryAsync("SELECT creatorID FROM creators WHERE firstName=? AND lastName=? AND fieldMode=0", [row.firstName, row.lastName]);
   1357 							if (newID) {
   1358 								yield Zotero.DB.queryAsync("UPDATE itemCreators SET creatorID=? WHERE creatorID=?", [newID, row.creatorID]);
   1359 								yield Zotero.DB.queryAsync("DELETE FROM creators WHERE creatorID=?", row.creatorID);
   1360 							}
   1361 							// Otherwise convert this one to fieldMode 0
   1362 							else {
   1363 								yield Zotero.DB.queryAsync("UPDATE creators SET fieldMode=0 WHERE creatorID=?", row.creatorID);
   1364 							}
   1365 						}
   1366 					});
   1367 				}
   1368 			]
   1369 		];
   1370 		
   1371 		for (let check of checks) {
   1372 			let errorsFound = false;
   1373 			// SQL statement
   1374 			if (typeof check[0] == 'string') {
   1375 				errorsFound = yield Zotero.DB.valueQueryAsync(check[0]);
   1376 			}
   1377 			// Function
   1378 			else {
   1379 				errorsFound = yield check[0]();
   1380 			}
   1381 			if (!errorsFound) {
   1382 				continue;
   1383 			}
   1384 			
   1385 			Zotero.debug("Test failed!", 1);
   1386 			
   1387 			if (fix) {
   1388 				try {
   1389 					// Single query
   1390 					if (typeof check[1] == 'string') {
   1391 						yield Zotero.DB.queryAsync(check[1]);
   1392 					}
   1393 					// Multiple queries
   1394 					else if (Array.isArray(check[1])) {
   1395 						for (let s of check[1]) {
   1396 							yield Zotero.DB.queryAsync(s);
   1397 						}
   1398 					}
   1399 					// Function
   1400 					else {
   1401 						yield check[1]();
   1402 					}
   1403 					continue;
   1404 				}
   1405 				catch (e) {
   1406 					Zotero.logError(e);
   1407 				}
   1408 			}
   1409 			
   1410 			return false;
   1411 		}
   1412 		
   1413 		return true;
   1414 	});
   1415 	
   1416 	
   1417 	/////////////////////////////////////////////////////////////////
   1418 	//
   1419 	// Private methods
   1420 	//
   1421 	/////////////////////////////////////////////////////////////////
   1422 	
   1423 	/**
   1424 	 * Retrieve the version from the top line of the schema SQL file
   1425 	 *
   1426 	 * @return {Promise:String} A promise for the SQL file's version number
   1427 	 */
   1428 	function _getSchemaSQLVersion(schema){
   1429 		return _getSchemaSQL(schema)
   1430 		.then(function (sql) {
   1431 			// Fetch the schema version from the first line of the file
   1432 			var schemaVersion = parseInt(sql.match(/^-- ([0-9]+)/)[1]);
   1433 			_schemaVersions[schema] = schemaVersion;
   1434 			return schemaVersion;
   1435 		});
   1436 	}
   1437 	
   1438 	
   1439 	/**
   1440 	 * Load in SQL schema
   1441 	 *
   1442 	 * @return {Promise:String} A promise for the contents of a schema SQL file
   1443 	 */
   1444 	function _getSchemaSQL(schema){
   1445 		if (!schema){
   1446 			throw ('Schema type not provided to _getSchemaSQL()');
   1447 		}
   1448 		
   1449 		return Zotero.File.getContentsFromURLAsync("resource://zotero/schema/" + schema + ".sql");
   1450 	}
   1451 	
   1452 	
   1453 	/*
   1454 	 * Determine the SQL statements necessary to drop the tables and indexed
   1455 	 * in a given schema file
   1456 	 *
   1457 	 * NOTE: This is not currently used.
   1458 	 *
   1459 	 * Returns the SQL statements as a string for feeding into query()
   1460 	 */
   1461 	function _getDropCommands(schema){
   1462 		var sql = _getSchemaSQL(schema); // FIXME: now a promise
   1463 		
   1464 		const re = /(?:[\r\n]|^)CREATE (TABLE|INDEX) IF NOT EXISTS ([^\s]+)/;
   1465 		var m, str="";
   1466 		while(matches = re.exec(sql)) {
   1467 			str += "DROP " + matches[1] + " IF EXISTS " + matches[2] + ";\n";
   1468 		}
   1469 		
   1470 		return str;
   1471 	}
   1472 	
   1473 	
   1474 	/*
   1475 	 * Create new DB schema
   1476 	 */
   1477 	function _initializeSchema(){
   1478 		return Zotero.DB.executeTransaction(function* (conn) {
   1479 			var userLibraryID = 1;
   1480 			
   1481 			// Enable auto-vacuuming
   1482 			yield Zotero.DB.queryAsync("PRAGMA page_size = 4096");
   1483 			yield Zotero.DB.queryAsync("PRAGMA encoding = 'UTF-8'");
   1484 			yield Zotero.DB.queryAsync("PRAGMA auto_vacuum = 1");
   1485 			
   1486 			yield _getSchemaSQL('system').then(function (sql) {
   1487 				return Zotero.DB.executeSQLFile(sql);
   1488 			});
   1489 			yield _getSchemaSQL('userdata').then(function (sql) {
   1490 				return Zotero.DB.executeSQLFile(sql);
   1491 			});
   1492 			yield _getSchemaSQL('triggers').then(function (sql) {
   1493 				return Zotero.DB.executeSQLFile(sql);
   1494 			});
   1495 			yield _updateCustomTables(true);
   1496 			
   1497 			yield _getSchemaSQLVersion('system').then(function (version) {
   1498 				return _updateDBVersion('system', version);
   1499 			});
   1500 			yield _getSchemaSQLVersion('userdata').then(function (version) {
   1501 				return _updateDBVersion('userdata', version);
   1502 			});
   1503 			yield _getSchemaSQLVersion('triggers').then(function (version) {
   1504 				return _updateDBVersion('triggers', version);
   1505 			});
   1506 			yield _updateDBVersion('compatibility', _maxCompatibility);
   1507 			
   1508 			var sql = "INSERT INTO libraries (libraryID, type, editable, filesEditable) "
   1509 				+ "VALUES "
   1510 				+ "(?, 'user', 1, 1)";
   1511 			yield Zotero.DB.queryAsync(sql, userLibraryID);
   1512 			
   1513 			yield _updateLastClientVersion();
   1514 			
   1515 			self.dbInitialized = true;
   1516 		})
   1517 		.catch(function (e) {
   1518 			Zotero.debug(e, 1);
   1519 			Components.utils.reportError(e);
   1520 			let ps = Components.classes["@mozilla.org/embedcomp/prompt-service;1"]
   1521 				.getService(Components.interfaces.nsIPromptService);
   1522 			ps.alert(null, Zotero.getString('general.error'), Zotero.getString('startupError'));
   1523 			throw e;
   1524 		});
   1525 	}
   1526 	
   1527 	
   1528 	/*
   1529 	 * Update a DB schema version tag in an existing database
   1530 	 */
   1531 	function _updateDBVersion(schema, version) {
   1532 		_dbVersions[schema] = version;
   1533 		var sql = "REPLACE INTO version (schema,version) VALUES (?,?)";
   1534 		return Zotero.DB.queryAsync(sql, [schema, parseInt(version)]);
   1535 	}
   1536 	
   1537 	
   1538 	/**
   1539 	 * Requires a transaction
   1540 	 */
   1541 	var _updateSchema = Zotero.Promise.coroutine(function* (schema) {
   1542 		var [dbVersion, schemaVersion] = yield Zotero.Promise.all(
   1543 			[Zotero.Schema.getDBVersion(schema), _getSchemaSQLVersion(schema)]
   1544 		);
   1545 		if (dbVersion == schemaVersion) {
   1546 			return false;
   1547 		}
   1548 		if (dbVersion > schemaVersion) {
   1549 			let dbClientVersion = yield Zotero.DB.valueQueryAsync(
   1550 				"SELECT value FROM settings WHERE setting='client' AND key='lastCompatibleVersion'"
   1551 			);
   1552 			throw new Zotero.DB.IncompatibleVersionException(
   1553 				`Zotero '${schema}' DB version (${dbVersion}) is newer than SQL file (${schemaVersion})`,
   1554 				dbClientVersion
   1555 			);
   1556 		}
   1557 		let sql = yield _getSchemaSQL(schema);
   1558 		yield Zotero.DB.executeSQLFile(sql);
   1559 		return _updateDBVersion(schema, schemaVersion);
   1560 	});
   1561 	
   1562 	
   1563 	var _updateCompatibility = Zotero.Promise.coroutine(function* (version) {
   1564 		if (version > _maxCompatibility) {
   1565 			throw new Error("Can't set compatibility greater than _maxCompatibility");
   1566 		}
   1567 		
   1568 		yield Zotero.DB.queryAsync(
   1569 			"REPLACE INTO settings VALUES ('client', 'lastCompatibleVersion', ?)", [Zotero.version]
   1570 		);
   1571 		yield _updateDBVersion('compatibility', version);
   1572 	});
   1573 	
   1574 	
   1575 	function _checkClientVersion() {
   1576 		return Zotero.DB.executeTransaction(function* () {
   1577 			var lastVersion = yield _getLastClientVersion();
   1578 			var currentVersion = Zotero.version;
   1579 			
   1580 			if (currentVersion == lastVersion) {
   1581 				return false;
   1582 			}
   1583 			
   1584 			Zotero.debug(`Client version has changed from ${lastVersion} to ${currentVersion}`);
   1585 			
   1586 			// Retry all queued objects immediately on upgrade
   1587 			yield Zotero.Sync.Data.Local.resetSyncQueueTries();
   1588 			
   1589 			// Update version
   1590 			yield _updateLastClientVersion();
   1591 			
   1592 			return true;
   1593 		}.bind(this));
   1594 	}
   1595 	
   1596 	
   1597 	function _getLastClientVersion() {
   1598 		var sql = "SELECT value FROM settings WHERE setting='client' AND key='lastVersion'";
   1599 		return Zotero.DB.valueQueryAsync(sql);
   1600 	}
   1601 	
   1602 	
   1603 	function _updateLastClientVersion() {
   1604 		var sql = "REPLACE INTO settings (setting, key, value) VALUES ('client', 'lastVersion', ?)";
   1605 		return Zotero.DB.queryAsync(sql, Zotero.version);
   1606 	}
   1607 	
   1608 	
   1609 	/**
   1610 	 * Process the response from the repository
   1611 	 *
   1612 	 * @return {Promise:Boolean} A promise for whether the update suceeded
   1613 	 **/
   1614 	async function _handleRepositoryResponse(xmlhttp, force) {
   1615 		if (!xmlhttp.responseXML){
   1616 			try {
   1617 				if (xmlhttp.status>1000){
   1618 					Zotero.debug('No network connection', 2);
   1619 				}
   1620 				else {
   1621 					Zotero.debug(xmlhttp.status);
   1622 					Zotero.debug(xmlhttp.responseText);
   1623 					Zotero.debug('Invalid response from repository', 2);
   1624 				}
   1625 			}
   1626 			catch (e){
   1627 				Zotero.debug('Repository cannot be contacted');
   1628 			}
   1629 			return false;
   1630 		}
   1631 		
   1632 		var currentTime = xmlhttp.responseXML.
   1633 			getElementsByTagName('currentTime')[0].firstChild.nodeValue;
   1634 		var lastCheckTime = Math.round(new Date().getTime()/1000);
   1635 		var translatorUpdates = xmlhttp.responseXML.getElementsByTagName('translator');
   1636 		var styleUpdates = xmlhttp.responseXML.getElementsByTagName('style');
   1637 		
   1638 		if (!translatorUpdates.length && !styleUpdates.length){
   1639 			await Zotero.DB.executeTransaction(function* (conn) {
   1640 				// Store the timestamp provided by the server
   1641 				yield _updateDBVersion('repository', currentTime);
   1642 				
   1643 				// And the local timestamp of the update time
   1644 				yield _updateDBVersion('lastcheck', lastCheckTime);
   1645 			});
   1646 			
   1647 			Zotero.debug('All translators and styles are up-to-date');
   1648 			if (!force) {
   1649 				_setRepositoryTimer(ZOTERO_CONFIG.REPOSITORY_CHECK_INTERVAL);
   1650 			}
   1651 			return true;
   1652 		}
   1653 		
   1654 		var updated = false;
   1655 		try {
   1656 			for (var i=0, len=translatorUpdates.length; i<len; i++){
   1657 				await _translatorXMLToFile(translatorUpdates[i]);
   1658 			}
   1659 			
   1660 			for (var i=0, len=styleUpdates.length; i<len; i++){
   1661 				await _styleXMLToFile(styleUpdates[i]);
   1662 			}
   1663 			
   1664 			// Rebuild caches
   1665 			await Zotero.Translators.reinit({ fromSchemaUpdate: force != 1 });
   1666 			await Zotero.Styles.reinit({ fromSchemaUpdate: force != 1 });
   1667 			
   1668 			updated = true;
   1669 		}
   1670 		catch (e) {
   1671 			Zotero.logError(e);
   1672 		}
   1673 		
   1674 		if (updated) {
   1675 			await Zotero.DB.executeTransaction(function* (conn) {
   1676 				// Store the timestamp provided by the server
   1677 				yield _updateDBVersion('repository', currentTime);
   1678 				
   1679 				// And the local timestamp of the update time
   1680 				yield _updateDBVersion('lastcheck', lastCheckTime);
   1681 			});
   1682 		}
   1683 		
   1684 		return updated;
   1685 	}
   1686 	
   1687 	
   1688 	/**
   1689 	* Set the interval between repository queries
   1690 	*
   1691 	* We add an additional two seconds to avoid race conditions
   1692 	**/
   1693 	function _setRepositoryTimer(delay) {
   1694 		var fudge = 2; // two seconds
   1695 		var displayInterval = delay + fudge;
   1696 		delay = (delay + fudge) * 1000; // convert to ms
   1697 		
   1698 		if (_repositoryTimerID) {
   1699 			clearTimeout(_repositoryTimerID);
   1700 			_repositoryTimerID = null;
   1701 		}
   1702 		if (_repositoryNotificationTimerID) {
   1703 			clearTimeout(_repositoryNotificationTimerID);
   1704 			_repositoryNotificationTimerID = null;
   1705 		}
   1706 		
   1707 		Zotero.debug('Scheduling next repository check in ' + displayInterval + ' seconds');
   1708 		_repositoryTimerID = setTimeout(() => Zotero.Schema.updateFromRepository(), delay);
   1709 		_nextRepositoryUpdate = Date.now() + delay;
   1710 	}
   1711 	
   1712 	
   1713 	/**
   1714 	 * Traverse an XML translator node from the repository and
   1715 	 * update the local translators folder with the translator data
   1716 	 *
   1717 	 * @return {Promise}
   1718 	 */
   1719 	var _translatorXMLToFile = Zotero.Promise.coroutine(function* (xmlnode) {
   1720 		// Don't split >4K chunks into multiple nodes
   1721 		// https://bugzilla.mozilla.org/show_bug.cgi?id=194231
   1722 		xmlnode.normalize();
   1723 		var translatorID = xmlnode.getAttribute('id');
   1724 		var translator = Zotero.Translators.get(translatorID);
   1725 		
   1726 		// Delete local version of remote translators with priority 0
   1727 		if (xmlnode.getElementsByTagName('priority')[0].firstChild.nodeValue === "0") {
   1728 			if (translator && (yield OS.File.exists(translator.path))) {
   1729 				Zotero.debug("Deleting translator '" + translator.label + "'");
   1730 				yield OS.File.remove(translator.path);
   1731 			}
   1732 			return false;
   1733 		}
   1734 		
   1735 		var metadata = {
   1736 			translatorID: translatorID,
   1737 			translatorType: parseInt(xmlnode.getAttribute('type')),
   1738 			label: xmlnode.getElementsByTagName('label')[0].firstChild.nodeValue,
   1739 			creator: xmlnode.getElementsByTagName('creator')[0].firstChild.nodeValue,
   1740 			target: (xmlnode.getElementsByTagName('target').item(0) &&
   1741 						xmlnode.getElementsByTagName('target')[0].firstChild)
   1742 					? xmlnode.getElementsByTagName('target')[0].firstChild.nodeValue
   1743 					: null,
   1744 			minVersion: xmlnode.getAttribute('minVersion'),
   1745 			maxVersion: xmlnode.getAttribute('maxVersion'),
   1746 			priority: parseInt(
   1747 				xmlnode.getElementsByTagName('priority')[0].firstChild.nodeValue
   1748 			),
   1749 			inRepository: true,
   1750 		};
   1751 		
   1752 		var browserSupport = xmlnode.getAttribute('browserSupport');
   1753 		if (browserSupport) {
   1754 			metadata.browserSupport = browserSupport;
   1755 		}
   1756 		
   1757 		for (let attr of ["configOptions", "displayOptions", "hiddenPrefs"]) {
   1758 			try {
   1759 				var tags = xmlnode.getElementsByTagName(attr);
   1760 				if(tags.length && tags[0].firstChild) {
   1761 					metadata[attr] = JSON.parse(tags[0].firstChild.nodeValue);
   1762 				}
   1763 			} catch(e) {
   1764 				Zotero.logError("Invalid JSON for "+attr+" in new version of "+metadata.label+" ("+translatorID+") from repository");
   1765 				return;
   1766 			}
   1767 		}
   1768 		
   1769 		metadata.lastUpdated = xmlnode.getAttribute('lastUpdated');
   1770 		
   1771 		// detectCode can not exist or be empty
   1772 		var detectCode = (xmlnode.getElementsByTagName('detectCode').item(0) &&
   1773 					xmlnode.getElementsByTagName('detectCode')[0].firstChild)
   1774 				? xmlnode.getElementsByTagName('detectCode')[0].firstChild.nodeValue
   1775 				: null;
   1776 		var code = xmlnode.getElementsByTagName('code')[0].firstChild.nodeValue;
   1777 		code = (detectCode ? detectCode + "\n\n" : "") + code;
   1778 		
   1779 		return Zotero.Translators.save(metadata, code);
   1780 	});
   1781 	
   1782 	
   1783 	/**
   1784 	 * Traverse an XML style node from the repository and
   1785 	 * update the local styles folder with the style data
   1786 	 */
   1787 	var _styleXMLToFile = Zotero.Promise.coroutine(function* (xmlnode) {
   1788 		// Don't split >4K chunks into multiple nodes
   1789 		// https://bugzilla.mozilla.org/show_bug.cgi?id=194231
   1790 		xmlnode.normalize();
   1791 		
   1792 		var uri = xmlnode.getAttribute('id');
   1793 		var shortName = uri.replace("http://www.zotero.org/styles/", "");
   1794 		
   1795 		// Delete local style if CSL code is empty
   1796 		if (!xmlnode.firstChild) {
   1797 			var style = Zotero.Styles.get(uri);
   1798 			if (style) {
   1799 				yield OS.File.remove(style.path);
   1800 			}
   1801 			return;
   1802 		}
   1803 		
   1804 		// Remove renamed styles, as instructed by the server
   1805 		var oldID = xmlnode.getAttribute('oldID');
   1806 		if (oldID) {
   1807 			var style = Zotero.Styles.get(oldID, true);
   1808 			if (style && (yield OS.File.exists(style.path))) {
   1809 				Zotero.debug("Deleting renamed style '" + oldID + "'");
   1810 				yield OS.File.remove(style.path);
   1811 			}
   1812 		}
   1813 		
   1814 		var str = xmlnode.firstChild.nodeValue;
   1815 		var style = Zotero.Styles.get(uri);
   1816 		if (style) {
   1817 			yield Zotero.File.removeIfExists(style.path);
   1818 			var destFile = style.path;
   1819 		}
   1820 		else {
   1821 			// Get last part of URI for filename
   1822 			var matches = uri.match(/([^\/]+)$/);
   1823 			if (!matches) {
   1824 				throw ("Invalid style URI '" + uri + "' from repository");
   1825 			}
   1826 			var destFile = OS.Path.join(
   1827 				Zotero.getStylesDirectory().path,
   1828 				matches[1] + ".csl"
   1829 			);
   1830 			if (yield OS.File.exists(destFile)) {
   1831 				throw new Error("Different style with filename '" + matches[1]
   1832 					+ "' already exists");
   1833 			}
   1834 		}
   1835 		
   1836 		Zotero.debug("Saving style '" + uri + "'");
   1837 		return Zotero.File.putContentsAsync(destFile, str);
   1838 	});
   1839 	
   1840 	
   1841 	// TODO
   1842 	//
   1843 	// If libraryID set, make sure no relations still use a local user key, and then remove on-error code in sync.js
   1844 	
   1845 	var _migrateUserDataSchema = Zotero.Promise.coroutine(function* (fromVersion, options = {}) {
   1846 		var toVersion = yield _getSchemaSQLVersion('userdata');
   1847 		
   1848 		if (fromVersion >= toVersion) {
   1849 			return false;
   1850 		}
   1851 		
   1852 		Zotero.debug('Updating user data tables from version ' + fromVersion + ' to ' + toVersion);
   1853 		
   1854 		if (options.onBeforeUpdate) {
   1855 			let maybePromise = options.onBeforeUpdate({ minor: options.minor });
   1856 			if (maybePromise && maybePromise.then) {
   1857 				yield maybePromise;
   1858 			}
   1859 		}
   1860 		
   1861 		Zotero.DB.requireTransaction();
   1862 		
   1863 		// Step through version changes until we reach the current version
   1864 		//
   1865 		// Each block performs the changes necessary to move from the
   1866 		// previous revision to that one.
   1867 		for (let i = fromVersion + 1; i <= toVersion; i++) {
   1868 			if (i == 80) {
   1869 				yield _updateCompatibility(1);
   1870 				
   1871 				// Delete 'libraries' rows not in 'groups', which shouldn't exist
   1872 				yield Zotero.DB.queryAsync("DELETE FROM libraries WHERE libraryID != 0 AND libraryID NOT IN (SELECT libraryID FROM groups)");
   1873 				
   1874 				yield Zotero.DB.queryAsync("ALTER TABLE libraries RENAME TO librariesOld");
   1875 				yield Zotero.DB.queryAsync("CREATE TABLE libraries (\n    libraryID INTEGER PRIMARY KEY,\n    type TEXT NOT NULL,\n    editable INT NOT NULL,\n    filesEditable INT NOT NULL,\n    version INT NOT NULL DEFAULT 0,\n    lastSync INT NOT NULL DEFAULT 0,\n    lastStorageSync INT NOT NULL DEFAULT 0\n)");
   1876 				yield Zotero.DB.queryAsync("INSERT INTO libraries (libraryID, type, editable, filesEditable) VALUES (1, 'user', 1, 1)");
   1877 				yield Zotero.DB.queryAsync("INSERT INTO libraries (libraryID, type, editable, filesEditable) VALUES (4, 'publications', 1, 1)");
   1878 				yield Zotero.DB.queryAsync("INSERT INTO libraries SELECT libraryID, libraryType, editable, filesEditable, 0, 0, 0 FROM librariesOld JOIN groups USING (libraryID)");
   1879 				
   1880 				yield Zotero.DB.queryAsync("INSERT OR IGNORE INTO syncObjectTypes VALUES (7, 'setting')");
   1881 				yield Zotero.DB.queryAsync("DELETE FROM version WHERE schema IN ('userdata2', 'userdata3')");
   1882 				
   1883 				yield Zotero.DB.queryAsync("CREATE TABLE syncCache (\n    libraryID INT NOT NULL,\n    key TEXT NOT NULL,\n    syncObjectTypeID INT NOT NULL,\n    version INT NOT NULL,\n    data TEXT,\n    PRIMARY KEY (libraryID, key, syncObjectTypeID, version),\n    FOREIGN KEY (libraryID) REFERENCES libraries(libraryID) ON DELETE CASCADE,\n    FOREIGN KEY (syncObjectTypeID) REFERENCES syncObjectTypes(syncObjectTypeID)\n)");
   1884 				
   1885 				yield Zotero.DB.queryAsync("DROP TABLE translatorCache");
   1886 				yield Zotero.DB.queryAsync("CREATE TABLE translatorCache (\n    fileName TEXT PRIMARY KEY,\n    metadataJSON TEXT,\n    lastModifiedTime INT\n);");
   1887 				
   1888 				yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fki_annotations_itemID_itemAttachments_itemID");
   1889 				yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fku_annotations_itemID_itemAttachments_itemID");
   1890 				yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fkd_annotations_itemID_itemAttachments_itemID");
   1891 				yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fku_itemAttachments_itemID_annotations_itemID");
   1892 				yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fki_collections_parentCollectionID_collections_collectionID");
   1893 				yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fku_collections_parentCollectionID_collections_collectionID");
   1894 				yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fkd_collections_parentCollectionID_collections_collectionID");
   1895 				yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fku_collections_collectionID_collections_parentCollectionID");
   1896 				yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fki_collectionItems_collectionID_collections_collectionID");
   1897 				yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fku_collectionItems_collectionID_collections_collectionID");
   1898 				yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fkd_collectionItems_collectionID_collections_collectionID");
   1899 				yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fku_collections_collectionID_collectionItems_collectionID");
   1900 				yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fki_collectionItems_itemID_items_itemID");
   1901 				yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fku_collectionItems_itemID_items_itemID");
   1902 				yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fkd_collectionItems_itemID_items_itemID");
   1903 				yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fku_items_itemID_collectionItems_itemID");
   1904 				yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fki_creators_creatorDataID_creatorData_creatorDataID");
   1905 				yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fku_creators_creatorDataID_creatorData_creatorDataID");
   1906 				yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fkd_creators_creatorDataID_creatorData_creatorDataID");
   1907 				yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fku_creatorData_creatorDataID_creators_creatorDataID");
   1908 				yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fki_customBaseFieldMappings_customItemTypeID_customItemTypes_customItemTypeID");
   1909 				yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fku_customBaseFieldMappings_customItemTypeID_customItemTypes_customItemTypeID");
   1910 				yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fkd_customBaseFieldMappings_customItemTypeID_customItemTypes_customItemTypeID");
   1911 				yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fku_customItemTypes_customItemTypeID_customBaseFieldMappings_customItemTypeID");
   1912 				yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fki_customBaseFieldMappings_baseFieldID_fields_fieldID");
   1913 				yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fku_customBaseFieldMappings_baseFieldID_fields_fieldID");
   1914 				yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fki_customBaseFieldMappings_customFieldID_customFields_customFieldID");
   1915 				yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fku_customBaseFieldMappings_customFieldID_customFields_customFieldID");
   1916 				yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fkd_customBaseFieldMappings_customFieldID_customFields_customFieldID");
   1917 				yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fku_customFields_customFieldID_customBaseFieldMappings_customFieldID");
   1918 				yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fki_customItemTypeFields_customItemTypeID_customItemTypes_customItemTypeID");
   1919 				yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fku_customItemTypeFields_customItemTypeID_customItemTypes_customItemTypeID");
   1920 				yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fkd_customItemTypeFields_customItemTypeID_customItemTypes_customItemTypeID");
   1921 				yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fku_customItemTypes_customItemTypeID_customItemTypeFields_customItemTypeID");
   1922 				yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fki_customItemTypeFields_fieldID_fields_fieldID");
   1923 				yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fku_customItemTypeFields_fieldID_fields_fieldID");
   1924 				yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fku_customItemTypeFields_customFieldID_customFields_customFieldID");
   1925 				yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fkd_customItemTypeFields_customFieldID_customFields_customFieldID");
   1926 				yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fku_customFields_customFieldID_customItemTypeFields_customFieldID");
   1927 				yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fki_fulltextItems_itemID_items_itemID");
   1928 				yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fku_fulltextItems_itemID_items_itemID");
   1929 				yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fkd_fulltextItems_itemID_items_itemID");
   1930 				yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fku_items_itemID_fulltextItems_itemID");
   1931 				yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fki_fulltextItemWords_wordID_fulltextWords_wordID");
   1932 				yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fku_fulltextItemWords_wordID_fulltextWords_wordID");
   1933 				yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fkd_fulltextItemWords_wordID_fulltextWords_wordID");
   1934 				yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fku_fulltextWords_wordID_fulltextItemWords_wordID");
   1935 				yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fki_fulltextItemWords_itemID_items_itemID");
   1936 				yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fku_fulltextItemWords_itemID_items_itemID");
   1937 				yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fkd_fulltextItemWords_itemID_items_itemID");
   1938 				yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fku_items_itemID_fulltextItemWords_itemID");
   1939 				yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fki_groups_libraryID_libraries_libraryID");
   1940 				yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fku_groups_libraryID_libraries_libraryID");
   1941 				yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fkd_groups_libraryID_libraries_libraryID");
   1942 				yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fku_libraries_libraryID_groups_libraryID");
   1943 				yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fki_groupItems_createdByUserID_users_userID");
   1944 				yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fku_groupItems_createdByUserID_users_userID");
   1945 				yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fkd_groupItems_createdByUserID_users_userID");
   1946 				yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fku_users_userID_groupItems_createdByUserID");
   1947 				yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fki_groupItems_lastModifiedByUserID_users_userID");
   1948 				yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fku_groupItems_lastModifiedByUserID_users_userID");
   1949 				yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fkd_groupItems_lastModifiedByUserID_users_userID");
   1950 				yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fku_users_userID_groupItems_lastModifiedByUserID");
   1951 				yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fki_highlights_itemID_itemAttachments_itemID");
   1952 				yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fku_highlights_itemID_itemAttachments_itemID");
   1953 				yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fkd_highlights_itemID_itemAttachments_itemID");
   1954 				yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fku_itemAttachments_itemID_highlights_itemID");
   1955 				yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fki_itemAttachments_itemID_items_itemID");
   1956 				yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fku_itemAttachments_itemID_items_itemID");
   1957 				yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fkd_itemAttachments_itemID_items_itemID");
   1958 				yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fku_items_itemID_itemAttachments_itemID");
   1959 				yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fki_itemAttachments_sourceItemID_items_itemID");
   1960 				yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fku_itemAttachments_sourceItemID_items_itemID");
   1961 				yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fkd_itemAttachments_sourceItemID_items_itemID");
   1962 				yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fku_items_itemID_itemAttachments_sourceItemID");
   1963 				yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fki_itemCreators_itemID_items_itemID");
   1964 				yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fku_itemCreators_itemID_items_itemID");
   1965 				yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fkd_itemCreators_itemID_items_itemID");
   1966 				yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fku_items_itemID_itemCreators_itemID");
   1967 				yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fki_itemCreators_creatorID_creators_creatorID");
   1968 				yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fku_itemCreators_creatorID_creators_creatorID");
   1969 				yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fkd_itemCreators_creatorID_creators_creatorID");
   1970 				yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fku_creators_creatorID_itemCreators_creatorID");
   1971 				yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fki_itemCreators_creatorTypeID_creatorTypes_creatorTypeID");
   1972 				yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fku_itemCreators_creatorTypeID_creatorTypes_creatorTypeID");
   1973 				yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fkd_itemCreators_creatorTypeID_creatorTypes_creatorTypeID");
   1974 				yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fku_creatorTypes_creatorTypeID_itemCreators_creatorTypeID");
   1975 				yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fki_itemCreators_libraryID");
   1976 				yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fku_itemCreators_libraryID");
   1977 				yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fki_itemData_itemID_items_itemID");
   1978 				yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fku_itemData_itemID_items_itemID");
   1979 				yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fkd_itemData_itemID_items_itemID");
   1980 				yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fku_items_itemID_itemData_itemID");
   1981 				yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fki_itemData_fieldID_fields_fieldID");
   1982 				yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fku_itemData_fieldID_fields_fieldID");
   1983 				yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fki_itemData_valueID_itemDataValues_valueID");
   1984 				yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fku_itemData_valueID_itemDataValues_valueID");
   1985 				yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fkd_itemData_valueID_itemDataValues_valueID");
   1986 				yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fku_itemDataValues_valueID_itemData_valueID");
   1987 				yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fki_itemNotes_itemID_items_itemID");
   1988 				yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fku_itemNotes_itemID_items_itemID");
   1989 				yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fkd_itemNotes_itemID_items_itemID");
   1990 				yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fku_items_itemID_itemNotes_itemID");
   1991 				yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fki_itemNotes_sourceItemID_items_itemID");
   1992 				yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fku_itemNotes_sourceItemID_items_itemID");
   1993 				yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fkd_itemNotes_sourceItemID_items_itemID");
   1994 				yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fku_items_itemID_itemNotes_sourceItemID");
   1995 				yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fki_items_libraryID_libraries_libraryID");
   1996 				yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fku_items_libraryID_libraries_libraryID");
   1997 				yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fkd_items_libraryID_libraries_libraryID");
   1998 				yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fku_libraries_libraryID_items_libraryID");
   1999 				yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fki_itemSeeAlso_itemID_items_itemID");
   2000 				yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fku_itemSeeAlso_itemID_items_itemID");
   2001 				yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fkd_itemSeeAlso_itemID_items_itemID");
   2002 				yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fku_items_itemID_itemSeeAlso_itemID");
   2003 				yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fki_itemSeeAlso_linkedItemID_items_itemID");
   2004 				yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fku_itemSeeAlso_linkedItemID_items_itemID");
   2005 				yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fkd_itemSeeAlso_linkedItemID_items_itemID");
   2006 				yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fku_items_itemID_itemSeeAlso_linkedItemID");
   2007 				yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fki_itemTags_itemID_items_itemID");
   2008 				yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fku_itemTags_itemID_items_itemID");
   2009 				yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fkd_itemTags_itemID_items_itemID");
   2010 				yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fkd_items_itemID_itemTags_itemID");
   2011 				yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fki_itemTags_tagID_tags_tagID");
   2012 				yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fku_itemTags_tagID_tags_tagID");
   2013 				yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fkd_itemTags_tagID_tags_tagID");
   2014 				yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fku_tags_tagID_itemTags_tagID");
   2015 				yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fki_savedSearchConditions_savedSearchID_savedSearches_savedSearchID");
   2016 				yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fku_savedSearchConditions_savedSearchID_savedSearches_savedSearchID");
   2017 				yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fkd_savedSearchConditions_savedSearchID_savedSearches_savedSearchID");
   2018 				yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fku_savedSearches_savedSearchID_savedSearchConditions_savedSearchID");
   2019 				yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fki_deletedItems_itemID_items_itemID");
   2020 				yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fku_deletedItems_itemID_items_itemID");
   2021 				yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fkd_deletedItems_itemID_items_itemID");
   2022 				yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fku_items_itemID_deletedItems_itemID");
   2023 				yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fki_syncDeleteLog_syncObjectTypeID_syncObjectTypes_syncObjectTypeID");
   2024 				yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fku_syncDeleteLog_syncObjectTypeID_syncObjectTypes_syncObjectTypeID");
   2025 				yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fkd_syncDeleteLog_syncObjectTypeID_syncObjectTypes_syncObjectTypeID");
   2026 				yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fku_syncObjectTypes_syncObjectTypeID_syncDeleteLog_syncObjectTypeID");
   2027 				yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fki_proxyHosts_proxyID_proxies_proxyID");
   2028 				yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fku_proxyHosts_proxyID_proxies_proxyID");
   2029 				yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fkd_proxyHosts_proxyID_proxies_proxyID");
   2030 				yield Zotero.DB.queryAsync("DROP TRIGGER IF EXISTS fku_proxies_proxyID_proxyHosts_proxyID");
   2031 				
   2032 				yield Zotero.DB.queryAsync("ALTER TABLE collections RENAME TO collectionsOld");
   2033 				yield Zotero.DB.queryAsync("CREATE TABLE collections (\n    collectionID INTEGER PRIMARY KEY,\n    collectionName TEXT NOT NULL,\n    parentCollectionID INT DEFAULT NULL,\n    clientDateModified TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,\n    libraryID INT NOT NULL,\n    key TEXT NOT NULL,\n    version INT NOT NULL DEFAULT 0,\n    synced INT NOT NULL DEFAULT 0,\n    UNIQUE (libraryID, key),\n    FOREIGN KEY (libraryID) REFERENCES libraries(libraryID) ON DELETE CASCADE,\n    FOREIGN KEY (parentCollectionID) REFERENCES collections(collectionID) ON DELETE CASCADE\n)");
   2034 				yield Zotero.DB.queryAsync("INSERT OR IGNORE INTO collections SELECT collectionID, collectionName, parentCollectionID, clientDateModified, IFNULL(libraryID, 1), key, 0, 0 FROM collectionsOld ORDER BY collectionID DESC");
   2035 				yield Zotero.DB.queryAsync("CREATE INDEX collections_synced ON collections(synced)");
   2036 				
   2037 				yield Zotero.DB.queryAsync("ALTER TABLE items RENAME TO itemsOld");
   2038 				yield Zotero.DB.queryAsync("CREATE TABLE items (\n    itemID INTEGER PRIMARY KEY,\n    itemTypeID INT NOT NULL,\n    dateAdded TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,\n    dateModified TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,\n    clientDateModified TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,\n    libraryID INT NOT NULL,\n    key TEXT NOT NULL,\n    version INT NOT NULL DEFAULT 0,\n    synced INT NOT NULL DEFAULT 0,\n    UNIQUE (libraryID, key),\n    FOREIGN KEY (libraryID) REFERENCES libraries(libraryID) ON DELETE CASCADE\n)");
   2039 				yield Zotero.DB.queryAsync("INSERT OR IGNORE INTO items SELECT itemID, itemTypeID, dateAdded, dateModified, clientDateModified, IFNULL(libraryID, 1), key, 0, 0 FROM itemsOld ORDER BY dateAdded DESC");
   2040 				yield Zotero.DB.queryAsync("CREATE INDEX items_synced ON items(synced)");
   2041 				
   2042 				let rows = yield Zotero.DB.queryAsync("SELECT firstName, lastName, fieldMode, COUNT(*) FROM creatorData GROUP BY firstName, lastName, fieldMode HAVING COUNT(*) > 1");
   2043 				for (let row of rows) {
   2044 					let ids = yield Zotero.DB.columnQueryAsync("SELECT creatorDataID FROM creatorData WHERE firstName=? AND lastName=? AND fieldMode=?", [row.firstName, row.lastName, row.fieldMode]);
   2045 					yield Zotero.DB.queryAsync("UPDATE creators SET creatorDataID=" + ids[0] + " WHERE creatorDataID IN (" + ids.slice(1).join(", ") + ")");
   2046 				}
   2047 				yield Zotero.DB.queryAsync("DELETE FROM creatorData WHERE creatorDataID NOT IN (SELECT creatorDataID FROM creators)");
   2048 				yield Zotero.DB.queryAsync("ALTER TABLE creators RENAME TO creatorsOld");
   2049 				yield Zotero.DB.queryAsync("CREATE TABLE creators (\n    creatorID INTEGER PRIMARY KEY,\n    firstName TEXT,\n    lastName TEXT,\n    fieldMode INT,\n    UNIQUE (lastName, firstName, fieldMode)\n)");
   2050 				yield Zotero.DB.queryAsync("INSERT INTO creators SELECT creatorDataID, firstName, lastName, fieldMode FROM creatorData");
   2051 				yield Zotero.DB.queryAsync("ALTER TABLE itemCreators RENAME TO itemCreatorsOld");
   2052 				yield Zotero.DB.queryAsync("CREATE TABLE itemCreators (\n    itemID INT NOT NULL,\n    creatorID INT NOT NULL,\n    creatorTypeID INT NOT NULL DEFAULT 1,\n    orderIndex INT NOT NULL DEFAULT 0,\n    PRIMARY KEY (itemID, creatorID, creatorTypeID, orderIndex),\n    UNIQUE (itemID, orderIndex),\n    FOREIGN KEY (itemID) REFERENCES items(itemID) ON DELETE CASCADE,\n    FOREIGN KEY (creatorID) REFERENCES creators(creatorID) ON DELETE CASCADE,\n    FOREIGN KEY (creatorTypeID) REFERENCES creatorTypes(creatorTypeID)\n)");
   2053 				yield Zotero.DB.queryAsync("CREATE INDEX itemCreators_creatorTypeID ON itemCreators(creatorTypeID)");
   2054 				yield Zotero.DB.queryAsync("INSERT OR IGNORE INTO itemCreators SELECT itemID, C.creatorID, creatorTypeID, orderIndex FROM itemCreatorsOld ICO JOIN creatorsOld CO USING (creatorID) JOIN creators C ON (CO.creatorDataID=C.creatorID)");
   2055 				
   2056 				yield Zotero.DB.queryAsync("ALTER TABLE savedSearches RENAME TO savedSearchesOld");
   2057 				yield Zotero.DB.queryAsync("CREATE TABLE savedSearches (\n    savedSearchID INTEGER PRIMARY KEY,\n    savedSearchName TEXT NOT NULL,\n    clientDateModified TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,\n    libraryID INT NOT NULL,\n    key TEXT NOT NULL,\n    version INT NOT NULL DEFAULT 0,\n    synced INT NOT NULL DEFAULT 0,\n    UNIQUE (libraryID, key),\n    FOREIGN KEY (libraryID) REFERENCES libraries(libraryID) ON DELETE CASCADE\n)");
   2058 				yield Zotero.DB.queryAsync("INSERT OR IGNORE INTO savedSearches SELECT savedSearchID, savedSearchName, clientDateModified, IFNULL(libraryID, 1), key, 0, 0 FROM savedSearchesOld ORDER BY savedSearchID DESC");
   2059 				yield Zotero.DB.queryAsync("CREATE INDEX savedSearches_synced ON savedSearches(synced)");
   2060 				
   2061 				yield Zotero.DB.queryAsync("ALTER TABLE tags RENAME TO tagsOld");
   2062 				yield Zotero.DB.queryAsync("CREATE TABLE tags (\n    tagID INTEGER PRIMARY KEY,\n    name TEXT NOT NULL UNIQUE\n)");
   2063 				yield Zotero.DB.queryAsync("INSERT OR IGNORE INTO tags SELECT tagID, name FROM tagsOld");
   2064 				yield Zotero.DB.queryAsync("ALTER TABLE itemTags RENAME TO itemTagsOld");
   2065 				yield Zotero.DB.queryAsync("CREATE TABLE itemTags (\n    itemID INT NOT NULL,\n    tagID INT NOT NULL,\n    type INT NOT NULL,\n    PRIMARY KEY (itemID, tagID),\n    FOREIGN KEY (itemID) REFERENCES items(itemID) ON DELETE CASCADE,\n    FOREIGN KEY (tagID) REFERENCES tags(tagID) ON DELETE CASCADE\n)");
   2066 				yield Zotero.DB.queryAsync("INSERT OR IGNORE INTO itemTags SELECT itemID, T.tagID, TOld.type FROM itemTagsOld ITO JOIN tagsOld TOld USING (tagID) JOIN tags T ON (TOld.name=T.name COLLATE BINARY)");
   2067 				yield Zotero.DB.queryAsync("DROP INDEX IF EXISTS itemTags_tagID");
   2068 				yield Zotero.DB.queryAsync("CREATE INDEX itemTags_tagID ON itemTags(tagID)");
   2069 				
   2070 				yield Zotero.DB.queryAsync("CREATE TABLE IF NOT EXISTS syncedSettings (\n    setting TEXT NOT NULL,\n    libraryID INT NOT NULL,\n    value NOT NULL,\n    version INT NOT NULL DEFAULT 0,\n    synced INT NOT NULL DEFAULT 0,\n    PRIMARY KEY (setting, libraryID)\n)");
   2071 				yield Zotero.DB.queryAsync("ALTER TABLE syncedSettings RENAME TO syncedSettingsOld");
   2072 				yield Zotero.DB.queryAsync("CREATE TABLE syncedSettings (\n    setting TEXT NOT NULL,\n    libraryID INT NOT NULL,\n    value NOT NULL,\n    version INT NOT NULL DEFAULT 0,\n    synced INT NOT NULL DEFAULT 0,\n    PRIMARY KEY (setting, libraryID),\n    FOREIGN KEY (libraryID) REFERENCES libraries(libraryID) ON DELETE CASCADE\n)");
   2073 				yield Zotero.DB.queryAsync("UPDATE syncedSettingsOld SET libraryID=1 WHERE libraryID=0");
   2074 				yield Zotero.DB.queryAsync("INSERT OR IGNORE INTO syncedSettings SELECT * FROM syncedSettingsOld");
   2075 				
   2076 				yield Zotero.DB.queryAsync("ALTER TABLE itemData RENAME TO itemDataOld");
   2077 				yield Zotero.DB.queryAsync("CREATE TABLE itemData (\n    itemID INT,\n    fieldID INT,\n    valueID,\n    PRIMARY KEY (itemID, fieldID),\n    FOREIGN KEY (itemID) REFERENCES items(itemID) ON DELETE CASCADE,\n    FOREIGN KEY (fieldID) REFERENCES fieldsCombined(fieldID),\n    FOREIGN KEY (valueID) REFERENCES itemDataValues(valueID)\n)");
   2078 				yield Zotero.DB.queryAsync("INSERT OR IGNORE INTO itemData SELECT * FROM itemDataOld");
   2079 				yield Zotero.DB.queryAsync("DROP INDEX IF EXISTS itemData_fieldID");
   2080 				yield Zotero.DB.queryAsync("CREATE INDEX itemData_fieldID ON itemData(fieldID)");
   2081 				
   2082 				yield Zotero.DB.queryAsync("ALTER TABLE itemNotes RENAME TO itemNotesOld");
   2083 				yield Zotero.DB.queryAsync("CREATE TABLE itemNotes (\n    itemID INTEGER PRIMARY KEY,\n    parentItemID INT,\n    note TEXT,\n    title TEXT,\n    FOREIGN KEY (itemID) REFERENCES items(itemID) ON DELETE CASCADE,\n    FOREIGN KEY (parentItemID) REFERENCES items(itemID) ON DELETE CASCADE\n)");
   2084 				yield Zotero.DB.queryAsync("INSERT OR IGNORE INTO itemNotes SELECT * FROM itemNotesOld");
   2085 				yield Zotero.DB.queryAsync("CREATE INDEX itemNotes_parentItemID ON itemNotes(parentItemID)");
   2086 				
   2087 				yield Zotero.DB.queryAsync("CREATE TEMPORARY TABLE charsetsOld (charsetID INT, charset UNIQUE, canonical, PRIMARY KEY (charsetID))");
   2088 				yield Zotero.DB.queryAsync("INSERT INTO charsetsOld VALUES (1,'utf-8','utf-8'), (2,'ascii','windows-1252'), (3,'windows-1250','windows-1250'), (4,'windows-1251','windows-1251'), (5,'windows-1252','windows-1252'), (6,'windows-1253','windows-1253'), (7,'windows-1254','windows-1254'), (8,'windows-1257','windows-1257'), (9,'us',NULL), (10,'us-ascii','windows-1252'), (11,'utf-7',NULL), (12,'iso8859-1','windows-1252'), (13,'iso8859-15','iso-8859-15'), (14,'iso_646.irv:1991',NULL), (15,'iso_8859-1','windows-1252'), (16,'iso_8859-1:1987','windows-1252'), (17,'iso_8859-2','iso-8859-2'), (18,'iso_8859-2:1987','iso-8859-2'), (19,'iso_8859-4','iso-8859-4'), (20,'iso_8859-4:1988','iso-8859-4'), (21,'iso_8859-5','iso-8859-5'), (22,'iso_8859-5:1988','iso-8859-5'), (23,'iso_8859-7','iso-8859-7'), (24,'iso_8859-7:1987','iso-8859-7'), (25,'iso-8859-1','windows-1252'), (26,'iso-8859-1-windows-3.0-latin-1',NULL), (27,'iso-8859-1-windows-3.1-latin-1',NULL), (28,'iso-8859-15','iso-8859-15'), (29,'iso-8859-2','iso-8859-2'), (30,'iso-8859-2-windows-latin-2',NULL), (31,'iso-8859-3','iso-8859-3'), (32,'iso-8859-4','iso-8859-4'), (33,'iso-8859-5','iso-8859-5'), (34,'iso-8859-5-windows-latin-5',NULL), (35,'iso-8859-6','iso-8859-6'), (36,'iso-8859-7','iso-8859-7'), (37,'iso-8859-8','iso-8859-8'), (38,'iso-8859-9','windows-1254'), (39,'l1','windows-1252'), (40,'l2','iso-8859-2'), (41,'l4','iso-8859-4'), (42,'latin1','windows-1252'), (43,'latin2','iso-8859-2'), (44,'latin4','iso-8859-4'), (45,'x-mac-ce',NULL), (46,'x-mac-cyrillic','x-mac-cyrillic'), (47,'x-mac-greek',NULL), (48,'x-mac-roman','macintosh'), (49,'x-mac-turkish',NULL), (50,'adobe-symbol-encoding',NULL), (51,'ansi_x3.4-1968','windows-1252'), (52,'ansi_x3.4-1986',NULL), (53,'big5','big5'), (54,'chinese','gbk'), (55,'cn-big5','big5'), (56,'cn-gb',NULL), (57,'cn-gb-isoir165',NULL), (58,'cp367',NULL), (59,'cp819','windows-1252'), (60,'cp850',NULL), (61,'cp852',NULL), (62,'cp855',NULL), (63,'cp857',NULL), (64,'cp862',NULL), (65,'cp864',NULL), (66,'cp866','ibm866'), (67,'csascii',NULL), (68,'csbig5','big5'), (69,'cseuckr','euc-kr'), (70,'cseucpkdfmtjapanese','euc-jp'), (71,'csgb2312','gbk'), (72,'cshalfwidthkatakana',NULL), (73,'cshppsmath',NULL), (74,'csiso103t618bit',NULL), (75,'csiso159jisx02121990',NULL), (76,'csiso2022jp','iso-2022-jp'), (77,'csiso2022jp2',NULL), (78,'csiso2022kr','replacement'), (79,'csiso58gb231280','gbk'), (80,'csisolatin4','iso-8859-4'), (81,'csisolatincyrillic','iso-8859-5'), (82,'csisolatingreek','iso-8859-7'), (83,'cskoi8r','koi8-r'), (84,'csksc56011987','euc-kr'), (85,'csshiftjis','shift_jis'), (86,'csunicode11',NULL), (87,'csunicode11utf7',NULL), (88,'csunicodeascii',NULL), (89,'csunicodelatin1',NULL), (90,'cswindows31latin5',NULL), (91,'cyrillic','iso-8859-5'), (92,'ecma-118','iso-8859-7'), (93,'elot_928','iso-8859-7'), (94,'euc-jp','euc-jp'), (95,'euc-kr','euc-kr'), (96,'extended_unix_code_packed_format_for_japanese',NULL), (97,'gb2312','gbk'), (98,'gb_2312-80','gbk'), (99,'greek','iso-8859-7'), (100,'greek8','iso-8859-7'), (101,'hz-gb-2312','replacement'), (102,'ibm367',NULL), (103,'ibm819','windows-1252'), (104,'ibm850',NULL), (105,'ibm852',NULL), (106,'ibm855',NULL), (107,'ibm857',NULL), (108,'ibm862',NULL), (109,'ibm864',NULL), (110,'ibm866','ibm866'), (111,'iso-10646',NULL), (112,'iso-10646-j-1',NULL), (113,'iso-10646-ucs-2',NULL), (114,'iso-10646-ucs-4',NULL), (115,'iso-10646-ucs-basic',NULL), (116,'iso-10646-unicode-latin1',NULL), (117,'iso-2022-jp','iso-2022-jp'), (118,'iso-2022-jp-2',NULL), (119,'iso-2022-kr','replacement'), (120,'iso-ir-100','windows-1252'), (121,'iso-ir-101','iso-8859-2'), (122,'iso-ir-103',NULL), (123,'iso-ir-110','iso-8859-4'), (124,'iso-ir-126','iso-8859-7'), (125,'iso-ir-144','iso-8859-5'), (126,'iso-ir-149','euc-kr'), (127,'iso-ir-159',NULL), (128,'iso-ir-58','gbk'), (129,'iso-ir-6',NULL), (130,'iso646-us',NULL), (131,'jis_x0201',NULL), (132,'jis_x0208-1983',NULL), (133,'jis_x0212-1990',NULL), (134,'koi8-r','koi8-r'), (135,'korean','euc-kr'), (136,'ks_c_5601',NULL), (137,'ks_c_5601-1987','euc-kr'), (138,'ks_c_5601-1989','euc-kr'), (139,'ksc5601','euc-kr'), (140,'ksc_5601','euc-kr'), (141,'ms_kanji','shift_jis'), (142,'shift_jis','shift_jis'), (143,'t.61',NULL), (144,'t.61-8bit',NULL), (145,'unicode-1-1-utf-7',NULL), (146,'unicode-1-1-utf-8','utf-8'), (147,'unicode-2-0-utf-7',NULL), (148,'windows-31j','shift_jis'), (149,'x-cns11643-1',NULL), (150,'x-cns11643-1110',NULL), (151,'x-cns11643-2',NULL), (152,'x-cp1250','windows-1250'), (153,'x-cp1251','windows-1251'), (154,'x-cp1253','windows-1253'), (155,'x-dectech',NULL), (156,'x-dingbats',NULL), (157,'x-euc-jp','euc-jp'), (158,'x-euc-tw',NULL), (159,'x-gb2312-11',NULL), (160,'x-imap4-modified-utf7',NULL), (161,'x-jisx0208-11',NULL), (162,'x-ksc5601-11',NULL), (163,'x-sjis','shift_jis'), (164,'x-tis620',NULL), (165,'x-unicode-2-0-utf-7',NULL), (166,'x-x-big5','big5'), (167,'x0201',NULL), (168,'x0212',NULL)");
   2089 				yield Zotero.DB.queryAsync("CREATE INDEX charsetsOld_canonical ON charsetsOld(canonical)");
   2090 				
   2091 				yield Zotero.DB.queryAsync("ALTER TABLE itemAttachments RENAME TO itemAttachmentsOld");
   2092 				yield Zotero.DB.queryAsync("CREATE TABLE itemAttachments (\n    itemID INTEGER PRIMARY KEY,\n    parentItemID INT,\n    linkMode INT,\n    contentType TEXT,\n    charsetID INT,\n    path TEXT,\n    syncState INT DEFAULT 0,\n    storageModTime INT,\n    storageHash TEXT,\n    FOREIGN KEY (itemID) REFERENCES items(itemID) ON DELETE CASCADE,\n    FOREIGN KEY (parentItemID) REFERENCES items(itemID) ON DELETE CASCADE,\n    FOREIGN KEY (charsetID) REFERENCES charsets(charsetID) ON DELETE SET NULL\n)");
   2093 				yield Zotero.DB.queryAsync("INSERT OR IGNORE INTO itemAttachments SELECT itemID, sourceItemID, linkMode, mimeType, C.charsetID, path, syncState, storageModTime, storageHash FROM itemAttachmentsOld IA LEFT JOIN charsetsOld CO ON (IA.charsetID=CO.charsetID) LEFT JOIN charsets C ON (CO.canonical=C.charset)");
   2094 				yield Zotero.DB.queryAsync("CREATE INDEX itemAttachments_parentItemID ON itemAttachments(parentItemID)");
   2095 				yield Zotero.DB.queryAsync("CREATE INDEX itemAttachments_charsetID ON itemAttachments(charsetID)");
   2096 				yield Zotero.DB.queryAsync("CREATE INDEX itemAttachments_contentType ON itemAttachments(contentType)");
   2097 				yield Zotero.DB.queryAsync("DROP INDEX IF EXISTS itemAttachments_syncState");
   2098 				yield Zotero.DB.queryAsync("CREATE INDEX itemAttachments_syncState ON itemAttachments(syncState)");
   2099 				
   2100 				yield _migrateUserData_80_filePaths();
   2101 				
   2102 				yield Zotero.DB.queryAsync("ALTER TABLE collectionItems RENAME TO collectionItemsOld");
   2103 				yield Zotero.DB.queryAsync("CREATE TABLE collectionItems (\n    collectionID INT NOT NULL,\n    itemID INT NOT NULL,\n    orderIndex INT NOT NULL DEFAULT 0,\n    PRIMARY KEY (collectionID, itemID),\n    FOREIGN KEY (collectionID) REFERENCES collections(collectionID) ON DELETE CASCADE,\n    FOREIGN KEY (itemID) REFERENCES items(itemID) ON DELETE CASCADE\n)");
   2104 				yield Zotero.DB.queryAsync("INSERT OR IGNORE INTO collectionItems SELECT * FROM collectionItemsOld");
   2105 				yield Zotero.DB.queryAsync("DROP INDEX IF EXISTS itemID"); // incorrect old name
   2106 				yield Zotero.DB.queryAsync("CREATE INDEX collectionItems_itemID ON collectionItems(itemID)");
   2107 				
   2108 				yield Zotero.DB.queryAsync("ALTER TABLE savedSearchConditions RENAME TO savedSearchConditionsOld");
   2109 				yield Zotero.DB.queryAsync("CREATE TABLE savedSearchConditions (\n    savedSearchID INT NOT NULL,\n    searchConditionID INT NOT NULL,\n    condition TEXT NOT NULL,\n    operator TEXT,\n    value TEXT,\n    required NONE,\n    PRIMARY KEY (savedSearchID, searchConditionID),\n    FOREIGN KEY (savedSearchID) REFERENCES savedSearches(savedSearchID) ON DELETE CASCADE\n)");
   2110 				yield Zotero.DB.queryAsync("INSERT OR IGNORE INTO savedSearchConditions SELECT * FROM savedSearchConditionsOld");
   2111 				yield Zotero.DB.queryAsync("DROP TABLE savedSearchConditionsOld");
   2112 				
   2113 				yield Zotero.DB.queryAsync("ALTER TABLE deletedItems RENAME TO deletedItemsOld");
   2114 				yield Zotero.DB.queryAsync("CREATE TABLE deletedItems (\n    itemID INTEGER PRIMARY KEY,\n    dateDeleted DEFAULT CURRENT_TIMESTAMP NOT NULL,\n    FOREIGN KEY (itemID) REFERENCES items(itemID) ON DELETE CASCADE\n)");
   2115 				yield Zotero.DB.queryAsync("INSERT OR IGNORE INTO deletedItems SELECT * FROM deletedItemsOld");
   2116 				yield Zotero.DB.queryAsync("DROP INDEX IF EXISTS deletedItems_dateDeleted");
   2117 				yield Zotero.DB.queryAsync("CREATE INDEX deletedItems_dateDeleted ON deletedItems(dateDeleted)");
   2118 				
   2119 				yield _migrateUserData_80_relations();
   2120 				
   2121 				yield Zotero.DB.queryAsync("ALTER TABLE groups RENAME TO groupsOld");
   2122 				yield Zotero.DB.queryAsync("CREATE TABLE groups (\n    groupID INTEGER PRIMARY KEY,\n    libraryID INT NOT NULL UNIQUE,\n    name TEXT NOT NULL,\n    description TEXT NOT NULL,\n    version INT NOT NULL,\n    FOREIGN KEY (libraryID) REFERENCES libraries(libraryID) ON DELETE CASCADE\n)");
   2123 				yield Zotero.DB.queryAsync("INSERT OR IGNORE INTO groups SELECT groupID, libraryID, name, description, 0 FROM groupsOld");
   2124 				
   2125 				yield Zotero.DB.queryAsync("ALTER TABLE groupItems RENAME TO groupItemsOld");
   2126 				yield Zotero.DB.queryAsync("CREATE TABLE groupItems (\n    itemID INTEGER PRIMARY KEY,\n    createdByUserID INT,\n    lastModifiedByUserID INT,\n    FOREIGN KEY (itemID) REFERENCES items(itemID) ON DELETE CASCADE,\n    FOREIGN KEY (createdByUserID) REFERENCES users(userID) ON DELETE SET NULL,\n    FOREIGN KEY (lastModifiedByUserID) REFERENCES users(userID) ON DELETE SET NULL\n)");
   2127 				yield Zotero.DB.queryAsync("INSERT OR IGNORE INTO groupItems SELECT * FROM groupItemsOld");
   2128 				
   2129 				let cols = yield Zotero.DB.getColumns('fulltextItems');
   2130 				if (cols.indexOf("synced") == -1) {
   2131 					Zotero.DB.queryAsync("ALTER TABLE fulltextItems ADD COLUMN synced INT DEFAULT 0");
   2132 				}
   2133 				yield Zotero.DB.queryAsync("DELETE FROM settings WHERE setting='fulltext'");
   2134 				yield Zotero.DB.queryAsync("ALTER TABLE fulltextItems RENAME TO fulltextItemsOld");
   2135 				yield Zotero.DB.queryAsync("CREATE TABLE fulltextItems (\n    itemID INTEGER PRIMARY KEY,\n    indexedPages INT,\n    totalPages INT,\n    indexedChars INT,\n    totalChars INT,\n    version INT NOT NULL DEFAULT 0,\n    synced INT NOT NULL DEFAULT 0,\n    FOREIGN KEY (itemID) REFERENCES items(itemID) ON DELETE CASCADE\n)");
   2136 				yield Zotero.DB.queryAsync("INSERT OR IGNORE INTO fulltextItems SELECT itemID, indexedPages, totalPages, indexedChars, totalChars, version, synced FROM fulltextItemsOld");
   2137 				yield Zotero.DB.queryAsync("DROP INDEX IF EXISTS fulltextItems_version");
   2138 				yield Zotero.DB.queryAsync("CREATE INDEX fulltextItems_synced ON fulltextItems(synced)");
   2139 				yield Zotero.DB.queryAsync("CREATE INDEX fulltextItems_version ON fulltextItems(version)");
   2140 				
   2141 				yield Zotero.DB.queryAsync("ALTER TABLE fulltextItemWords RENAME TO fulltextItemWordsOld");
   2142 				yield Zotero.DB.queryAsync("CREATE TABLE fulltextItemWords (\n    wordID INT,\n    itemID INT,\n    PRIMARY KEY (wordID, itemID),\n    FOREIGN KEY (wordID) REFERENCES fulltextWords(wordID),\n    FOREIGN KEY (itemID) REFERENCES items(itemID) ON DELETE CASCADE\n)");
   2143 				yield Zotero.DB.queryAsync("INSERT OR IGNORE INTO fulltextItemWords SELECT * FROM fulltextItemWordsOld");
   2144 				yield Zotero.DB.queryAsync("DROP INDEX IF EXISTS fulltextItemWords_itemID");
   2145 				yield Zotero.DB.queryAsync("CREATE INDEX fulltextItemWords_itemID ON fulltextItemWords(itemID)");
   2146 				
   2147 				yield Zotero.DB.queryAsync("UPDATE syncDeleteLog SET libraryID=1 WHERE libraryID=0");
   2148 				yield Zotero.DB.queryAsync("ALTER TABLE syncDeleteLog RENAME TO syncDeleteLogOld");
   2149 				yield Zotero.DB.queryAsync("CREATE TABLE syncDeleteLog (\n    syncObjectTypeID INT NOT NULL,\n    libraryID INT NOT NULL,\n    key TEXT NOT NULL,\n    dateDeleted TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,\n    UNIQUE (syncObjectTypeID, libraryID, key),\n    FOREIGN KEY (syncObjectTypeID) REFERENCES syncObjectTypes(syncObjectTypeID),\n    FOREIGN KEY (libraryID) REFERENCES libraries(libraryID) ON DELETE CASCADE\n)");
   2150 				yield Zotero.DB.queryAsync("INSERT OR IGNORE INTO syncDeleteLog SELECT syncObjectTypeID, libraryID, key, timestamp FROM syncDeleteLogOld");
   2151 				yield Zotero.DB.queryAsync("DROP INDEX IF EXISTS syncDeleteLog_timestamp");
   2152 				// TODO: Something special for tag deletions?
   2153 				//yield Zotero.DB.queryAsync("DELETE FROM syncDeleteLog WHERE syncObjectTypeID IN (2, 5, 6)");
   2154 				//yield Zotero.DB.queryAsync("DELETE FROM syncObjectTypes WHERE syncObjectTypeID IN (2, 5, 6)");
   2155 				
   2156 				yield Zotero.DB.queryAsync("UPDATE storageDeleteLog SET libraryID=1 WHERE libraryID=0");
   2157 				yield Zotero.DB.queryAsync("ALTER TABLE storageDeleteLog RENAME TO storageDeleteLogOld");
   2158 				yield Zotero.DB.queryAsync("CREATE TABLE storageDeleteLog (\n    libraryID INT NOT NULL,\n    key TEXT NOT NULL,\n    dateDeleted TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,\n    PRIMARY KEY (libraryID, key),\n    FOREIGN KEY (libraryID) REFERENCES libraries(libraryID) ON DELETE CASCADE\n)");
   2159 				yield Zotero.DB.queryAsync("INSERT OR IGNORE INTO storageDeleteLog SELECT libraryID, key, timestamp FROM storageDeleteLogOld");
   2160 				yield Zotero.DB.queryAsync("DROP INDEX IF EXISTS storageDeleteLog_timestamp");
   2161 				
   2162 				yield Zotero.DB.queryAsync("ALTER TABLE annotations RENAME TO annotationsOld");
   2163 				yield Zotero.DB.queryAsync("CREATE TABLE annotations (\n    annotationID INTEGER PRIMARY KEY,\n    itemID INT NOT NULL,\n    parent TEXT,\n    textNode INT,\n    offset INT,\n    x INT,\n    y INT,\n    cols INT,\n    rows INT,\n    text TEXT,\n    collapsed BOOL,\n    dateModified DATE,\n    FOREIGN KEY (itemID) REFERENCES itemAttachments(itemID) ON DELETE CASCADE\n)");
   2164 				yield Zotero.DB.queryAsync("INSERT OR IGNORE INTO annotations SELECT * FROM annotationsOld");
   2165 				yield Zotero.DB.queryAsync("DROP INDEX IF EXISTS annotations_itemID");
   2166 				yield Zotero.DB.queryAsync("CREATE INDEX annotations_itemID ON annotations(itemID)");
   2167 				
   2168 				yield Zotero.DB.queryAsync("ALTER TABLE highlights RENAME TO highlightsOld");
   2169 				yield Zotero.DB.queryAsync("CREATE TABLE highlights (\n    highlightID INTEGER PRIMARY KEY,\n    itemID INT NOT NULL,\n    startParent TEXT,\n    startTextNode INT,\n    startOffset INT,\n    endParent TEXT,\n    endTextNode INT,\n    endOffset INT,\n    dateModified DATE,\n    FOREIGN KEY (itemID) REFERENCES itemAttachments(itemID) ON DELETE CASCADE\n)");
   2170 				yield Zotero.DB.queryAsync("INSERT OR IGNORE INTO highlights SELECT * FROM highlightsOld");
   2171 				yield Zotero.DB.queryAsync("DROP INDEX IF EXISTS highlights_itemID");
   2172 				yield Zotero.DB.queryAsync("CREATE INDEX highlights_itemID ON highlights(itemID)");
   2173 				
   2174 				yield Zotero.DB.queryAsync("ALTER TABLE customBaseFieldMappings RENAME TO customBaseFieldMappingsOld");
   2175 				yield Zotero.DB.queryAsync("CREATE TABLE customBaseFieldMappings (\n    customItemTypeID INT,\n    baseFieldID INT,\n    customFieldID INT,\n    PRIMARY KEY (customItemTypeID, baseFieldID, customFieldID),\n    FOREIGN KEY (customItemTypeID) REFERENCES customItemTypes(customItemTypeID),\n    FOREIGN KEY (baseFieldID) REFERENCES fields(fieldID),\n    FOREIGN KEY (customFieldID) REFERENCES customFields(customFieldID)\n)");
   2176 				yield Zotero.DB.queryAsync("INSERT OR IGNORE INTO customBaseFieldMappings SELECT * FROM customBaseFieldMappingsOld");
   2177 				yield Zotero.DB.queryAsync("DROP INDEX IF EXISTS customBaseFieldMappings_baseFieldID");
   2178 				yield Zotero.DB.queryAsync("DROP INDEX IF EXISTS customBaseFieldMappings_customFieldID");
   2179 				yield Zotero.DB.queryAsync("CREATE INDEX customBaseFieldMappings_baseFieldID ON customBaseFieldMappings(baseFieldID)");
   2180 				yield Zotero.DB.queryAsync("CREATE INDEX customBaseFieldMappings_customFieldID ON customBaseFieldMappings(customFieldID)");
   2181 				
   2182 				yield Zotero.DB.queryAsync("DELETE FROM settings WHERE setting='account' AND key='libraryID'");
   2183 				
   2184 				yield Zotero.DB.queryAsync("DROP TABLE annotationsOld");
   2185 				yield Zotero.DB.queryAsync("DROP TABLE collectionItemsOld");
   2186 				yield Zotero.DB.queryAsync("DROP TABLE charsetsOld");
   2187 				yield Zotero.DB.queryAsync("DROP TABLE customBaseFieldMappingsOld");
   2188 				yield Zotero.DB.queryAsync("DROP TABLE deletedItemsOld");
   2189 				yield Zotero.DB.queryAsync("DROP TABLE fulltextItemWordsOld");
   2190 				yield Zotero.DB.queryAsync("DROP TABLE fulltextItemsOld");
   2191 				yield Zotero.DB.queryAsync("DROP TABLE groupItemsOld");
   2192 				yield Zotero.DB.queryAsync("DROP TABLE groupsOld");
   2193 				yield Zotero.DB.queryAsync("DROP TABLE highlightsOld");
   2194 				yield Zotero.DB.queryAsync("DROP TABLE itemAttachmentsOld");
   2195 				yield Zotero.DB.queryAsync("DROP TABLE itemCreatorsOld");
   2196 				yield Zotero.DB.queryAsync("DROP TABLE itemDataOld");
   2197 				yield Zotero.DB.queryAsync("DROP TABLE itemNotesOld");
   2198 				yield Zotero.DB.queryAsync("DROP TABLE itemTagsOld");
   2199 				yield Zotero.DB.queryAsync("DROP TABLE savedSearchesOld");
   2200 				yield Zotero.DB.queryAsync("DROP TABLE storageDeleteLogOld");
   2201 				yield Zotero.DB.queryAsync("DROP TABLE syncDeleteLogOld");
   2202 				yield Zotero.DB.queryAsync("DROP TABLE syncedSettingsOld");
   2203 				yield Zotero.DB.queryAsync("DROP TABLE collectionsOld");
   2204 				yield Zotero.DB.queryAsync("DROP TABLE creatorsOld");
   2205 				yield Zotero.DB.queryAsync("DROP TABLE creatorData");
   2206 				yield Zotero.DB.queryAsync("DROP TABLE itemsOld");
   2207 				yield Zotero.DB.queryAsync("DROP TABLE tagsOld");
   2208 				yield Zotero.DB.queryAsync("DROP TABLE librariesOld");
   2209 				
   2210 			}
   2211 			
   2212 			else if (i == 81) {
   2213 				yield _updateCompatibility(2);
   2214 				
   2215 				yield Zotero.DB.queryAsync("ALTER TABLE libraries RENAME TO librariesOld");
   2216 				yield Zotero.DB.queryAsync("CREATE TABLE libraries (\n    libraryID INTEGER PRIMARY KEY,\n    type TEXT NOT NULL,\n    editable INT NOT NULL,\n    filesEditable INT NOT NULL,\n    version INT NOT NULL DEFAULT 0,\n    storageVersion INT NOT NULL DEFAULT 0,\n    lastSync INT NOT NULL DEFAULT 0\n)");
   2217 				yield Zotero.DB.queryAsync("INSERT INTO libraries SELECT libraryID, type, editable, filesEditable, version, 0, lastSync FROM librariesOld");
   2218 				yield Zotero.DB.queryAsync("DROP TABLE librariesOld");
   2219 				
   2220 				yield Zotero.DB.queryAsync("DELETE FROM version WHERE schema LIKE ?", "storage_%");
   2221 			}
   2222 			
   2223 			else if (i == 82) {
   2224 				yield Zotero.DB.queryAsync("DELETE FROM itemTypeFields WHERE itemTypeID=17 AND orderIndex BETWEEN 3 AND 9");
   2225 				yield Zotero.DB.queryAsync("INSERT INTO itemTypeFields VALUES (17, 44, NULL, 3)");
   2226 				yield Zotero.DB.queryAsync("INSERT INTO itemTypeFields VALUES (17, 96, NULL, 4)");
   2227 				yield Zotero.DB.queryAsync("INSERT INTO itemTypeFields VALUES (17, 117, NULL, 5)");
   2228 				yield Zotero.DB.queryAsync("INSERT INTO itemTypeFields VALUES (17, 43, NULL, 6)");
   2229 				yield Zotero.DB.queryAsync("INSERT INTO itemTypeFields VALUES (17, 97, NULL, 7)");
   2230 				yield Zotero.DB.queryAsync("INSERT INTO itemTypeFields VALUES (17, 98, NULL, 8)");
   2231 				yield Zotero.DB.queryAsync("INSERT INTO itemTypeFields VALUES (17, 42, NULL, 9)");
   2232 			}
   2233 			
   2234 			else if (i == 83) {
   2235 				// Feeds
   2236 				yield Zotero.DB.queryAsync("DROP TABLE IF EXISTS feeds");
   2237 				yield Zotero.DB.queryAsync("DROP TABLE IF EXISTS feedItems");
   2238 				yield Zotero.DB.queryAsync("CREATE TABLE feeds (\n    libraryID INTEGER PRIMARY KEY,\n    name TEXT NOT NULL,\n    url TEXT NOT NULL UNIQUE,\n    lastUpdate TIMESTAMP,\n    lastCheck TIMESTAMP,\n    lastCheckError TEXT,\n    cleanupAfter INT,\n    refreshInterval INT,\n    FOREIGN KEY (libraryID) REFERENCES libraries(libraryID) ON DELETE CASCADE\n)");
   2239 				yield Zotero.DB.queryAsync("CREATE TABLE feedItems (\n    itemID INTEGER PRIMARY KEY,\n    guid TEXT NOT NULL UNIQUE,\n    readTime TIMESTAMP,\n    translatedTime TIMESTAMP,\n    FOREIGN KEY (itemID) REFERENCES items(itemID) ON DELETE CASCADE\n)");
   2240 			}
   2241 			
   2242 			else if (i == 84) {
   2243 				yield Zotero.DB.queryAsync("CREATE TABLE syncQueue (\n    libraryID INT NOT NULL,\n    key TEXT NOT NULL,\n    syncObjectTypeID INT NOT NULL,\n    lastCheck TIMESTAMP,\n    tries INT,\n    PRIMARY KEY (libraryID, key, syncObjectTypeID),\n    FOREIGN KEY (libraryID) REFERENCES libraries(libraryID) ON DELETE CASCADE,\n    FOREIGN KEY (syncObjectTypeID) REFERENCES syncObjectTypes(syncObjectTypeID) ON DELETE CASCADE\n)");
   2244 			}
   2245 			
   2246 			else if (i == 85) {
   2247 				yield Zotero.DB.queryAsync("DELETE FROM version WHERE schema IN ('sync', 'syncdeletelog')");
   2248 			}
   2249 			
   2250 			else if (i == 86) {
   2251 				let rows = yield Zotero.DB.queryAsync("SELECT ROWID AS id, * FROM itemRelations WHERE SUBSTR(object, 1, 18)='http://zotero.org/' AND NOT INSTR(object, 'item')");
   2252 				for (let i = 0; i < rows.length; i++) {
   2253 					// http://zotero.org/users/local/aFeGasdGSdH/8QZ36WQ3 -> http://zotero.org/users/local/aFeGasdGSdH/items/8QZ36WQ3
   2254 					// http://zotero.org/users/12341/8QZ36WQ3 -> http://zotero.org/users/12341/items/8QZ36WQ3
   2255 					// http://zotero.org/groups/12341/8QZ36WQ3 -> http://zotero.org/groups/12341/items/8QZ36WQ3
   2256 					let newObject = rows[i].object.replace(/^(http:\/\/zotero.org\/(?:(?:users|groups)\/\d+|users\/local\/[^\/]+))\/([A-Z0-9]{8})$/, '$1/items/$2');
   2257 					yield Zotero.DB.queryAsync("UPDATE itemRelations SET object=? WHERE ROWID=?", [newObject, rows[i].id]);
   2258 				}
   2259 			}
   2260 			
   2261 			else if (i == 87) {
   2262 				yield _updateCompatibility(3);
   2263 				let rows = yield Zotero.DB.queryAsync("SELECT valueID, value FROM itemDataValues WHERE TYPEOF(value) = 'integer'");
   2264 				for (let i = 0; i < rows.length; i++) {
   2265 					let row = rows[i];
   2266 					let valueID = yield Zotero.DB.valueQueryAsync("SELECT valueID FROM itemDataValues WHERE value=?", "" + row.value);
   2267 					if (valueID) {
   2268 						yield Zotero.DB.queryAsync("UPDATE itemData SET valueID=? WHERE valueID=?", [valueID, row.valueID]);
   2269 						yield Zotero.DB.queryAsync("DELETE FROM itemDataValues WHERE valueID=?", row.valueID);
   2270 					}
   2271 					else {
   2272 						yield Zotero.DB.queryAsync("UPDATE itemDataValues SET value=? WHERE valueID=?", ["" + row.value, row.valueID]);
   2273 					}
   2274 				}
   2275 			}
   2276 			
   2277 			else if (i == 89) {
   2278 				let groupLibraryMap = {};
   2279 				let libraryGroupMap = {};
   2280 				let resolveLibrary = Zotero.Promise.coroutine(function* (usersOrGroups, id) {
   2281 					if (usersOrGroups == 'users') return 1;
   2282 					if (groupLibraryMap[id] !== undefined) return groupLibraryMap[id];
   2283 					return groupLibraryMap[id] = (yield Zotero.DB.valueQueryAsync("SELECT libraryID FROM groups WHERE groupID=?", id));
   2284 				});
   2285 				let resolveGroup = Zotero.Promise.coroutine(function* (id) {
   2286 					if (libraryGroupMap[id] !== undefined) return libraryGroupMap[id];
   2287 					return libraryGroupMap[id] = (yield Zotero.DB.valueQueryAsync("SELECT groupID FROM groups WHERE libraryID=?", id));
   2288 				});
   2289 				
   2290 				let userSegment = yield Zotero.DB.valueQueryAsync("SELECT IFNULL((SELECT value FROM settings WHERE setting='account' AND key='userID'), 'local/' || (SELECT value FROM settings WHERE setting='account' AND key='localUserKey'))");
   2291 				
   2292 				let predicateID = yield Zotero.DB.valueQueryAsync("SELECT predicateID FROM relationPredicates WHERE predicate='dc:relation'");
   2293 				if (!predicateID) continue;
   2294 				let rows = yield Zotero.DB.queryAsync("SELECT ROWID AS id, * FROM itemRelations WHERE predicateID=?", predicateID);
   2295 				for (let i = 0; i < rows.length; i++) {
   2296 					let row = rows[i];
   2297 					let newSubjectlibraryID, newSubjectKey, newObjectKey;
   2298 					
   2299 					let object = row.object;
   2300 					if (!object.startsWith('http://zotero.org/')) continue;
   2301 					object = object.substr(18);
   2302 					let newObjectURI = 'http://zotero.org/';
   2303 					
   2304 					// Fix missing 'local' from 80
   2305 					let matches = object.match(/^users\/([a-zA-Z0-9]{8})\/items\/([A-Z0-9]{8})$/);
   2306 					// http://zotero.org/users/aFeGasdG/items/8QZ36WQ3 -> http://zotero.org/users/local/aFeGasdG/items/8QZ36WQ3
   2307 					if (matches) {
   2308 						object = `users/local/${matches[1]}/items/${matches[2]}`;
   2309 						let uri = `http://zotero.org/users/local/${matches[1]}/items/${matches[2]}`;
   2310 						yield Zotero.DB.queryAsync("UPDATE itemRelations SET object=? WHERE ROWID=?", [uri, row.id]);
   2311 					}
   2312 					
   2313 					// Add missing bidirectional from 80
   2314 					if (object.startsWith('users')) {
   2315 						matches = object.match(/^users\/(local\/\w+|\d+)\/items\/([A-Z0-9]{8})$/);
   2316 						if (!matches) continue;
   2317 						newSubjectlibraryID = 1;
   2318 						newSubjectKey = matches[2];
   2319 					}
   2320 					else if (object.startsWith('groups')) {
   2321 						matches = object.match(/^groups\/(\d+)\/items\/([A-Z0-9]{8})$/);
   2322 						if (!matches) continue;
   2323 						newSubjectlibraryID = yield resolveLibrary('groups', matches[1]);
   2324 						newSubjectKey = matches[2];
   2325 					}
   2326 					else {
   2327 						continue;
   2328 					}
   2329 					let newSubjectID = yield Zotero.DB.valueQueryAsync("SELECT itemID FROM items WHERE libraryID=? AND key=?", [newSubjectlibraryID, newSubjectKey]);
   2330 					if (!newSubjectID) continue;
   2331 					let { libraryID, key } = yield Zotero.DB.rowQueryAsync("SELECT libraryID, key FROM items WHERE itemID=?", row.itemID);
   2332 					if (libraryID == 1) {
   2333 						newObjectURI += `users/${userSegment}/items/${key}`;
   2334 					}
   2335 					else {
   2336 						let groupID = yield resolveGroup(libraryID);
   2337 						newObjectURI += `groups/${groupID}/items/${key}`;
   2338 					}
   2339 					yield Zotero.DB.queryAsync("INSERT OR IGNORE INTO itemRelations VALUES (?, ?, ?)", [newSubjectID, predicateID, newObjectURI]);
   2340 				}
   2341 			}
   2342 			
   2343 			else if (i == 90) {
   2344 				yield _updateCompatibility(4);
   2345 				yield Zotero.DB.queryAsync("ALTER TABLE feeds RENAME TO feedsOld");
   2346 				yield Zotero.DB.queryAsync("CREATE TABLE feeds (\n    libraryID INTEGER PRIMARY KEY,\n    name TEXT NOT NULL,\n    url TEXT NOT NULL UNIQUE,\n    lastUpdate TIMESTAMP,\n    lastCheck TIMESTAMP,\n    lastCheckError TEXT,\n    cleanupReadAfter INT,\n    cleanupUnreadAfter INT,\n    refreshInterval INT,\n    FOREIGN KEY (libraryID) REFERENCES libraries(libraryID) ON DELETE CASCADE\n)");
   2347 				yield Zotero.DB.queryAsync("INSERT INTO feeds SELECT libraryID, name, url, lastUpdate, lastCheck, lastCheckError, 30, cleanupAfter, refreshInterval FROM feedsOld");
   2348 				yield Zotero.DB.queryAsync("DROP TABLE feedsOld");
   2349 			}
   2350 			
   2351 			else if (i == 91) {
   2352 				yield Zotero.DB.queryAsync("ALTER TABLE libraries ADD COLUMN archived INT NOT NULL DEFAULT 0");
   2353 			}
   2354 			
   2355 			else if (i == 92) {
   2356 				let userID = yield Zotero.DB.valueQueryAsync("SELECT value FROM settings WHERE setting='account' AND key='userID'");
   2357 				if (userID) {
   2358 					yield Zotero.DB.queryAsync("UPDATE itemRelations SET object='http://zotero.org/users/' || ? || SUBSTR(object, 39) WHERE object LIKE ?", [userID, 'http://zotero.org/users/local/%']);
   2359 				}
   2360 			}
   2361 			
   2362 			else if (i == 93) {
   2363 				yield _updateCompatibility(5);
   2364 				yield Zotero.DB.queryAsync("CREATE TABLE publicationsItems (\n    itemID INTEGER PRIMARY KEY\n);");
   2365 				yield Zotero.DB.queryAsync("INSERT INTO publicationsItems SELECT itemID FROM items WHERE libraryID=4");
   2366 				yield Zotero.DB.queryAsync("UPDATE OR IGNORE items SET libraryID=1, synced=0 WHERE libraryID=4");
   2367 				yield Zotero.DB.queryAsync("DELETE FROM itemRelations WHERE object LIKE ? AND object LIKE ?", ['http://zotero.org/users/%', '%/publications/items%']);
   2368 				yield Zotero.DB.queryAsync("DELETE FROM libraries WHERE libraryID=4");
   2369 				
   2370 				let rows = yield Zotero.DB.queryAsync("SELECT itemID, data FROM syncCache JOIN items USING (libraryID, key, version) WHERE syncObjectTypeID=3");
   2371 				let ids = [];
   2372 				for (let row of rows) {
   2373 					let json = JSON.parse(row.data);
   2374 					if (json.data && json.data.inPublications) {
   2375 						ids.push(row.itemID);
   2376 					}
   2377 				}
   2378 				if (ids.length) {
   2379 					yield Zotero.DB.queryAsync("INSERT INTO publicationsItems (itemID) VALUES "
   2380 						+ ids.map(id => `(${id})`).join(', '));
   2381 				}
   2382 			}
   2383 			
   2384 			else if (i == 94) {
   2385 				let ids = yield Zotero.DB.columnQueryAsync("SELECT itemID FROM publicationsItems WHERE itemID IN (SELECT itemID FROM items JOIN itemAttachments USING (itemID) WHERE linkMode=2)");
   2386 				for (let id of ids) {
   2387 					yield Zotero.DB.queryAsync("UPDATE items SET synced=0, clientDateModified=CURRENT_TIMESTAMP WHERE itemID=?", id);
   2388 				}
   2389 				yield Zotero.DB.queryAsync("DELETE FROM publicationsItems WHERE itemID IN (SELECT itemID FROM items JOIN itemAttachments USING (itemID) WHERE linkMode=2)");
   2390 			}
   2391 			
   2392 			else if (i == 95) {
   2393 				yield Zotero.DB.queryAsync("DELETE FROM publicationsItems WHERE itemID NOT IN (SELECT itemID FROM items WHERE libraryID=1)");
   2394 			}
   2395 			
   2396 			else if (i == 96) {
   2397 				yield Zotero.DB.queryAsync("REPLACE INTO fileTypeMIMETypes VALUES(7, 'application/vnd.ms-powerpoint')");
   2398 			}
   2399 			
   2400 			else if (i == 97) {
   2401 				let where = "WHERE predicate IN (" + Array.from(Array(20).keys()).map(i => `'${i}'`).join(', ') + ")";
   2402 				let rows = yield Zotero.DB.queryAsync("SELECT * FROM relationPredicates " + where);
   2403 				for (let row of rows) {
   2404 					yield Zotero.DB.columnQueryAsync("UPDATE items SET synced=0 WHERE itemID IN (SELECT itemID FROM itemRelations WHERE predicateID=?)", row.predicateID);
   2405 					yield Zotero.DB.queryAsync("DELETE FROM itemRelations WHERE predicateID=?", row.predicateID);
   2406 				}
   2407 				yield Zotero.DB.queryAsync("DELETE FROM relationPredicates " + where);
   2408 			}
   2409 			
   2410 			else if (i == 98) {
   2411 				yield Zotero.DB.queryAsync("DELETE FROM itemRelations WHERE predicateID=(SELECT predicateID FROM relationPredicates WHERE predicate='owl:sameAs') AND object LIKE ?", 'http://www.archive.org/%');
   2412 			}
   2413 			
   2414 			else if (i == 99) {
   2415 				yield Zotero.DB.queryAsync("DELETE FROM itemRelations WHERE predicateID=(SELECT predicateID FROM relationPredicates WHERE predicate='dc:isReplacedBy')");
   2416 				yield Zotero.DB.queryAsync("DELETE FROM relationPredicates WHERE predicate='dc:isReplacedBy'");
   2417 			}
   2418 			
   2419 			else if (i == 100) {
   2420 				let userID = yield Zotero.DB.valueQueryAsync("SELECT value FROM settings WHERE setting='account' AND key='userID'");
   2421 				let predicateID = yield Zotero.DB.valueQueryAsync("SELECT predicateID FROM relationPredicates WHERE predicate='dc:relation'");
   2422 				if (userID && predicateID) {
   2423 					let rows = yield Zotero.DB.queryAsync("SELECT itemID, object FROM items JOIN itemRelations IR USING (itemID) WHERE libraryID=? AND predicateID=?", [1, predicateID]);
   2424 					for (let row of rows) {
   2425 						let matches = row.object.match(/^http:\/\/zotero.org\/users\/(\d+)\/items\/([A-Z0-9]+)$/);
   2426 						if (matches) {
   2427 							// Wrong libraryID
   2428 							if (matches[1] != userID) {
   2429 								yield Zotero.DB.queryAsync(`UPDATE OR REPLACE itemRelations SET object='http://zotero.org/users/${userID}/items/${matches[2]}' WHERE itemID=? AND predicateID=?`, [row.itemID, predicateID]);
   2430 							}
   2431 						}
   2432 					}
   2433 				}
   2434 			}
   2435 			
   2436 			else if (i == 101) {
   2437 				Components.utils.import("chrome://zotero/content/import/mendeley/mendeleyImport.js");
   2438 				let importer = new Zotero_Import_Mendeley();
   2439 				if (yield importer.hasImportedFiles()) {
   2440 					yield importer.queueFileCleanup();
   2441 				}
   2442 			}
   2443 			
   2444 			// If breaking compatibility or doing anything dangerous, clear minorUpdateFrom
   2445 		}
   2446 		
   2447 		yield _updateDBVersion('userdata', toVersion);
   2448 		return true;
   2449 	});
   2450 	
   2451 	
   2452 	//
   2453 	// Longer functions for specific upgrade steps
   2454 	//
   2455 	
   2456 	/**
   2457 	 * Convert Mozilla-specific relative descriptors below storage and base directories to UTF-8
   2458 	 * paths using '/' separators
   2459 	 */
   2460 	var _migrateUserData_80_filePaths = Zotero.Promise.coroutine(function* () {
   2461 		var rows = yield Zotero.DB.queryAsync("SELECT itemID, libraryID, key, linkMode, path FROM items JOIN itemAttachments USING (itemID) WHERE path != ''");
   2462 		var tmpDirFile = Zotero.getTempDirectory();
   2463 		var tmpFilePath = OS.Path.normalize(tmpDirFile.path)
   2464 			// Since relative paths can be applied on different platforms,
   2465 			// just use "/" everywhere for oonsistency, and convert on use
   2466 			.replace(/\\/g, '/');
   2467 		
   2468 		for (let i = 0; i < rows.length; i++) {
   2469 			let row = rows[i];
   2470 			let libraryKey = row.libraryID + "/" + row.key;
   2471 			let path = row.path;
   2472 			let prefix = path.match(/^(attachments|storage):/);
   2473 			if (prefix) {
   2474 				prefix = prefix[0];
   2475 				let relPath = path.substr(prefix.length)
   2476 				let file = tmpDirFile.clone();
   2477 				file.setRelativeDescriptor(file, relPath);
   2478 				path = OS.Path.normalize(file.path).replace(/\\/g, '/');
   2479 				
   2480 				// setRelativeDescriptor() silently uses the parent directory on Windows
   2481 				// if the filename contains certain characters, so strip them —
   2482 				// but don't skip characters outside of XML range, since they may be
   2483 				// correct in the opaque relative descriptor string
   2484 				//
   2485 				// This is a bad place for this, since the change doesn't make it
   2486 				// back up to the sync server, but we do it to make sure we don't
   2487 				// accidentally use the parent dir.
   2488 				if (path == tmpFilePath) {
   2489 					file.setRelativeDescriptor(file, Zotero.File.getValidFileName(relPath, true));
   2490 					path = OS.Path.normalize(file.path);
   2491 					if (path == tmpFilePath) {
   2492 						Zotero.logError("Cannot fix relative descriptor for item " + libraryKey + " -- not converting path");
   2493 						continue;
   2494 					}
   2495 					else {
   2496 						Zotero.logError("Filtered relative descriptor for item " + libraryKey);
   2497 					}
   2498 				}
   2499 				
   2500 				if (!path.startsWith(tmpFilePath)) {
   2501 					Zotero.logError(path + " does not start with " + tmpFilePath
   2502 						+ " -- not converting relative path for item " + libraryKey);
   2503 					continue;
   2504 				}
   2505 				path = prefix + path.substr(tmpFilePath.length + 1);
   2506 			}
   2507 			else {
   2508 				let file = Components.classes["@mozilla.org/file/local;1"]
   2509 					.createInstance(Components.interfaces.nsILocalFile);
   2510 				try {
   2511 					file.persistentDescriptor = path;
   2512 				}
   2513 				catch (e) {
   2514 					Zotero.logError("Invalid persistent descriptor for item " + libraryKey + " -- not converting path");
   2515 					continue;
   2516 				}
   2517 				path = file.path;
   2518 			}
   2519 			
   2520 			yield Zotero.DB.queryAsync("UPDATE itemAttachments SET path=? WHERE itemID=?", [path, row.itemID]);
   2521 		}
   2522 	})
   2523 	
   2524 	var _migrateUserData_80_relations = Zotero.Promise.coroutine(function* () {
   2525 		yield Zotero.DB.queryAsync("CREATE TABLE relationPredicates (\n    predicateID INTEGER PRIMARY KEY,\n    predicate TEXT UNIQUE\n)");
   2526 		
   2527 		yield Zotero.DB.queryAsync("CREATE TABLE collectionRelations (\n    collectionID INT NOT NULL,\n    predicateID INT NOT NULL,\n    object TEXT NOT NULL,\n    PRIMARY KEY (collectionID, predicateID, object),\n    FOREIGN KEY (collectionID) REFERENCES collections(collectionID) ON DELETE CASCADE,\n    FOREIGN KEY (predicateID) REFERENCES relationPredicates(predicateID) ON DELETE CASCADE\n)");
   2528 		yield Zotero.DB.queryAsync("CREATE INDEX collectionRelations_predicateID ON collectionRelations(predicateID)");
   2529 		yield Zotero.DB.queryAsync("CREATE INDEX collectionRelations_object ON collectionRelations(object);");
   2530 		yield Zotero.DB.queryAsync("CREATE TABLE itemRelations (\n    itemID INT NOT NULL,\n    predicateID INT NOT NULL,\n    object TEXT NOT NULL,\n    PRIMARY KEY (itemID, predicateID, object),\n    FOREIGN KEY (itemID) REFERENCES items(itemID) ON DELETE CASCADE,\n    FOREIGN KEY (predicateID) REFERENCES relationPredicates(predicateID) ON DELETE CASCADE\n)");
   2531 		yield Zotero.DB.queryAsync("CREATE INDEX itemRelations_predicateID ON itemRelations(predicateID)");
   2532 		yield Zotero.DB.queryAsync("CREATE INDEX itemRelations_object ON itemRelations(object);");
   2533 		
   2534 		yield Zotero.DB.queryAsync("UPDATE relations SET subject=object, predicate='dc:replaces', object=subject WHERE predicate='dc:isReplacedBy'");
   2535 		
   2536 		var start = 0;
   2537 		var limit = 100;
   2538 		var collectionSQL = "INSERT OR IGNORE INTO collectionRelations (collectionID, predicateID, object) VALUES ";
   2539 		var itemSQL = "INSERT OR IGNORE INTO itemRelations (itemID, predicateID, object) VALUES ";
   2540 		//                  1        2                1         2       3                    4
   2541 		var objectRE = /(?:(users)\/(\d+|local\/\w+)|(groups)\/(\d+))\/(collections|items)\/([A-Z0-9]{8})/;
   2542 		//                1        2                1         2               3
   2543 		var itemRE = /(?:(users)\/(\d+|local\/\w+)|(groups)\/(\d+))\/items\/([A-Z0-9]{8})/;
   2544 		var report = "";
   2545 		var groupLibraryIDMap = {};
   2546 		var resolveLibrary = Zotero.Promise.coroutine(function* (usersOrGroups, id) {
   2547 			if (usersOrGroups == 'users') return 1;
   2548 			if (groupLibraryIDMap[id] !== undefined) return groupLibraryIDMap[id];
   2549 			return groupLibraryIDMap[id] = (yield Zotero.DB.valueQueryAsync("SELECT libraryID FROM groups WHERE groupID=?", id));
   2550 		});
   2551 		var predicateMap = {};
   2552 		var resolvePredicate = Zotero.Promise.coroutine(function* (predicate) {
   2553 			if (predicateMap[predicate]) return predicateMap[predicate];
   2554 			yield Zotero.DB.queryAsync("INSERT INTO relationPredicates (predicateID, predicate) VALUES (NULL, ?)", predicate);
   2555 			return predicateMap[predicate] = Zotero.DB.valueQueryAsync("SELECT predicateID FROM relationPredicates WHERE predicate=?", predicate);
   2556 		});
   2557 		while (true) {
   2558 			let rows = yield Zotero.DB.queryAsync("SELECT subject, predicate, object FROM relations LIMIT ?, ?", [start, limit]);
   2559 			if (!rows.length) {
   2560 				break;
   2561 			}
   2562 			
   2563 			let collectionRels = [];
   2564 			let itemRels = [];
   2565 			
   2566 			for (let i = 0; i < rows.length; i++) {
   2567 				let row = rows[i];
   2568 				let concat = row.subject + " - " + row.predicate + " - " + row.object;
   2569 				
   2570 				try {
   2571 					switch (row.predicate) {
   2572 					case 'owl:sameAs':
   2573 						let subjectMatch = row.subject.match(objectRE);
   2574 						let objectMatch = row.object.match(objectRE);
   2575 						if (!subjectMatch && !objectMatch) {
   2576 							Zotero.debug("No match for relation subject or object: " + concat, 2);
   2577 							report += concat + "\n";
   2578 							continue;
   2579 						}
   2580 						// Remove empty captured groups
   2581 						subjectMatch = subjectMatch ? subjectMatch.filter(x => x) : false;
   2582 						objectMatch = objectMatch ? objectMatch.filter(x => x) : false;
   2583 						let subjectLibraryID = false;
   2584 						let subjectType = false;
   2585 						let subject = false;
   2586 						let objectLibraryID = false;
   2587 						let objectType = false;
   2588 						let object = false;
   2589 						if (subjectMatch) {
   2590 							subjectLibraryID = (yield resolveLibrary(subjectMatch[1], subjectMatch[2])) || false;
   2591 							subjectType = subjectMatch[3];
   2592 						}
   2593 						if (objectMatch) {
   2594 							objectLibraryID = (yield resolveLibrary(objectMatch[1], objectMatch[2])) || false;
   2595 							objectType = objectMatch[3];
   2596 						}
   2597 						// Use subject if it's a user library or it isn't but neither is object, and if object can be found
   2598 						if (subjectLibraryID && (subjectLibraryID == 1 || objectLibraryID != 1)) {
   2599 							let key = subjectMatch[4];
   2600 							if (subjectType == 'collection') {
   2601 								let collectionID = yield Zotero.DB.valueQueryAsync("SELECT collectionID FROM collections WHERE libraryID=? AND key=?", [subjectLibraryID, key]);
   2602 								if (collectionID) {
   2603 									collectionRels.push([collectionID, row.predicate, row.object]);
   2604 									continue;
   2605 								}
   2606 							}
   2607 							else {
   2608 								let itemID = yield Zotero.DB.valueQueryAsync("SELECT itemID FROM items WHERE libraryID=? AND key=?", [subjectLibraryID, key]);
   2609 								if (itemID) {
   2610 									itemRels.push([itemID, row.predicate, row.object]);
   2611 									continue;
   2612 								}
   2613 							}
   2614 						}
   2615 						
   2616 						// Otherwise use object if it can be found
   2617 						if (objectLibraryID) {
   2618 							let key = objectMatch[4];
   2619 							if (objectType == 'collection') {
   2620 								let collectionID = yield Zotero.DB.valueQueryAsync("SELECT collectionID FROM collections WHERE libraryID=? AND key=?", [objectLibraryID, key]);
   2621 								if (collectionID) {
   2622 									collectionRels.push([collectionID, row.predicate, row.subject]);
   2623 									continue;
   2624 								}
   2625 							}
   2626 							else {
   2627 								let itemID = yield Zotero.DB.valueQueryAsync("SELECT itemID FROM items WHERE libraryID=? AND key=?", [objectLibraryID, key]);
   2628 								if (itemID) {
   2629 									itemRels.push([itemID, row.predicate, row.subject]);
   2630 									continue;
   2631 								}
   2632 							}
   2633 							Zotero.debug("Neither subject nor object found: " + concat, 2);
   2634 							report += concat + "\n";
   2635 						}
   2636 						break;
   2637 					
   2638 					case 'dc:replaces':
   2639 						let match = row.subject.match(itemRE);
   2640 						if (!match) {
   2641 							Zotero.debug("Unrecognized subject: " + concat, 2);
   2642 							report += concat + "\n";
   2643 							continue;
   2644 						}
   2645 						// Remove empty captured groups
   2646 						match = match.filter(x => x);
   2647 						let libraryID;
   2648 						// Users
   2649 						if (match[1] == 'users') {
   2650 							let itemID = yield Zotero.DB.valueQueryAsync("SELECT itemID FROM items WHERE libraryID=? AND key=?", [1, match[3]]);
   2651 							if (!itemID) {
   2652 								Zotero.debug("Subject not found: " + concat, 2);
   2653 								report += concat + "\n";
   2654 								continue;
   2655 							}
   2656 							itemRels.push([itemID, row.predicate, row.object]);
   2657 						}
   2658 						// Groups
   2659 						else {
   2660 							let itemID = yield Zotero.DB.valueQueryAsync("SELECT itemID FROM items JOIN groups USING (libraryID) WHERE groupID=? AND key=?", [match[2], match[3]]);
   2661 							if (!itemID) {
   2662 								Zotero.debug("Subject not found: " + concat, 2);
   2663 								report += concat + "\n";
   2664 								continue;
   2665 							}
   2666 							itemRels.push([itemID, row.predicate, row.object]);
   2667 						}
   2668 						break;
   2669 					
   2670 					default:
   2671 						Zotero.debug("Unknown predicate '" + row.predicate + "': " + concat, 2);
   2672 						report += concat + "\n";
   2673 						continue;
   2674 					}
   2675 				}
   2676 				catch (e) {
   2677 					Zotero.logError(e);
   2678 				}
   2679 			}
   2680 			
   2681 			if (collectionRels.length) {
   2682 				for (let i = 0; i < collectionRels.length; i++) {
   2683 					collectionRels[i][1] = yield resolvePredicate(collectionRels[i][1]);
   2684 				}
   2685 				yield Zotero.DB.queryAsync(collectionSQL + collectionRels.map(() => "(?, ?, ?)").join(", "), collectionRels.reduce((x, y) => x.concat(y)));
   2686 			}
   2687 			if (itemRels.length) {
   2688 				for (let i = 0; i < itemRels.length; i++) {
   2689 					itemRels[i][1] = yield resolvePredicate(itemRels[i][1]);
   2690 				}
   2691 				yield Zotero.DB.queryAsync(itemSQL + itemRels.map(() => "(?, ?, ?)").join(", "), itemRels.reduce((x, y) => x.concat(y)));
   2692 			}
   2693 			
   2694 			start += limit;
   2695 		}
   2696 		if (report.length) {
   2697 			report = "Removed relations:\n\n" + report;
   2698 			Zotero.debug(report);
   2699 		}
   2700 		yield Zotero.DB.queryAsync("DROP TABLE relations");
   2701 		
   2702 		//
   2703 		// Migrate related items
   2704 		//
   2705 		// If no user id and no local key, create a local key
   2706 		if (!(yield Zotero.DB.valueQueryAsync("SELECT value FROM settings WHERE setting='account' AND key='userID'"))
   2707 				&& !(yield Zotero.DB.valueQueryAsync("SELECT value FROM settings WHERE setting='account' AND key='localUserKey'"))) {
   2708 			yield Zotero.DB.queryAsync("INSERT INTO settings (setting, key, value) VALUES ('account', 'localUserKey', ?)", Zotero.randomString(8));
   2709 		}
   2710 		var predicateID = predicateMap["dc:relation"];
   2711 		if (!predicateID) {
   2712 			yield Zotero.DB.queryAsync("INSERT OR IGNORE INTO relationPredicates VALUES (NULL, 'dc:relation')");
   2713 			predicateID = yield Zotero.DB.valueQueryAsync("SELECT predicateID FROM relationPredicates WHERE predicate=?", 'dc:relation');
   2714 		}
   2715 		yield Zotero.DB.queryAsync("INSERT OR IGNORE INTO itemRelations SELECT ISA.itemID, " + predicateID + ", 'http://zotero.org/' || (CASE WHEN G.libraryID IS NULL THEN 'users/' || IFNULL((SELECT value FROM settings WHERE setting='account' AND key='userID'), 'local/' || (SELECT value FROM settings WHERE setting='account' AND key='localUserKey')) ELSE 'groups/' || G.groupID END) || '/items/' || I.key FROM itemSeeAlso ISA JOIN items I ON (ISA.linkedItemID=I.itemID) LEFT JOIN groups G USING (libraryID)");
   2716 		yield Zotero.DB.queryAsync("DROP TABLE itemSeeAlso");
   2717 	});
   2718 }