Make Original Date a date field

Update the global schema to 45, resolve a field's date type through its
base-field mapping in ItemFields.isDate() (to cover priorityDate), and
convert stored values of date-type fields to multipart dates on schema
upgrade.
This commit is contained in:
Dan Stillman 2026-08-19 18:35:44 -04:00
parent 04796dff22
commit 73f4273e2d
6 changed files with 139 additions and 11 deletions

View file

@ -1999,8 +1999,7 @@
let tooltipText;
if (fieldID && !this._extraItems.length) {
// Display the SQL date as a tooltip for date fields
// TEMP - filingDate
if (Zotero.ItemFields.isFieldOfBase(fieldID, 'date') || fieldName == 'filingDate') {
if (Zotero.ItemFields.isDate(fieldID)) {
tooltipText = Zotero.Date.multipartToSQL(this.item.getField(fieldName, true));
}
}
@ -2549,9 +2548,9 @@
break;
default:
// TODO: generalize to all date rows/fields
if (Zotero.ItemFields.isFieldOfBase(fieldName, 'date')) {
// Parse 'yesterday'/'today'/'tomorrow'
// Parse 'yesterday'/'today'/'tomorrow' for date fields -- not applied to Original Date
if (Zotero.ItemFields.isDate(fieldName)
&& !Zotero.ItemFields.isFieldOfBase(fieldName, 'originalDate')) {
value = Zotero.Date.parseDescriptiveString(value);
}
}

View file

@ -190,11 +190,16 @@ Zotero.ItemFields = new function () {
this.isDate = function (field) {
var fieldID = this.getID(field);
var fieldName = this.getName(field);
if (Zotero.ItemFields.isFieldOfBase(fieldID, 'date')) {
return true;
}
if (Zotero.Schema.globalSchemaMeta.fields[fieldName]) {
return Zotero.Schema.globalSchemaMeta.fields[fieldName].type == 'date'
return Zotero.Schema.globalSchemaMeta.fields[fieldName].type == 'date';
}
// A type-specific field has the type of the base field it maps to
for (let baseFieldID in _typeFieldIDsByBase) {
if (_typeFieldIDsByBase[baseFieldID].includes(fieldID)) {
let baseName = this.getName(parseInt(baseFieldID));
let baseMeta = Zotero.Schema.globalSchemaMeta.fields[baseName];
return !!baseMeta && baseMeta.type == 'date';
}
}
return false;
};

View file

@ -587,7 +587,40 @@ Zotero.Schema = new function () {
+ baseFieldMappingsValueSets.join(", "));
await Zotero.DB.queryAsync("INSERT INTO itemTypeCreatorTypes VALUES "
+ itemTypeCreatorTypesValueSets.join(", "));
// Convert stored values of date-type fields to multipart dates, for
// values saved while a field was a text field
var dateFieldIDs = new Set();
for (let [fieldName, meta] of Object.entries(data.meta?.fields || {})) {
if (meta.type != 'date') {
continue;
}
let fieldID = postFieldIDsByName.get(fieldName);
if (!fieldID) {
continue;
}
dateFieldIDs.add(fieldID);
// Type-specific fields mapped to a date-type base field
let mappedIDs = await Zotero.DB.columnQueryAsync(
"SELECT DISTINCT fieldID FROM baseFieldMappings WHERE baseFieldID=?",
fieldID
);
for (let id of mappedIDs) {
dateFieldIDs.add(id);
}
}
if (dateFieldIDs.size) {
// strToMultipart() needs the month strings, which aren't loaded
// yet this early in startup
Zotero.Date.init();
await _updateItemDataValues(
[...dateFieldIDs],
value => (Zotero.Date.isMultipart(value)
? false
: Zotero.Date.strToMultipart(value))
);
}
// Store data in DB as compressed binary string. This lets us use a schema that matches the
// DB tables even if the user downgrades to a version with an earlier bundled schema file.
var pako = require('pako');
@ -630,6 +663,51 @@ Zotero.Schema = new function () {
await Zotero.DB.queryAsync("PRAGMA foreign_keys=ON");
}
};
/**
* Rewrite stored itemData values through a transform, reusing or creating
* rows in itemDataValues
*
* @param {Number[]} fieldIDs - Fields whose values to rewrite
* @param {Function} transform - Given a value, returns the new value, or
* false to leave the value as it is
*/
async function _updateItemDataValues(fieldIDs, transform) {
if (!fieldIDs.length) {
return;
}
var rows = await Zotero.DB.queryAsync(
"SELECT itemID, fieldID, value FROM itemData "
+ "JOIN itemDataValues USING (valueID) "
+ "WHERE fieldID IN (" + fieldIDs.join(", ") + ")"
);
for (let row of rows) {
let value = transform(row.value);
if (value === false || value === row.value) {
continue;
}
let valueID = await Zotero.DB.valueQueryAsync(
"SELECT valueID FROM itemDataValues WHERE value=?", value
);
if (!valueID) {
valueID = Zotero.ID.get('itemDataValues');
await Zotero.DB.queryAsync(
"INSERT INTO itemDataValues (valueID, value, valueNormalized) "
+ "VALUES (?, ?, ?)",
[
valueID,
value,
Zotero.Utilities.Internal.normalizeForSearchStorage(value)
]
);
}
await Zotero.DB.queryAsync(
"UPDATE itemData SET valueID=? WHERE itemID=? AND fieldID=?",
[valueID, row.itemID, row.fieldID]
);
}
}

@ -1 +1 @@
Subproject commit 70c3aa98627413d6a30dca955886eafbde085ce9
Subproject commit b86c79b56479cadac3288e1f122cf34ef04e8809

View file

@ -45,6 +45,16 @@ describe("Zotero.ItemFields", function () {
});
});
describe("#isDate()", function () {
it("should treat base and mapped date-type fields as dates", function () {
assert.isTrue(Zotero.ItemFields.isDate('date'));
assert.isTrue(Zotero.ItemFields.isDate('dateDecided'));
assert.isTrue(Zotero.ItemFields.isDate('originalDate'));
assert.isTrue(Zotero.ItemFields.isDate('priorityDate'));
assert.isFalse(Zotero.ItemFields.isDate('title'));
});
});
describe("#getDirection()", function () {
it("should follow app locale for primary field", function () {
assert.equal(Zotero.ItemFields.getDirection('book', 'dateAdded', ''), Zotero.dir)

View file

@ -94,6 +94,42 @@ describe("Zotero.Schema", function () {
});
});
describe("date-type field conversion", function () {
it("should convert stored text values of date-type fields to multipart dates", async function () {
var item = await createDataObject('item', { itemType: 'patent' });
// A value stored while the field was a text field
var fieldID = Zotero.ItemFields.getID('priorityDate');
var valueID = Zotero.ID.get('itemDataValues');
await Zotero.DB.queryAsync(
"INSERT INTO itemDataValues (valueID, value, valueNormalized) VALUES (?, ?, ?)",
[valueID, '1969-04-20', null]
);
await Zotero.DB.queryAsync(
"INSERT INTO itemData VALUES (?, ?, ?)",
[item.id, fieldID, valueID]
);
schema.version++;
await Zotero.Schema._updateGlobalSchemaForTest(schema);
await item.reload(['itemData'], true);
var stored = await Zotero.DB.valueQueryAsync(
"SELECT value FROM itemData JOIN itemDataValues USING (valueID) "
+ "WHERE itemID=? AND fieldID=?",
[item.id, fieldID]
);
assert.isTrue(Zotero.Date.isMultipart(stored));
assert.equal(item.getField('priorityDate'), '1969-04-20');
// The converted value matches a date search on the base field
var s = new Zotero.Search();
s.libraryID = item.libraryID;
s.addCondition('originalDate', 'isBefore', '1970');
var matches = await s.search();
assert.include(matches, item.id);
});
});
describe("#migrateExtraFields()", function () {
async function migrate(options) {
schema.version++;