Backport Zotero.ID changes (fabc2ba6a) to 4.0

This commit is contained in:
Dan Stillman 2016-04-07 04:43:54 -04:00
parent 7a449e8deb
commit a360494724
2 changed files with 47 additions and 357 deletions

View file

@ -536,7 +536,7 @@ Zotero.Creator.prototype._checkValue = function (field, value) {
break; break;
case 'key': case 'key':
if (!Zotero.ID.isValidKey(value)) { if (!this._isValidKey(value)) {
this._invalidValueError(field, value); this._invalidValueError(field, value);
} }
break; break;
@ -559,3 +559,8 @@ Zotero.Creator.prototype._generateKey = function () {
Zotero.Creator.prototype._invalidValueError = function (field, value) { Zotero.Creator.prototype._invalidValueError = function (field, value) {
throw ("Invalid '" + field + "' value '" + value + "' in Zotero.Creator._invalidValueError()"); throw ("Invalid '" + field + "' value '" + value + "' in Zotero.Creator._invalidValueError()");
} }
Zotero.Creator.prototype._isValidKey = function (value) {
var re = /^[23456789ABCDEFGHIJKLMNPQRSTUVWXYZ]{8}$/
return re.test(value);
}

View file

@ -24,342 +24,43 @@
*/ */
Zotero.ID_Tracker = function () { Zotero.ID_Tracker = function () {
this.get = get; var _initialized = false;
this.getBigInt = getBigInt; var _tables = [
this.skip = skip; 'collections',
this.getTableName = getTableName; 'creators',
'creatorData',
// Number of ids to compare against at a time 'customFields',
this.__defineGetter__('numIDs', function () 10000); 'customItemTypes',
'itemDataValues',
// Number of times to try increasing the maxID if first range fails 'items',
this.__defineGetter__('maxTries', function () 3); 'savedSearches',
'tags'
// Total number of ids to find ];
this.__defineGetter__('maxToFind', function () 1000); var _nextIDs = {};
var _available = {};
var _min = {};
var _skip = {};
/* function _init() {
Zotero.debug("Initializing ids");
for (let table of _tables) {
_nextIDs[table] = _getNext(table);
}
_initialized = true;
}
/**
* Gets an unused primary key id for a DB table * Gets an unused primary key id for a DB table
*/ */
function get(table, notNull) { this.get = function (table) {
table = this.getTableName(table); if (!_initialized) {
_init();
switch (table) {
// Autoincrement tables
//
// Callers need to handle a potential NULL for these unless they
// pass |notNull|
case 'items':
case 'creators':
case 'creatorData':
case 'collections':
case 'savedSearches':
case 'tags':
case 'customItemTypes':
case 'customFields':
var id = _getNextAvailable(table);
if (!id && notNull) {
return _getNext(table);
}
return id;
// Non-autoincrement tables
//
// TODO: use autoincrement instead where available in 1.5
case 'itemDataValues':
var id = _getNextAvailable(table);
if (!id) {
// If we can't find an empty id quickly, just use MAX() + 1
return _getNext(table);
}
return id;
default:
throw ("Unsupported table '" + table + "' in Zotero.ID.get()");
} }
} if (!_nextIDs[table]) {
throw new Error("IDs not loaded for table '" + table + "'");
this.isValidKey = function (value) {
var re = /^[23456789ABCDEFGHIJKLMNPQRSTUVWXYZ]{8}$/
return re.test(value);
}
function getBigInt(max) {
if (!max) {
max = 9007199254740991;
}
return Math.floor(Math.random() * (max)) + 1;
}
/**
* Mark ids as used
*
* @param string table
* @param int|array ids Item ids to skip
*/
function skip(table, ids) {
table = this.getTableName(table);
switch (ids.constructor.name) {
case 'Array':
break;
case 'Number':
ids = [ids];
break;
default:
throw ("ids must be an int or array of ints in Zotero.ID.skip()");
} }
if (!ids.length) { return ++_nextIDs[table];
return; };
}
if (!_skip[table]) {
_skip[table] = {};
}
for (var i=0, len=ids.length; i<len; i++) {
_skip[table][ids[i]] = true;
}
}
function getTableName(table) {
// Used in sync.js
if (table == 'searches') {
table = 'savedSearches';
}
switch (table) {
case 'collections':
case 'creators':
case 'creatorData':
case 'itemDataValues':
case 'items':
case 'savedSearches':
case 'tags':
case 'customItemTypes':
case 'customFields':
return table;
default:
throw ("Invalid table '" + table + "' in Zotero.ID");
}
}
/*
* Returns the lowest available unused primary key id for table,
* or NULL if none could be loaded in _loadAvailable()
*/
function _getNextAvailable(table) {
if (!_available[table]) {
_loadAvailable(table);
}
var arr = _available[table];
while (arr[0]) {
var id = arr[0][0];
// End of range -- remove range
if (id == arr[0][1]) {
arr.shift();
// Prepare table for refresh if all rows used
if (arr.length == 0) {
delete _available[table];
}
}
// Within range -- increment
else {
arr[0][0]++;
}
if (_skip[table] && _skip[table][id]) {
Zotero.debug("Skipping " + table + " id " + id);
if (!_available[table]) {
_loadAvailable(table);
}
continue;
}
_min[table] = id;
return id;
}
return null;
}
/*
* Get MAX(id) + 1 from table
*/
function _getNext(table) {
var column = _getTableColumn(table);
var sql = 'SELECT MAX(';
if (_skip[table]) {
var max = 0;
for (var id in _skip[table]) {
if (parseInt(id) > max) {
max = parseInt(id);
}
}
if (!max) {
throw ("_skip['" + table + "'] must contain positive values in Zotero.ID._getNext()");
}
sql += 'MAX(' + column + ', ' + max + ')';
}
else {
sql += column;
}
sql += ')+1 FROM ' + table;
return Zotero.DB.valueQuery(sql);
}
/*
* Loads available ids for table into memory
*/
function _loadAvailable(table) {
Zotero.debug("Loading available ids for table '" + table + "'");
var minID = _min[table] ? _min[table] + 1 : 1;
var numIDs = Zotero.ID.numIDs;
var maxTries = Zotero.ID.maxTries;
var maxToFind = Zotero.ID.maxToFind;
var column = _getTableColumn(table);
switch (table) {
case 'creators':
case 'creatorData':
case 'items':
case 'itemDataValues':
case 'tags':
break;
case 'collections':
case 'savedSearches':
case 'customItemTypes':
case 'customFields':
var maxToFind = 100;
break;
default:
throw ("Unsupported table '" + table + "' in Zotero.ID._loadAvailable()");
}
var maxID = minID + numIDs - 1;
var sql = "SELECT " + column + " FROM " + table
+ " WHERE " + column + " BETWEEN ? AND ? ORDER BY " + column;
var ids = Zotero.DB.columnQuery(sql, [minID, maxID]);
// If no ids found, we have numIDs unused ids
if (!ids) {
maxID = Math.min(maxID, minID + (maxToFind - 1));
Zotero.debug("Found " + (maxID - minID + 1) + " available ids in table '" + table + "'");
_available[table] = [[minID, maxID]];
return;
}
// If we didn't find any unused ids, try increasing maxID a few times
while (ids.length == numIDs && maxTries>0) {
Zotero.debug('No available ids found between ' + minID + ' and ' + maxID + '; trying next ' + numIDs);
minID = maxID + 1;
maxID = minID + numIDs - 1;
ids = Zotero.DB.columnQuery(sql, [minID, maxID]);
maxTries--;
}
// Didn't find any unused ids -- _getNextAvailable() will return NULL for
// this table for rest of session
if (ids.length == numIDs) {
Zotero.debug("Found no available ids in table '" + table + "'");
_available[table] = [];
return;
}
var available = [], found = 0, j = 0, availableStart = null;
for (var i=minID; i<=maxID && found<maxToFind; i++) {
// We've gone past the found ids, so all remaining ids up to maxID
// are available
if (!ids[j]) {
available.push([i, maxID]);
found += (maxID - i) + 1;
break;
}
// Skip ahead while ids are occupied
if (ids[j] == i) {
j++;
continue;
}
// Advance counter while it's below the next used id
while (ids[j] > i && i<=maxID) {
if (!availableStart) {
availableStart = i;
}
i++;
if ((found + (i - availableStart) + 1) > maxToFind) {
break;
}
}
if (availableStart) {
available.push([availableStart, i-1]);
// Keep track of how many empties we've found
found += ((i-1) - availableStart) + 1;
availableStart = null;
}
j++;
}
Zotero.debug("Found " + found + " available ids in table '" + table + "'");
_available[table] = available;
}
/**
* Find a unique random id for use in a DB table
*
* (No longer used)
**/
function _getRandomID(table, max){
var column = _getTableColumn(table);
var sql = 'SELECT COUNT(*) FROM ' + table + ' WHERE ' + column + '= ?';
if (!max){
max = 16383;
}
max--; // since we use ceil(), decrement max by 1
var tries = 3; // # of tries to find a unique id
for (var i=0; i<tries; i++) {
var rnd = Math.ceil(Math.random() * max);
var exists = Zotero.DB.valueQuery(sql, { int: rnd });
if (!exists) {
return rnd;
}
}
// If no luck after number of tries, try a larger range
var sql = 'SELECT MAX(' + column + ') + 1 FROM ' + table;
return Zotero.valueQuery(sql);
}
function _getTableColumn(table) { function _getTableColumn(table) {
@ -377,33 +78,17 @@ Zotero.ID_Tracker = function () {
return table.substr(0, table.length - 1) + 'ID'; return table.substr(0, table.length - 1) + 'ID';
} }
} }
/**
* Get MAX(id) + 1 from table
*
* @return {Promise<Integer>}
*/
function _getNext(table) {
var sql = 'SELECT COALESCE(MAX(' + _getTableColumn(table) + ') + 1, 1) FROM ' + table;
return Zotero.DB.valueQuery(sql);
};
} }
Zotero.ID = new Zotero.ID_Tracker; Zotero.ID = new Zotero.ID_Tracker;
/**
* Notifier observer to mark saved object ids as used
*/
Zotero.ID.EventListener = new function () {
this.init = init;
this.notify = notify;
function init() {
Zotero.Notifier.registerObserver(this);
}
function notify(event, type, ids) {
if (event == 'add') {
try {
var table = Zotero.ID.getTableName(type);
}
// Skip if not a table we handle
catch (e) {
return;
}
Zotero.ID.skip(table, ids);
}
}
}