From 2e2fa0dcfa10b9526e0289e4cc74a7f081482023 Mon Sep 17 00:00:00 2001 From: Dan Stillman Date: Fri, 16 Mar 2007 16:28:50 +0000 Subject: [PATCH] *As always, but in particular this time, do not test this commit with valuable data -- but please do test.* - Massive optimization of data layer -- with ~11,000-item test library on a Mac Pro, decreased initial Zotero pane loading from several minutes to ~10 seconds. This included some small API changes and new methods (e.g. Items.cacheFiles()) in the data layer, but most of it was changing the way loading and caching of data worked internally. - Moved unique itemData values out to separate itemDataValues table for better normalization - Updated itemTreeView.sort() to be able to sort a single row into the items list for performance reasons -- itemTreeView.notify() now only sorts a single row when possible (and sometimes doesn't need to sort anything). This should make general interface use dramatically less sluggish with large libraries. - Consolidated purging on item deletes, which should speed up multi-item deletes quite a bit -- clients should use Items.erase() instead of Item.erase(), since the former calls the new Items.purge() method (which calls the various other purge() methods) automatically - Notifier no longer throws errors in notify() callbacks and instead just logs them to the Error Console -- this way a misbehaving utility (or Zotero itself) won't keep other observers from receiving change notifications - Better handling of database corruption -- if an SQL query throws a file corruption error, Zotero adds a marker file to the storage directory and displays a message prompting the user to restart to attempt auto-repair--and, most importantly, no longer copies the corrupt file over the last backup. - A "Loading items list..." message appears over the items list (at least, sometimes) while data is loading -- useful for large libraries, but may need to be fine-tuned to not be annoying for smaller ones. - Note titles are now cached in itemNoteTitles table - orderIndex values are no longer consolidated when removing items from collections -- it just leaves gaps - Fixed shameful bug in getRandomID() that could result in an item with itemID 0, which wouldn't display correctly and would be impossible to remove - Fixed autocomplete and search for new location of 'title' field - Added proper multipart date support for type-specific 'date' fields - Made the pre-modification array passed to Notifier observers on item updates actually be pre-modification - New method Zotero.ItemFields.isFieldOfBase(field, baseField) -- for example, isFieldOfBase('label', 'publisher') returns true, as does isFieldOfBase('publisher', 'publisher') - Restored ability to drag child items in collections into top-level items in those collections - Disabled unresponsive script message when opening Zotero pane (necessary for large libraries, or at least was before the optimizations) - Collections in background windows didn't update on item changes - Modifying an item would cause it to appear incorrectly in other collections in background windows - Fixed an error when dragging, hovering to open, and dropping a note or attachment on another item - Removed deprecated Notifier methods registerCollectionObserver(), registerItemObserver(), unregisterCollectionObserver(), and unregisterItemObserver() - Loading of Zotero core object can be cancelled on error with Zotero.skipLoading - Removed old disabled DebugLogger code - New method Zotero.log(message, type, sourceName, sourceLine, lineNumber, columnNumber, category) to log to Error Console -- wrapper for nsIConsoleService.logMessage(nsIScriptError) - New method Zotero.getErrors(), currently unused, to return array of error strings that have occurred since startup, excluding CSS and content JS errors -- will enable an upcoming Talkback-like feature - Fixed some JS strict warnings in Zotero.Date.strToMultipart() --- chrome/content/zotero/itemPane.js | 27 +- chrome/content/zotero/overlay.js | 2 + chrome/content/zotero/overlay.xul | 2 +- chrome/content/zotero/xpcom/data_access.js | 770 +++++++++++++------ chrome/content/zotero/xpcom/db.js | 58 +- chrome/content/zotero/xpcom/itemTreeView.js | 270 +++++-- chrome/content/zotero/xpcom/notifier.js | 41 +- chrome/content/zotero/xpcom/schema.js | 80 +- chrome/content/zotero/xpcom/search.js | 54 +- chrome/content/zotero/xpcom/zotero.js | 93 ++- chrome/locale/en-US/zotero/zotero.properties | 3 + chrome/skin/default/zotero/overlay.css | 5 + components/zotero-autocomplete.js | 15 +- userdata.sql | 19 +- 14 files changed, 977 insertions(+), 462 deletions(-) diff --git a/chrome/content/zotero/itemPane.js b/chrome/content/zotero/itemPane.js index 5c934bf89..f4159db39 100644 --- a/chrome/content/zotero/itemPane.js +++ b/chrome/content/zotero/itemPane.js @@ -288,15 +288,15 @@ var ZoteroItemPane = new function() for(var i = 0; i0 ? _tabIndexMinFields + i : 1) : 0; _tabIndexMaxInfoFields = Math.max(_tabIndexMaxInfoFields, tabindex); - if (fieldNames[i]=='date'){ - addDateRow(_itemBeingEdited.getField('date', true), tabindex); + if (editable && Zotero.ItemFields.isFieldOfBase(fieldID, 'date')) { + addDateRow(fieldNames[i], _itemBeingEdited.getField(fieldNames[i], true), tabindex); continue; } @@ -695,15 +695,15 @@ var ZoteroItemPane = new function() /** * Add a date row with a label editor and a ymd indicator to show date parsing */ - function addDateRow(value, tabindex) + function addDateRow(field, value, tabindex) { var label = document.createElement("label"); - label.setAttribute("value", Zotero.getString("itemFields.date") + ':'); - label.setAttribute("fieldname",'date'); + label.setAttribute("value", Zotero.getString("itemFields." + field) + ':'); + label.setAttribute("fieldname", field); label.setAttribute("onclick", "this.nextSibling.firstChild.blur()"); var hbox = document.createElement("hbox"); - var elem = createValueElement(Zotero.Date.multipartToStr(value), 'date', tabindex); + var elem = createValueElement(Zotero.Date.multipartToStr(value), field, tabindex); // y-m-d status indicator var datebox = document.createElement('hbox'); @@ -897,12 +897,6 @@ var ZoteroItemPane = new function() _tabIndexMaxTagsFields = Math.max(_tabIndexMaxTagsFields, tabindex); break; - // Display the SQL date as a tooltip for the date field - case 'date': - valueElement.setAttribute('tooltiptext', - Zotero.Date.multipartToSQL(_itemBeingEdited.getField('date', true))); - break; - // Convert dates from UTC case 'dateAdded': case 'dateModified': @@ -919,6 +913,13 @@ var ZoteroItemPane = new function() break; } + // Display the SQL date as a tooltip for date fields + var fieldID = Zotero.ItemFields.getID(fieldName); + if (fieldID && Zotero.ItemFields.isFieldOfBase(fieldID, 'date')) { + valueElement.setAttribute('tooltiptext', + Zotero.Date.multipartToSQL(_itemBeingEdited.getField(fieldName, true))); + } + if (fieldName.indexOf('firstName')!=-1){ valueElement.setAttribute('flex', '1'); } diff --git a/chrome/content/zotero/overlay.js b/chrome/content/zotero/overlay.js index ea43cfca6..d7b6c7441 100644 --- a/chrome/content/zotero/overlay.js +++ b/chrome/content/zotero/overlay.js @@ -569,10 +569,12 @@ var ZoteroPane = new function() itemgroup.setSearch(''); itemgroup.setTags(getTagSelection()); + Zotero.UnresponsiveScriptIndicator.disable(); this.itemsView = new Zotero.ItemTreeView(itemgroup); this.itemsView.addCallback(_setTagScope); document.getElementById('zotero-items-tree').view = this.itemsView; this.itemsView.selection.clearSelection(); + Zotero.UnresponsiveScriptIndicator.enable(); } else { diff --git a/chrome/content/zotero/overlay.xul b/chrome/content/zotero/overlay.xul index b78348fcd..5f4a88fa9 100644 --- a/chrome/content/zotero/overlay.xul +++ b/chrome/content/zotero/overlay.xul @@ -261,7 +261,7 @@ - + diff --git a/chrome/content/zotero/xpcom/data_access.js b/chrome/content/zotero/xpcom/data_access.js index 2e12ebd20..f1906179b 100644 --- a/chrome/content/zotero/xpcom/data_access.js +++ b/chrome/content/zotero/xpcom/data_access.js @@ -26,20 +26,31 @@ * Generally should be called through Zotero.Items rather than directly */ Zotero.Item = function(itemTypeOrID){ - this._init(); + this._init(itemTypeOrID); - if (itemTypeOrID){ - this.setType(Zotero.ItemTypes.getID(itemTypeOrID)); + if (itemTypeOrID) { + this._changed.set('itemTypeID'); } } -Zotero.Item.prototype._init = function(){ +Zotero.Item.prototype._init = function(itemTypeOrID, create) { // - // Public members for access by public methods -- do not access directly + // These members are public so that they can be accessed by public methods + // -- do not access directly // - this._data = []; + this._data = {}; + this.isPrimaryField('itemID'); // make sure primary field hash array exists + for (var field in Zotero.Item.primaryFields) { + this._data[field] = null; + } + this._creators = []; - this._itemData = []; + this._itemData = null; + + if (itemTypeOrID) { + // setType initializes type-specific properties in this._itemData + this.setType(Zotero.ItemTypes.getID(itemTypeOrID), true); + } this._creatorsLoaded = false; this._itemDataLoaded = false; @@ -48,6 +59,8 @@ Zotero.Item.prototype._init = function(){ this._changedCreators = new Zotero.Hash(); this._changedItemData = new Zotero.Hash(); + this._preChangeArray = null; + this._noteTitle = null; this._noteText = null; this._noteAccessTime = null; @@ -70,7 +83,6 @@ Zotero.Item.prototype.isPrimaryField = function(field){ if (!Zotero.Item.primaryFields){ Zotero.Item.primaryFields = Zotero.DB.getColumnHash('items'); Zotero.Item.primaryFields['firstCreator'] = true; - Zotero.Item.primaryFields['numChildren'] = true; Zotero.Item.primaryFields['numNotes'] = true; Zotero.Item.primaryFields['numAttachments'] = true; } @@ -81,39 +93,80 @@ Zotero.Item.prototype.isPrimaryField = function(field){ /* * Build object from database */ -Zotero.Item.prototype.loadFromID = function(id){ - // Should be the same as query in Zotero.Items.loadFromID, just - // without itemID clause - var sql = 'SELECT I.*, lastName || ' - + 'CASE ((SELECT COUNT(*) FROM itemCreators WHERE itemID=' + id + ')>1) ' - + "WHEN 0 THEN '' ELSE ' et al.' END AS firstCreator, " - + "(SELECT COUNT(*) FROM itemNotes WHERE sourceItemID=I.itemID) AS numNotes, " - + "(SELECT COUNT(*) FROM itemAttachments WHERE sourceItemID=I.itemID) AS numAttachments " - + 'FROM items I ' - + 'LEFT JOIN itemCreators IC ON (I.itemID=IC.itemID) ' - + 'LEFT JOIN creators C ON (IC.creatorID=C.creatorID) ' - + 'WHERE itemID=' + id - + ' AND (IC.orderIndex=0 OR IC.orderIndex IS NULL)'; // first creator +Zotero.Item.prototype.loadFromID = function(id) { + var columns = [], join = [], where = []; + for (var field in Zotero.Item.primaryFields) { + var colSQL = null, joinSQL = null, whereSQL = null; + // If field not already set + if (this._data[field] === null) { + // Parts should be the same as query in Zotero.Items._load, just + // without itemID clause + switch (field) { + case 'itemTypeID': + case 'dateAdded': + case 'dateModified': + colSQL = 'I.' + field; + break; + + case 'firstCreator': + colSQL = 'CASE ((SELECT COUNT(*) FROM itemCreators ' + + 'WHERE itemID=' + id + ')>1) ' + + "WHEN 0 THEN '' ELSE ' et al.' END AS firstCreator"; + joinSQL = 'LEFT JOIN itemCreators IC ON (I.itemID=IC.itemID) ' + + 'LEFT JOIN creators C ON (IC.creatorID=C.creatorID)'; + whereSQL = '(IC.orderIndex=0 OR IC.orderIndex IS NULL)'; + break; + + case 'numNotes': + colSQL = '(SELECT COUNT(*) FROM itemNotes ' + + 'WHERE sourceItemID=I.itemID) AS numNotes'; + break; + + case 'numAttachments': + colSQL = '(SELECT COUNT(*) FROM itemAttachments ' + + 'WHERE sourceItemID=I.itemID) AS numAttachments'; + break; + } + if (colSQL) { + columns.push(colSQL); + } + if (joinSQL) { + join.push(joinSQL); + } + if (whereSQL) { + where.push(whereSQL); + } + } + } + + var sql = 'SELECT I.itemID' + (columns.length ? ', ' + columns.join(', ') : '') + + " FROM items I " + (join.length ? join.join(' ') + ' ' : '') + + "WHERE I.itemID=" + id + (where.length ? ' AND ' + where.join(' AND ') : ''); var row = Zotero.DB.rowQuery(sql); this.loadFromRow(row); + this._itemDataLoaded = true; } /* * Populate basic item data from a database row */ -Zotero.Item.prototype.loadFromRow = function(row){ - this._init(); +Zotero.Item.prototype.loadFromRow = function(row, reload) { + // If necessary or reloading, set the type and initialize this._itemData + if (reload || (!this.getType() && row['itemTypeID'])) { + this.setType(row['itemTypeID'], true); + } + for (var col in row){ // Only accept primary field data through loadFromRow() if (this.isPrimaryField(col)){ - this._data[col] = row[col]; + //Zotero.debug('Setting field ' + col + ' for item ' + this.getID()); + this._data[col] = row[col] ? row[col] : false; } else { Zotero.debug(col + ' is not a valid primary field'); } } - return true; } @@ -139,17 +192,17 @@ Zotero.Item.prototype.getType = function(){ /* * Set or change the item's type */ -Zotero.Item.prototype.setType = function(itemTypeID){ +Zotero.Item.prototype.setType = function(itemTypeID, loadIn) { if (itemTypeID==this.getType()){ return true; } // If there's an existing type if (this.getType()){ + var copiedFields = []; + var obsoleteFields = this.getFieldsNotInType(itemTypeID); if (obsoleteFields) { - var copiedFields = []; - for each(var oldFieldID in obsoleteFields) { // Try to get a base type for this field var baseFieldID = @@ -170,6 +223,13 @@ Zotero.Item.prototype.setType = function(itemTypeID){ } } + for (var fieldID in this._itemData) { + if (this._itemData[fieldID] && + (!obsoleteFields || obsoleteFields.indexOf(fieldID) == -1)) { + copiedFields.push([fieldID, this.getField(fieldID)]); + } + } + // And reset custom creator types to the default var creators = this.getCreators(); if (creators){ @@ -185,7 +245,13 @@ Zotero.Item.prototype.setType = function(itemTypeID){ } this._data['itemTypeID'] = itemTypeID; - this._changed.set('itemTypeID'); + + // Initialize this._itemData with type-specific fields + this._itemData = {}; + var fields = Zotero.ItemFields.getItemTypeFields(itemTypeID); + for each(var fieldID in fields) { + this._itemData[fieldID] = null; + } if (copiedFields) { for each(var f in copiedFields) { @@ -193,6 +259,10 @@ Zotero.Item.prototype.setType = function(itemTypeID){ } } + if (!loadIn) { + this._changed.set('itemTypeID'); + } + return true; } @@ -418,20 +488,21 @@ Zotero.Item.prototype.creatorExists = function(firstName, lastName, creatorTypeI Zotero.Item.prototype.getField = function(field, unformatted, includeBaseMapped) { //Zotero.debug('Requesting field ' + field + ' for item ' + this.getID(), 4); if (this.isPrimaryField(field)){ + if (this.getID() && this._data[field] === null) { + this.loadFromID(this.getID()); + } + //Zotero.debug('Returning ' + (this._data[field] ? this._data[field] : '')); return this._data[field] ? this._data[field] : ''; } - if (Zotero.ItemFields.getName(field) == 'title' && this.isNote()) { - if (this._noteTitle !== null) { - return this._noteTitle; + if (this.isNote()) { + switch (Zotero.ItemFields.getName(field)) { + case 'title': + return this.getNoteTitle(); + + default: + return ''; } - var title = this._noteToTitle(); - this._noteTitle = title; - return title; - } - - if (this.getID() && !this._itemDataLoaded){ - this._loadItemData(); } if (includeBaseMapped) { @@ -444,14 +515,24 @@ Zotero.Item.prototype.getField = function(field, unformatted, includeBaseMapped) var fieldID = Zotero.ItemFields.getID(field); } + if (typeof this._itemData[fieldID] == 'undefined') { + //Zotero.debug('Returning blank for ' + field + ' in ' + this.getType()); + return ''; + } + + if (this.getID() && this._itemData[fieldID] === null && !this._itemDataLoaded) { + this._loadItemData(); + } + var value = this._itemData[fieldID] ? this._itemData[fieldID] : ''; if (!unformatted){ - if (fieldID==Zotero.ItemFields.getID('date')){ + // Multipart date fields + if (Zotero.ItemFields.isFieldOfBase(fieldID, 'date')) { value = Zotero.Date.multipartToStr(value); } } - + //Zotero.debug('Returning ' + value); return value; } @@ -471,14 +552,13 @@ Zotero.Item.prototype.setField = function(field, value, loadIn){ throw ('Primary field ' + field + ' cannot be changed through setField'); } - // Type-specific field if (!this.getType()){ throw ('Item type must be set before setting field data.'); } // If existing item, load field data first unless we're already in // the middle of a load - if (this.getID() && !loadIn && !this._itemDataLoaded){ + if (this.getID() && !loadIn && !this._itemDataLoaded) { this._loadItemData(); } @@ -488,18 +568,23 @@ Zotero.Item.prototype.setField = function(field, value, loadIn){ throw (field + ' is not a valid itemData field.'); } - if (!Zotero.ItemFields.isValidForType(fieldID, this.getType())){ - throw (field + ' is not a valid field for this type.'); + if (loadIn && this.isNote() && field == 110) { // title + this._noteTitle = value; + return true; + } + + if (!Zotero.ItemFields.isValidForType(fieldID, this.getType())){ + throw ('"' + field + "' is not a valid field for this type."); } - // Save date field as multipart date if (!loadIn){ - if (fieldID==Zotero.ItemFields.getID('date') && - !Zotero.Date.isMultipart(value)){ + // Save date field as multipart date + if (Zotero.ItemFields.isFieldOfBase(fieldID, 'date') && + !Zotero.Date.isMultipart(value)) { value = Zotero.Date.strToMultipart(value); } - - if (fieldID == Zotero.ItemFields.getID('accessDate')) { + // Validate access date + else if (fieldID == Zotero.ItemFields.getID('accessDate')) { if (!Zotero.Date.isSQLDate(value) && !Zotero.Date.isSQLDateTime(value) && value != 'CURRENT_TIMESTAMP') { @@ -508,21 +593,22 @@ Zotero.Item.prototype.setField = function(field, value, loadIn){ return false; } } + + // If existing value, make sure it's actually changing + if ((!this._itemData[fieldID] && !value) || + (this._itemData[fieldID] && this._itemData[fieldID]==value)) { + return false; + } + + // Save a copy of the object before modifying + if (!this._preChangeArray) { + this._preChangeArray = this.toArray(); + } } - // If existing value, make sure it's actually changing - if ((!this._itemData[fieldID] && !value) || - (this._itemData[fieldID] && this._itemData[fieldID]==value)){ - return false; - } this._itemData[fieldID] = value; - if (loadIn) { - // Not ideal to do this here, but there's not a great way to set this - // private variable from Z.Items._load() - this._itemDataLoaded = true; - } - else { + if (!loadIn) { this._changedItemData.set(fieldID); } return true; @@ -532,8 +618,6 @@ Zotero.Item.prototype.setField = function(field, value, loadIn){ /* * Save changes back to database * - * Note: Does not call notify() if transaction is in progress - * * Returns true on item update or itemID of new item */ Zotero.Item.prototype.save = function(){ @@ -561,7 +645,6 @@ Zotero.Item.prototype.save = function(){ Zotero.debug('Updating database with new item data', 4); var itemID = this.getID(); - var preItemArray = this.toArray(); try { Zotero.DB.beginTransaction(); @@ -645,15 +728,6 @@ Zotero.Item.prototype.save = function(){ 'itemID-creatorID-creatorTypeID', [this.getID(), creatorID, creator['creatorTypeID']]); } - - // Delete obsolete creators - var deleted; - if (deleted = Zotero.Creators.purge()){ - for (var i in deleted){ - // Add purged creators to history - Zotero.History.remove('creators', 'creatorID', i); - } - } } @@ -663,94 +737,75 @@ Zotero.Item.prototype.save = function(){ if (this._changedItemData.length){ var del = new Array(); - sql = "SELECT COUNT(*) FROM itemData WHERE itemID=? AND fieldID=?"; - var countStatement = Zotero.DB.getStatement(sql); + sql = "SELECT valueID FROM itemDataValues WHERE value=?"; + var valueStatement = Zotero.DB.getStatement(sql); - sql = "UPDATE itemData SET value=? WHERE itemID=? AND fieldID=?"; - var updateStatement = Zotero.DB.getStatement(sql); - - sql = "INSERT INTO itemData VALUES (?,?,?)"; + sql = "INSERT INTO itemDataValues VALUES (?,?)"; var insertStatement = Zotero.DB.getStatement(sql); + sql = "REPLACE INTO itemData VALUES (?,?,?)"; + var replaceStatement = Zotero.DB.getStatement(sql); + for (fieldID in this._changedItemData.items){ - if (this.getField(fieldID)){ - // Oh, for an INSERT...ON DUPLICATE KEY UPDATE - countStatement.bindInt32Parameter(0, this.getID()); - countStatement.bindInt32Parameter(1, fieldID); - countStatement.executeStep(); - var exists = countStatement.getInt64(0); - countStatement.reset(); - - // Update - if (exists){ - updateStatement.bindInt32Parameter(1, this.getID()); - + var value = this.getField(fieldID, true); + if (value) { + // Field exists + if (this._preChangeArray[Zotero.ItemFields.getName(fieldID)]) { Zotero.History.modify('itemData', 'itemID-fieldID', [this.getID(), fieldID]); + } + // Field is new + else { + Zotero.History.add('itemData', 'itemID-fieldID', + [this.getID(), fieldID]); + } + + valueStatement.bindUTF8StringParameter(0, value); + if (valueStatement.executeStep()) { + var valueID = valueStatement.getInt32(0); + } + else { + var valueID = null; + } + valueStatement.reset(); + + if (!valueID) { + valueID = Zotero.getRandomID('itemDataValues', 'valueID', 2097152); // stored in 3 bytes + insertStatement.bindInt32Parameter(0, valueID); - // Don't bind CURRENT_TIMESTAMP as string - if (Zotero.ItemFields.getID('accessDate')==fieldID - && this.getField(fieldID)=='CURRENT_TIMESTAMP') - { - sql = "UPDATE itemData SET value=CURRENT_TIMESTAMP" - + " WHERE itemID=? AND fieldID=?"; - Zotero.DB.query(sql, - [{int:this.getID()}, {int:fieldID}]); + if (Zotero.ItemFields.getID('accessDate') == fieldID + && this.getField(fieldID) == 'CURRENT_TIMESTAMP') { + sql = "INSERT INTO itemDataValues VALUES " + + "(?,CURRENT_TIMESTAMP)"; + Zotero.DB.query(sql, {int: valueID}); } else { - // Take advantage of SQLite's manifest typing - if (Zotero.ItemFields.isInteger(fieldID)){ - updateStatement.bindInt32Parameter(0, - this.getField(fieldID, true)); + if (Zotero.ItemFields.isInteger(fieldID)) { + insertStatement. + bindInt32Parameter(1, value); } else { - updateStatement.bindUTF8StringParameter(0, - this.getField(fieldID, true)); + insertStatement. + bindUTF8StringParameter(1, value); } - updateStatement.bindInt32Parameter(2, fieldID); try { - updateStatement.execute(); + insertStatement.execute(); } - catch(e){ - throw(Zotero.DB.getLastErrorString()); + catch (e) { + throw (Zotero.DB.getLastErrorString()); } } } - // Insert - else { - Zotero.History.add('itemData', 'itemID-fieldID', - [this.getID(), fieldID]); + replaceStatement.bindInt32Parameter(0, this.getID()); + replaceStatement.bindInt32Parameter(1, fieldID); + replaceStatement.bindInt32Parameter(2, valueID); - insertStatement.bindInt32Parameter(0, this.getID()); - insertStatement.bindInt32Parameter(1, fieldID); - - if (Zotero.ItemFields.getID('accessDate')==fieldID - && this.getField(fieldID)=='CURRENT_TIMESTAMP') - { - sql = "INSERT INTO itemData VALUES " - + "(?,?,CURRENT_TIMESTAMP)"; - - Zotero.DB.query(sql, - [{int:this.getID()}, {int:fieldID}]); - } - else { - if (Zotero.ItemFields.isInteger(fieldID)){ - insertStatement.bindInt32Parameter(2, - this.getField(fieldID, true)); - } - else { - insertStatement.bindUTF8StringParameter(2, - this.getField(fieldID, true)); - } - - try { - insertStatement.execute(); - } - catch(e){ - throw(Zotero.DB.getLastErrorString()); - } - } + try { + replaceStatement.execute(); + } + catch (e) { + throw (Zotero.DB.getLastErrorString()); } } @@ -760,9 +815,8 @@ Zotero.Item.prototype.save = function(){ } } - countStatement.reset(); - updateStatement.reset(); insertStatement.reset(); + replaceStatement.reset(); // Delete blank fields if (del.length){ @@ -839,43 +893,76 @@ Zotero.Item.prototype.save = function(){ // if (this._changedItemData.length){ // Use manual bound parameters to speed things up - var statement = - Zotero.DB.getStatement("INSERT INTO itemData VALUES (?,?,?)"); + sql = "SELECT valueID FROM itemDataValues WHERE value=?"; + var valueStatement = Zotero.DB.getStatement(sql); + sql = "INSERT INTO itemDataValues VALUES (?,?)"; + var insertValueStatement = Zotero.DB.getStatement(sql); + + sql = "INSERT INTO itemData VALUES (?,?,?)"; + var insertStatement = Zotero.DB.getStatement(sql); + + Zotero.debug(this._changedItemData.items); for (fieldID in this._changedItemData.items){ - if (!this.getField(fieldID, true)){ + var value = this.getField(fieldID, true); + if (!value) { continue; } - statement.bindInt32Parameter(0, this.getID()); - statement.bindInt32Parameter(1, fieldID); - - if (Zotero.ItemFields.getID('accessDate')==fieldID - && this.getField(fieldID)=='CURRENT_TIMESTAMP') - { - sql = "INSERT INTO itemData VALUES (?,?,CURRENT_TIMESTAMP)"; - Zotero.DB.query(sql, [{int:this.getID()}, {int:fieldID}]) + valueStatement.bindUTF8StringParameter(0, value); + if (valueStatement.executeStep()) { + var valueID = valueStatement.getInt32(0); } else { - if (Zotero.ItemFields.isInteger(fieldID)){ - statement.bindInt32Parameter(2, this.getField(fieldID, true)); + var valueID = null; + } + valueStatement.reset(); + + if (!valueID) { + valueID = Zotero.getRandomID('itemDataValues', 'valueID', 2097152); // stored in 3 bytes + insertValueStatement.bindInt32Parameter(0, valueID); + + if (Zotero.ItemFields.getID('accessDate') == fieldID + && this.getField(fieldID) == 'CURRENT_TIMESTAMP') { + sql = "INSERT INTO itemDataValues VALUES " + + "(?,CURRENT_TIMESTAMP)"; + Zotero.DB.query(sql, {int: valueID}); } else { - statement.bindUTF8StringParameter(2, this.getField(fieldID, true)); - } - try { - statement.execute(); - } - catch(e){ - throw(Zotero.DB.getLastErrorString()); + if (Zotero.ItemFields.isInteger(fieldID)) { + insertValueStatement. + bindInt32Parameter(1, value); + } + else { + insertValueStatement. + bindUTF8StringParameter(1, value); + } + try { + insertValueStatement.execute(); + } + catch (e) { + throw (Zotero.DB.getLastErrorString()); + } } } + insertStatement.bindInt32Parameter(0, this.getID()); + insertStatement.bindInt32Parameter(1, fieldID); + insertStatement.bindInt32Parameter(2, valueID); + + try { + insertStatement.execute(); + } + catch(e) { + throw(Zotero.DB.getLastErrorString()); + } + Zotero.History.add('itemData', 'itemID-fieldID', [itemID, fieldID]); } - statement.reset(); + insertValueStatement.reset(); + insertStatement.reset(); } // @@ -940,7 +1027,7 @@ Zotero.Item.prototype.save = function(){ return this.getID(); } else { - Zotero.Notifier.trigger('modify', 'item', this.getID(), { old: preItemArray }); + Zotero.Notifier.trigger('modify', 'item', this.getID(), { old: this._preChangeArray }); return true; } } @@ -1008,14 +1095,18 @@ Zotero.Item.prototype.updateNote = function(text){ var preItemArray = this.toArray(); + var title = Zotero.Notes.noteToTitle(text); + if (this.isNote()){ var sourceItemID = this.getSource(); + + Zotero.DB.query("REPLACE INTO itemNoteTitles VALUES (?,?)", + [this.getID(), {string: title}]); } if (sourceItemID) { - var sql = "REPLACE INTO itemNotes (note, sourceItemID, itemID) " - + "VALUES (?,?,?)"; + var sql = "REPLACE INTO itemNotes VALUES (?,?,?)"; var bindParams = [{string:text}, sourceItemID, this.getID()]; } else @@ -1028,7 +1119,9 @@ Zotero.Item.prototype.updateNote = function(text){ if (updated){ this.updateDateModified(); Zotero.DB.commitTransaction(); - this.updateNoteCache(text); + + this._noteText = text ? text : ''; + this._noteTitle = title ? title : ''; Zotero.Notifier.trigger('modify', 'item', this.getID(), { old: preItemArray }); } @@ -1038,12 +1131,12 @@ Zotero.Item.prototype.updateNote = function(text){ } -Zotero.Item.prototype.updateNoteCache = function(text){ - // Update cached values +/* + * Update the cached value of the note + */ +Zotero.Item.prototype.updateNoteCache = function(text, title) { this._noteText = text ? text : ''; - if (this.isNote()){ - this._noteTitle = this._noteToTitle(); - } + this._noteTitle = title ? title : ''; } @@ -1168,6 +1261,27 @@ Zotero.Item.prototype.numNotes = function(){ } +/** +* Get the first line of the note for display in the items list +**/ +Zotero.Item.prototype.getNoteTitle = function(){ + if (!this.isNote() && !this.isAttachment()){ + throw ("getNoteTitle() can only be called on notes and attachments"); + } + + if (this._noteTitle !== null){ + return this._noteTitle; + } + + var sql = "SELECT title FROM itemNoteTitles WHERE itemID=" + this.getID(); + var title = Zotero.DB.valueQuery(sql); + + this._noteTitle = title ? title : ''; + + return title ? title : ''; +} + + /** * Get the text of an item note **/ @@ -1176,9 +1290,14 @@ Zotero.Item.prototype.getNote = function(){ throw ("getNote() can only be called on notes and attachments"); } + if (!this.getID()) { + return ''; + } + + // Store access time for later garbage collection + this._noteAccessTime = new Date(); + if (this._noteText !== null){ - // Store access time for later garbage collection - this._noteAccessTime = new Date(); return this._noteText; } @@ -1195,6 +1314,10 @@ Zotero.Item.prototype.getNote = function(){ * Get the itemID of the source item for a note or file **/ Zotero.Item.prototype.getSource = function(){ + if (!this.getID()) { + return false; + } + if (this.isNote()){ var Type = 'Note'; } @@ -1523,6 +1646,7 @@ Zotero.Item.prototype.getBestSnapshot = function(){ var sql = "SELECT IA.itemID FROM itemAttachments IA NATURAL JOIN items I " + "LEFT JOIN itemData ID ON (IA.itemID=ID.itemID AND fieldID=1) " + + "NATURAL JOIN ItemDataValues " + "WHERE sourceItemID=? AND linkMode=? AND value=? " + "ORDER BY dateAdded DESC LIMIT 1"; @@ -1648,6 +1772,9 @@ Zotero.Item.prototype.hasTags = function(tagIDs) { } Zotero.Item.prototype.getTags = function(){ + if (!this.getID()) { + return false; + } var sql = "SELECT tagID AS id, tag, tagType AS type FROM tags WHERE tagID IN " + "(SELECT tagID FROM itemTags WHERE itemID=" + this.getID() + ") " + "ORDER BY tag COLLATE NOCASE"; @@ -1822,6 +1949,9 @@ Zotero.Item.prototype.removeAllRelated = function() { } Zotero.Item.prototype.getSeeAlso = function(){ + if (!this.getID()) { + return false; + } // Check both ways, using a UNION to take advantage of indexes var sql ="SELECT linkedItemID FROM itemSeeAlso WHERE itemID=?1 UNION " + "SELECT itemID FROM itemSeeAlso WHERE linkedItemID=?1"; @@ -1915,6 +2045,8 @@ Zotero.Item.prototype.clone = function() { /** * Delete item from database and clear from Zotero.Items internal array +* +* Items.erase() should be used instead of this **/ Zotero.Item.prototype.erase = function(deleteChildren){ if (!this.getID()){ @@ -2039,11 +2171,11 @@ Zotero.Item.prototype.erase = function(deleteChildren){ if (this.isAttachment()) { Zotero.Fulltext.clearItemWords(this.getID()); //Zotero.Fulltext.clearItemContent(this.getID()); - Zotero.Fulltext.purgeUnusedWords(); } sql = 'DELETE FROM itemCreators WHERE itemID=' + this.getID() + ";\n"; sql += 'DELETE FROM itemNotes WHERE itemID=' + this.getID() + ";\n"; + sql += 'DELETE FROM itemNoteTitles WHERE itemID=' + this.getID() + ";\n"; sql += 'DELETE FROM itemAttachments WHERE itemID=' + this.getID() + ";\n"; sql += 'DELETE FROM itemSeeAlso WHERE itemID=' + this.getID() + ";\n"; sql += 'DELETE FROM itemSeeAlso WHERE linkedItemID=' + this.getID() + ";\n"; @@ -2052,8 +2184,6 @@ Zotero.Item.prototype.erase = function(deleteChildren){ sql += 'DELETE FROM items WHERE itemID=' + this.getID() + ";\n"; Zotero.DB.query(sql); - Zotero.Creators.purge(); - Zotero.Tags.purge(); try { Zotero.DB.commitTransaction(); @@ -2236,50 +2366,48 @@ Zotero.Item.prototype._loadItemData = function(){ throw ('ItemID not set for object before attempting to load data'); } - var sql = 'SELECT fieldID, value FROM itemData WHERE itemID=?'; + var sql = "SELECT fieldID, value FROM itemData NATURAL JOIN itemDataValues " + + "WHERE itemID=?"; var fields = Zotero.DB.query(sql, this.getID()); + var itemTypeFields = Zotero.ItemFields.getItemTypeFields(this.getType()); + for each(var field in fields) { this.setField(field['fieldID'], field['value'], true); } + // Mark nonexistent fields as loaded + for each(var fieldID in itemTypeFields) { + if (this._itemData[fieldID] === null) { + this._itemData[fieldID] = false; + } + } + this._itemDataLoaded = true; } -/** -* Return first line (or first MAX_LENGTH characters) of note content -**/ -Zotero.Item.prototype._noteToTitle = function(){ - var MAX_LENGTH = 80; - - var t = this.getNote().substring(0, MAX_LENGTH); - var ln = t.indexOf("\n"); - if (ln>-1 && ln1) ' + "WHEN 0 THEN '' ELSE ' et al.' END AS firstCreator, " + "(SELECT COUNT(*) FROM itemNotes WHERE sourceItemID=I.itemID) AS numNotes, " @@ -2480,25 +2716,20 @@ Zotero.Items = new function(){ // Item doesn't exist -- create new object and stuff in array if (!_items[row['itemID']]){ var item = new Zotero.Item(); - item.loadFromRow(row); + item.loadFromRow(row, true); _items[row['itemID']] = item; } // Existing item -- reload in place else { - _items[row['itemID']].loadFromRow(row); + _items[row['itemID']].loadFromRow(row, true); } } - var sql = "SELECT * FROM itemData"; - if (arguments[0]) { - sql += " WHERE itemID IN (" + Zotero.join(arguments, ',') + ")"; + if (!arguments[0]) { + _itemsLoaded = true; + _cachedFields = ['itemID', 'itemTypeID', 'dateModified', + 'firstCreator', 'numNotes', 'numAttachments', 'numChildren']; } - var itemDataRows = Zotero.DB.query(sql); - for each(var row in itemDataRows) { - _items[row['itemID']].setField(row['fieldID'], row['value'], true); - } - - return true; } } @@ -2507,6 +2738,7 @@ Zotero.Items = new function(){ Zotero.Notes = new function(){ this.add = add; + this.noteToTitle = noteToTitle; /** * Create a new item of type 'note' and add the note text to the itemNotes table @@ -2531,6 +2763,10 @@ Zotero.Notes = new function(){ var note = new Zotero.Item('note'); note.save(); + var title = text ? this.noteToTitle(text) : ''; + var sql = "INSERT INTO itemNoteTitles VALUES (?,?)"; + Zotero.DB.query(sql, [note.getID(), title]); + var sql = "INSERT INTO itemNotes VALUES (?,?,?)"; var bindParams = [ note.getID(), @@ -2542,7 +2778,7 @@ Zotero.Notes = new function(){ // Switch to Zotero.Items version var note = Zotero.Items.get(note.getID()); - note.updateNoteCache(text); + note.updateNoteCache(text, title); if (sourceItemID){ var notifierData = { old: sourceItem.toArray() }; @@ -2550,10 +2786,24 @@ Zotero.Notes = new function(){ Zotero.Notifier.trigger('modify', 'item', sourceItemID, notifierData); } - Zotero.Notifier.trigger('add', 'item', note.getID()); - return note.getID(); } + + + /** + * Return first line (or first MAX_LENGTH characters) of note content + **/ + function noteToTitle(text) { + var MAX_LENGTH = 80; + + var t = text.substring(0, MAX_LENGTH); + var ln = t.indexOf("\n"); + if (ln>-1 && ln-1 && ln<80) { t = t.substring(0, ln); } return t; } + for each(var note in notes) { + Zotero.DB.query("INSERT INTO itemNoteTitles VALUES (?,?)", [note['itemID'], f(note['note'])]); + } + + Zotero.DB.query("CREATE TABLE IF NOT EXISTS itemDataValues (\n valueID INT,\n value,\n PRIMARY KEY (valueID)\n);"); + var values = Zotero.DB.columnQuery("SELECT DISTINCT value FROM itemData"); + for each(var value in values) { + var valueID = Zotero.getRandomID('itemDataValues', 'valueID', 2097152); // Stored in 3 bytes + Zotero.DB.query("INSERT INTO itemDataValues VALUES (?,?)", [valueID, value]); + } + + Zotero.DB.query("CREATE TEMPORARY TABLE itemDataTemp AS SELECT itemID, fieldID, (SELECT valueID FROM itemDataValues WHERE value=ID.value) AS valueID FROM itemData ID"); + Zotero.DB.query("DROP TABLE itemData"); + Zotero.DB.query("CREATE TABLE itemData (\n itemID INT,\n fieldID INT,\n valueID INT,\n PRIMARY KEY (itemID, fieldID),\n FOREIGN KEY (itemID) REFERENCES items(itemID),\n FOREIGN KEY (fieldID) REFERENCES fields(fieldID)\n FOREIGN KEY (valueID) REFERENCES itemDataValues(valueID)\n);"); + Zotero.DB.query("INSERT INTO itemData SELECT * FROM itemDataTemp"); + Zotero.DB.query("DROP TABLE itemDataTemp"); + } + + if (i==24) { + var rows = Zotero.DB.query("SELECT * FROM itemData NATURAL JOIN itemDataValues WHERE fieldID IN (52,96,100)"); + for each(var row in rows) { + if (!Zotero.Date.isMultipart(row['value'])) { + var value = Zotero.Date.strToMultipart(row['value']); + var valueID = Zotero.DB.valueQuery("SELECT valueID FROM itemDataValues WHERE value=?", value); + if (!valueID) { + var valueID = Zotero.getRandomID('itemDataValues', 'valueID', 2097152); + Zotero.DB.query("INSERT INTO itemDataValues VALUES (?,?)", [valueID, value]); + } + Zotero.DB.query("UPDATE itemData SET valueID=? WHERE itemID=? AND fieldID=?", [valueID, row['itemID'], row['fieldID']]); + } + } + } } _updateSchema('userdata'); @@ -824,14 +884,14 @@ Zotero.Schema = new function(){ try { Zotero.DB.query("DROP TRIGGER insert_date_field"); } catch (e) {} try { Zotero.DB.query("DROP TRIGGER update_date_field"); } catch (e) {} - var itemDataTrigger = " FOR EACH ROW WHEN NEW.fieldID IN (14, 27)\n" + var itemDataTrigger = " FOR EACH ROW WHEN NEW.fieldID IN (14, 27, 52, 96, 100)\n" + " BEGIN\n" + " SELECT CASE\n" - + " CAST(SUBSTR(NEW.value, 1, 4) AS INT) BETWEEN 0 AND 9999 AND\n" - + " SUBSTR(NEW.value, 5, 1) = '-' AND\n" - + " CAST(SUBSTR(NEW.value, 6, 2) AS INT) BETWEEN 0 AND 12 AND\n" - + " SUBSTR(NEW.value, 8, 1) = '-' AND\n" - + " CAST(SUBSTR(NEW.value, 9, 2) AS INT) BETWEEN 0 AND 31\n" + + " CAST(SUBSTR((SELECT value FROM itemDataValues WHERE valueID=NEW.valueID), 1, 4) AS INT) BETWEEN 0 AND 9999 AND\n" + + " SUBSTR((SELECT value FROM itemDataValues WHERE valueID=NEW.valueID), 5, 1) = '-' AND\n" + + " CAST(SUBSTR((SELECT value FROM itemDataValues WHERE valueID=NEW.valueID), 6, 2) AS INT) BETWEEN 0 AND 12 AND\n" + + " SUBSTR((SELECT value FROM itemDataValues WHERE valueID=NEW.valueID), 8, 1) = '-' AND\n" + + " CAST(SUBSTR((SELECT value FROM itemDataValues WHERE valueID=NEW.valueID), 9, 2) AS INT) BETWEEN 0 AND 31\n" + " WHEN 0 THEN RAISE (ABORT, 'Date field must begin with SQL date') END;\n" + " END;\n"; diff --git a/chrome/content/zotero/xpcom/search.js b/chrome/content/zotero/xpcom/search.js index 015514218..6b1c6b561 100644 --- a/chrome/content/zotero/xpcom/search.js +++ b/chrome/content/zotero/xpcom/search.js @@ -192,9 +192,7 @@ Zotero.Search.prototype.addCondition = function(condition, operator, value, requ for each(var part in parts) { this.addCondition('blockStart'); - this.addCondition('title', operator, part.text, false); this.addCondition('field', operator, part.text, false); - this.addCondition('numberfield', operator, part.text, false); this.addCondition('creator', operator, part.text, false); this.addCondition('tag', operator, part.text, false); this.addCondition('note', operator, part.text, false); @@ -578,28 +576,30 @@ Zotero.Search.prototype._buildQuery = function(){ case 'field': case 'datefield': case 'numberfield': - if (!condition['alias']){ - break; + if (condition['alias']) { + // Add base field + condSQLParams.push( + Zotero.ItemFields.getID(condition['alias']) + ); + var typeFields = Zotero.ItemFields.getTypeFieldsFromBase(condition['alias']); + if (typeFields) { + condSQL += 'fieldID IN (?,'; + // Add type-specific fields + for each(var fieldID in typeFields) { + condSQL += '?,'; + condSQLParams.push(fieldID); + } + condSQL = condSQL.substr(0, condSQL.length - 1); + condSQL += ') AND '; + } + else { + condSQL += 'fieldID=? AND '; + } } - // Add base field - condSQLParams.push( - Zotero.ItemFields.getID(condition['alias']) - ); - var typeFields = Zotero.ItemFields.getTypeFieldsFromBase(condition['alias']); - if (typeFields) { - condSQL += 'fieldID IN (?,'; - // Add type-specific fields - for each(var fieldID in typeFields) { - condSQL += '?,'; - condSQLParams.push(fieldID); - } - condSQL = condSQL.substr(0, condSQL.length - 1); - condSQL += ') AND '; - } - else { - condSQL += 'fieldID=? AND '; - } + condSQL += "valueID IN (SELECT valueID FROM " + + "itemDataValues WHERE "; + openParens++; break; case 'collectionID': @@ -1104,16 +1104,6 @@ Zotero.SearchConditions = new function(){ field: 'collectionID' }, - { - name: 'title', - operators: { - contains: true, - doesNotContain: true - }, - table: 'items', - field: 'title' - }, - { name: 'dateAdded', operators: { diff --git a/chrome/content/zotero/xpcom/zotero.js b/chrome/content/zotero/xpcom/zotero.js index ea9f57c9c..6155a07b3 100644 --- a/chrome/content/zotero/xpcom/zotero.js +++ b/chrome/content/zotero/xpcom/zotero.js @@ -41,6 +41,8 @@ var Zotero = new function(){ this.getZoteroDatabase = getZoteroDatabase; this.chooseZoteroDirectory = chooseZoteroDirectory; this.debug = debug; + this.log = log; + this.getErrors = getErrors; this.varDump = varDump; this.safeDebug = safeDebug; this.getString = getString; @@ -58,6 +60,7 @@ var Zotero = new function(){ // Public properties this.initialized = false; + this.skipLoading = false; this.__defineGetter__("startupError", function() { return _startupError; }); this.__defineGetter__("startupErrorHandler", function() { return _startupErrorHandler; }); this.version; @@ -79,7 +82,7 @@ var Zotero = new function(){ * Initialize the extension */ function init(){ - if (this.initialized){ + if (this.initialized || this.skipLoading) { return false; } @@ -361,28 +364,68 @@ var Zotero = new function(){ return false; } - // Note: DebugLogger extension is old and no longer supported -- if there's - // a better extension we could switch to that as an option - if (false && ZOTERO_CONFIG['DEBUG_TO_CONSOLE']){ - try { - var logManager = - Components.classes["@mozmonkey.com/debuglogger/manager;1"] - .getService(Components.interfaces.nsIDebugLoggerManager); - var logger = logManager.registerLogger("Zotero"); - } - catch (e){} - } - - if (logger){ - logger.log(level, message); - } - else { - dump('zotero(' + level + '): ' + message + "\n\n"); - } + dump('zotero(' + level + '): ' + message + "\n\n"); return true; } + /* + * Log a message to the Mozilla JS error console + * + * |type| is a string with one of the flag types in nsIScriptError: + * 'error', 'warning', 'exception', 'strict' + */ + function log(message, type, sourceName, sourceLine, lineNumber, + columnNumber, category) { + var consoleService = Components.classes["@mozilla.org/consoleservice;1"] + .getService(Components.interfaces.nsIConsoleService); + var scriptError = Components.classes["@mozilla.org/scripterror;1"] + .createInstance(Components.interfaces.nsIScriptError); + + if (!type) { + type = 'warning'; + } + var flags = scriptError[type + 'Flag']; + + scriptError.init( + message, + sourceName ? sourceName : null, + sourceLine != undefined ? sourceLine : null, + lineNumber != undefined ? lineNumber : null, + columnNumber != undefined ? columnNumber : null, + flags, + category + ); + consoleService.logMessage(scriptError); + } + + + function getErrors() { + var errors = []; + var cs = Components.classes["@mozilla.org/consoleservice;1"]. + getService(Components.interfaces.nsIConsoleService); + var messages = {}; + cs.getMessageArray(messages, {}) + + var skip = ['CSS Parser', 'content javascript']; + + for each(var msg in messages) { + Zotero.debug(msg); + try { + msg.QueryInterface(Components.interfaces.nsIScriptError); + if (skip.indexOf(msg.category) != -1) { + continue; + } + errors.push(msg.errorMessage); + } + catch(e) { + errors.push(msg.message); + } + } + return errors; + } + + /** * PHP var_dump equivalent for JS * @@ -622,18 +665,18 @@ var Zotero = new function(){ max = 16383; } + max--; // since we use ceil(), decrement max by 1 var tries = 3; // # of tries to find a unique id do { // If no luck after number of tries, try a larger range if (!tries){ max = max * 128; } - var rnd = Math.floor(Math.random()*max); + var rnd = Math.ceil(Math.random()*max); var exists = Zotero.DB.valueQuery(sql + rnd); tries--; } while (exists); - return rnd; } @@ -1226,14 +1269,13 @@ Zotero.Date = new function(){ var utils = new Zotero.Utilities(); var parts = strToDate(str); - parts.month = typeof parts.month != undefined ? parts.month + 1 : ''; + parts.month = typeof parts.month != "undefined" ? parts.month + 1 : ''; - var multi = utils.lpad(parts.year, '0', 4) + '-' + var multi = (parts.year ? utils.lpad(parts.year, '0', 4) : '0000') + '-' + utils.lpad(parts.month, '0', 2) + '-' - + utils.lpad(parts.day, '0', 2) + + (parts.day ? utils.lpad(parts.day, '0', 2) : '00') + ' ' + str; - return multi; } @@ -1455,7 +1497,6 @@ Zotero.WebProgressFinishListener = function(onFinish) { //Zotero.debug('onFinish() called before STATE_STOP in WebProgressFinishListener.onStateChange()'); return; } - onFinish(); } } diff --git a/chrome/locale/en-US/zotero/zotero.properties b/chrome/locale/en-US/zotero/zotero.properties index 816d4ff3d..513017df7 100644 --- a/chrome/locale/en-US/zotero/zotero.properties +++ b/chrome/locale/en-US/zotero/zotero.properties @@ -40,6 +40,7 @@ pane.tagSelector.numSelected.none = 0 tags selected pane.tagSelector.numSelected.singular = %S tag selected pane.tagSelector.numSelected.plural = %S tags selected +pane.items.loading = Loading items list... pane.items.delete = Are you sure you want to delete the selected item? pane.items.delete.multiple = Are you sure you want to delete the selected items? pane.items.delete.title = Delete @@ -274,6 +275,8 @@ ingester.scrapeError = Could Not Save Item ingester.scrapeErrorDescription = An error occurred while saving this item. Check %S for more information. ingester.scrapeErrorDescription.linkText = Known Translator Issues +db.dbCorrupted = The Zotero database '%S' appears to have become corrupted. +db.dbCorrupted.restart = Please restart Firefox to attempt an automatic restore from the last backup. db.dbCorruptedNoBackup = The Zotero database '%S' appears to have become corrupted, and no automatic backup is available.\n\nA new database file has been created. The damaged file was saved in your Zotero directory. db.dbRestored = The Zotero database '%1$S' appears to have become corrupted.\n\nYour data was restored from the last automatic backup made on %2$S at %3$S. The damaged file was saved in your Zotero directory. db.dbRestoreFailed = The Zotero database '%S' appears to have become corrupted, and an attempt to restore from the last automatic backup failed.\n\nA new database file has been created. The damaged file was saved in your Zotero directory. diff --git a/chrome/skin/default/zotero/overlay.css b/chrome/skin/default/zotero/overlay.css index 3cfa7bcff..409918bf1 100644 --- a/chrome/skin/default/zotero/overlay.css +++ b/chrome/skin/default/zotero/overlay.css @@ -248,6 +248,11 @@ background-image: none; } +#zotero-items-pane-message-box +{ + -moz-appearance: listbox; +} + #zotero-annotate-tb-add { diff --git a/components/zotero-autocomplete.js b/components/zotero-autocomplete.js index e7163dbe5..bac2dd74b 100644 --- a/components/zotero-autocomplete.js +++ b/components/zotero-autocomplete.js @@ -208,12 +208,6 @@ ZoteroAutoComplete.prototype.startSearch = function(searchString, searchParam, } break; - case 'title': - var sql = "SELECT DISTINCT " + searchParam + " FROM items " - + "WHERE " + searchParam + " LIKE ? ORDER BY " + searchParam; - var results = this._zotero.DB.columnQuery(sql, searchString + '%'); - break; - case 'dateModified': case 'dateAdded': var sql = "SELECT DISTINCT DATE(" + searchParam + ", 'localtime') FROM items " @@ -245,14 +239,15 @@ ZoteroAutoComplete.prototype.startSearch = function(searchString, searchParam, // use the user part of the multipart field var valueField = searchParam=='date' ? 'SUBSTR(value, 12, 100)' : 'value'; - var sql = "SELECT DISTINCT " + valueField; - sql += " FROM itemData WHERE fieldID=?1 AND " + valueField; - sql += " LIKE ?2 " + var sql = "SELECT DISTINCT " + valueField + + " FROM itemData NATURAL JOIN itemDataValues " + + "WHERE fieldID=?1 AND " + valueField + + " LIKE ?2 " var sqlParams = [fieldID, searchString + '%']; if (extra){ sql += "AND value NOT IN (SELECT value FROM itemData " - + "WHERE fieldID=?1 AND itemID=?3) "; + + "NATURAL JOIN itemDataValues WHERE fieldID=?1 AND itemID=?3) "; sqlParams.push(extra); } sql += "ORDER BY value"; diff --git a/userdata.sql b/userdata.sql index 8636e3261..0182aa60d 100644 --- a/userdata.sql +++ b/userdata.sql @@ -1,4 +1,4 @@ --- 21 +-- 24 -- This file creates tables containing user-specific data -- any changes -- to existing tables made here must be mirrored in transition steps in @@ -67,12 +67,18 @@ CREATE TABLE IF NOT EXISTS items ( CREATE TABLE IF NOT EXISTS itemData ( itemID INT, fieldID INT, - value, + valueID, PRIMARY KEY (itemID, fieldID), FOREIGN KEY (itemID) REFERENCES items(itemID), FOREIGN KEY (fieldID) REFERENCES fields(fieldID) + FOREIGN KEY (valueID) REFERENCES itemDataValues(valueID) +); + +CREATE TABLE IF NOT EXISTS itemDataValues ( + valueID INT, + value, + PRIMARY KEY (itemID) ); -CREATE INDEX IF NOT EXISTS value ON itemData(value); -- Note data for note items CREATE TABLE IF NOT EXISTS itemNotes ( @@ -85,6 +91,13 @@ CREATE TABLE IF NOT EXISTS itemNotes ( ); CREATE INDEX IF NOT EXISTS itemNotes_sourceItemID ON itemNotes(sourceItemID); +CREATE TABLE IF NOT EXISTS itemNoteTitles ( + itemID INT, + title TEXT, + PRIMARY KEY (itemID), + FOREIGN KEY (itemID) REFERENCES itemNotes(itemID) +); + -- Metadata for attachment items CREATE TABLE IF NOT EXISTS itemAttachments ( itemID INT,