--- a/mailnews/db/gloda/modules/fundattr.js
+++ b/mailnews/db/gloda/modules/fundattr.js
@@ -146,16 +146,17 @@ var GlodaFundAttr = {
extensionName: Gloda.BUILT_IN,
attributeType: Gloda.kAttrFundamental,
attributeName: "messageKey",
singular: true,
special: Gloda.kSpecialColumn,
specialColumnName: "messageKey",
subjectNouns: [Gloda.NOUN_MESSAGE],
objectNoun: Gloda.NOUN_NUMBER,
+ canQuery: true,
}); // tested-by: test_attributes_fundamental
// We need to surface the deleted attribute for querying, but there is no
// reason for user code, so let's call it "_deleted" rather than deleted.
// (In fact, our validity constraints require a special query formulation
// that user code should have no clue exists. That's right user code,
// that's a dare.)
Gloda.defineAttribute({
@@ -264,16 +265,17 @@ var GlodaFundAttr = {
attributeName: "conversation",
singular: true,
special: Gloda.kSpecialColumnParent,
specialColumnName: "conversationID",
idStorageAttributeName: "_conversationID",
valueStorageAttributeName: "_conversation",
subjectNouns: [Gloda.NOUN_MESSAGE],
objectNoun: Gloda.NOUN_CONVERSATION,
+ canQuery: true,
});
// --- Fundamental
// From
this._attrFrom = Gloda.defineAttribute({
provider: this,
extensionName: Gloda.BUILT_IN,
attributeType: Gloda.kAttrFundamental,
@@ -337,16 +339,17 @@ var GlodaFundAttr = {
extensionName: Gloda.BUILT_IN,
attributeType: Gloda.kAttrFundamental,
attributeName: "headerMessageID",
singular: true,
special: Gloda.kSpecialString,
specialColumnName: "headerMessageID",
subjectNouns: [Gloda.NOUN_MESSAGE],
objectNoun: Gloda.NOUN_STRING,
+ canQuery: true,
}); // tested-by: test_attributes_fundamental
// Attachment MIME Types
this._attrAttachmentTypes = Gloda.defineAttribute({
provider: this,
extensionName: Gloda.BUILT_IN,
attributeType: Gloda.kAttrFundamental,
attributeName: "attachmentTypes",
--- a/mailnews/db/gloda/modules/gloda.js
+++ b/mailnews/db/gloda/modules/gloda.js
@@ -1481,39 +1481,54 @@ var Gloda = {
* @XXX potentially rename to not suggest binding is required.
*/
_bindAttribute: function gloda_ns_bindAttr(aAttrDef, aSubjectNounDef) {
let objectNounDef = aAttrDef.objectNounDef;
// -- the query constraint helpers
if (aSubjectNounDef.queryClass !== undefined) {
let constrainer;
+ let canQuery = true;
if (aAttrDef.special == this.kSpecialFulltext) {
constrainer = function() {
let constraint = [GlodaDatastore.kConstraintFulltext, aAttrDef];
for (let iArg = 0; iArg < arguments.length; iArg++) {
constraint.push(arguments[iArg]);
}
this._constraints.push(constraint);
return this;
};
}
- else {
+ else if (aAttrDef.canQuery || aAttrDef.attributeName[0] == "_") {
constrainer = function() {
let constraint = [GlodaDatastore.kConstraintIn, aAttrDef];
for (let iArg = 0; iArg < arguments.length; iArg++) {
constraint.push(arguments[iArg]);
}
this._constraints.push(constraint);
return this;
};
+ } else {
+ constrainer = function() {
+ throw new Error(
+ "Cannot query on attribute "+aAttrDef.attributeName
+ + " because its canQuery parameter hasn't been set to true."
+ + " Reading the comments about Gloda.defineAttribute may be a"
+ + " sensible thing to do now.");
+ }
+ canQuery = false;
}
aSubjectNounDef.queryClass.prototype[aAttrDef.boundName] = constrainer;
+ // Don't bind extra query-able attributes if we're unable to perform a
+ // search on the attribute.
+ if (!canQuery)
+ return;
+
// - ranged value helper: fooRange
if (objectNounDef.continuous) {
// takes one or more tuples of [lower bound, upper bound]
let rangedConstrainer = function() {
let constraint = [GlodaDatastore.kConstraintRanges, aAttrDef];
for (let iArg = 0; iArg < arguments.length; iArg++ ) {
constraint.push(arguments[iArg]);
}
@@ -1632,16 +1647,19 @@ var Gloda = {
// and explanation of what it holds, and use that to be friendlier?
throw Error("You omitted a required attribute defining property, please" +
" consult the documentation as penance.");
// -- Fill in defaults
if (!("emptySetIsSignificant" in aAttrDef))
aAttrDef.emptySetIsSignificant = false;
+ if (!("canQuery" in aAttrDef))
+ aAttrDef.canQuery = aAttrDef.facet ? true : false;
+
// return if the attribute has already been defined
if (aAttrDef.dbDef)
return aAttrDef;
// - first time we've seen a provider init logic
if (!(aAttrDef.provider.providerName in this._attrProviders)) {
this._attrProviders[aAttrDef.provider.providerName] = [];
if (aAttrDef.provider.contentWhittle)
@@ -2004,20 +2022,37 @@ var Gloda = {
let toJSON = objectNounDef.toJSON;
jsonDict[attrib.id] = [toJSON(subValue) for each
([, subValue] in Iterator(value))] ;
}
else
jsonDict[attrib.id] = value;
}
+ let oldValue = aOldItem[key];
+
+ // the 'old' item is still the canonical one; update it
+ // do the update now, because we may skip operations on addDBAttribs and
+ // removeDBattribs, if the attribute is not to generate entries in
+ // messageAttributes
+ if (oldValue !== undefined || !aIsConceptuallyNew)
+ aOldItem[key] = value;
+
+ // the new canQuery property has to be explicitly set to generate entries
+ // in the messageAttributes table, hence making the message query-able.
+ if (!attrib.canQuery) {
+ this._log.debug("Not inserting attribute "+attrib.attributeName
+ +" into the db, since we don't plan on querying on it");
+ continue;
+ }
+ this._log.debug("Inserting attribute "+attrib.attributeName);
+
// - database index attributes
// perform a delta analysis against the old value, if we have one
- let oldValue = aOldItem[key];
if (oldValue !== undefined) {
// in the singular case if they don't match, it's one add and one remove
if (attrib.singular) {
// test for identicality, failing that, see if they have explicit
// equals support.
if ((value !== oldValue) &&
(!value.equals || !value.equals(oldValue))) {
addDBAttribs.push(attribDB.convertValuesToDBAttributes([value])[0]);
@@ -2070,26 +2105,19 @@ var Gloda = {
if (attrib.emptySetIsSignificant) {
// if we are now non-zero but previously were zero, remove.
if (value.length && !oldValue.length)
removeDBAttribs.push([GlodaDatastore.kEmptySetAttrId, attribDB.id]);
// if we are now zero length but previously were not, add
else if (!value.length && oldValue.length)
addDBAttribs.push([GlodaDatastore.kEmptySetAttrId, attribDB.id]);
}
-
- // replace the old value with the new values... (the 'old' item is
- // canonical)
- aOldItem[key] = value;
}
// no old value, all values are new
else {
- // the 'old' item is still the canonical one; update it
- if (!aIsConceptuallyNew)
- aOldItem[key] = value;
// add the db reps on the new values
if (attrib.singular)
value = [value];
addDBAttribs.push.apply(addDBAttribs,
attribDB.convertValuesToDBAttributes(value));
// Add the empty set indicator for the attribute id if appropriate.
if (!value.length && attrib.emptySetIsSignificant)
addDBAttribs.push([GlodaDatastore.kEmptySetAttrId, attribDB.id]);
@@ -2108,27 +2136,35 @@ var Gloda = {
continue;
// find the attribute definition that corresponds to this key
let attrib = attribsByBoundName[key];
// if there's no attribute, that's not good, but not horrible.
if (attrib === undefined) {
continue;
}
+
+ // delete these from the old item, as the old item is canonical, and
+ // should no longer have these values
+ delete aOldItem[key];
+
+ if (!attrib.canQuery) {
+ this._log.debug("Not inserting attribute "+attrib.attributeName
+ +" into the db, since we don't plan on querying on it");
+ continue;
+ }
+
if (attrib.singular)
value = [value];
let attribDB = attrib.dbDef;
removeDBAttribs.push.apply(removeDBAttribs,
attribDB.convertValuesToDBAttributes(value));
// remove the empty set marker if there should have been one
if (!value.length && attrib.emptySetIsSignificant)
removeDBAttribs.push([GlodaDatastore.kEmptySetAttrId, attribDB.id]);
- // delete these from the old item, as the old item is canonical, and
- // should no longer have these values
- delete aOldItem[key];
}
aItem._jsonText = JSON.stringify(jsonDict);
this._log.debug(" json text: " + aItem._jsonText);
if (aIsRecordNew) {
this._log.debug(" inserting item");
itemNounDef.objInsert.call(itemNounDef.datastore, aItem);
--- a/mailnews/db/gloda/modules/index_ab.js
+++ b/mailnews/db/gloda/modules/index_ab.js
@@ -192,93 +192,100 @@ var GlodaABAttrs = {
extensionName: Gloda.BUILT_IN,
attributeType: Gloda.kAttrFundamental,
attributeName: "name",
singular: true,
special: Gloda.kSpecialString,
specialColumnName: "name",
subjectNouns: [Gloda.NOUN_CONTACT],
objectNoun: Gloda.NOUN_STRING,
+ canQuery: true,
}); // tested-by: test_attributes_fundamental
this._attrContactPopularity = Gloda.defineAttribute({
provider: this,
extensionName: Gloda.BUILT_IN,
attributeType: Gloda.kAttrDerived,
attributeName: "popularity",
singular: true,
special: Gloda.kSpecialColumn,
specialColumnName: "popularity",
subjectNouns: [Gloda.NOUN_CONTACT],
objectNoun: Gloda.NOUN_NUMBER,
+ canQuery: true,
}); // not-tested
this._attrContactFrecency = Gloda.defineAttribute({
provider: this,
extensionName: Gloda.BUILT_IN,
attributeType: Gloda.kAttrDerived,
attributeName: "frecency",
singular: true,
special: Gloda.kSpecialColumn,
specialColumnName: "frecency",
subjectNouns: [Gloda.NOUN_CONTACT],
objectNoun: Gloda.NOUN_NUMBER,
+ canQuery: true,
}); // not-tested
/* ***** Identities ***** */
this._attrIdentityContact = Gloda.defineAttribute({
provider: this,
extensionName: Gloda.BUILT_IN,
attributeType: Gloda.kAttrDerived,
attributeName: "contact",
singular: true,
special: Gloda.kSpecialColumnParent,
specialColumnName: "contactID", // the column in the db
idStorageAttributeName: "_contactID",
valueStorageAttributeName: "_contact",
subjectNouns: [Gloda.NOUN_IDENTITY],
objectNoun: Gloda.NOUN_CONTACT,
+ canQuery: true,
}); // tested-by: test_attributes_fundamental
this._attrIdentityKind = Gloda.defineAttribute({
provider: this,
extensionName: Gloda.BUILT_IN,
attributeType: Gloda.kAttrFundamental,
attributeName: "kind",
singular: true,
special: Gloda.kSpecialString,
specialColumnName: "kind",
subjectNouns: [Gloda.NOUN_IDENTITY],
objectNoun: Gloda.NOUN_STRING,
+ canQuery: true,
}); // tested-by: test_attributes_fundamental
this._attrIdentityValue = Gloda.defineAttribute({
provider: this,
extensionName: Gloda.BUILT_IN,
attributeType: Gloda.kAttrFundamental,
attributeName: "value",
singular: true,
special: Gloda.kSpecialString,
specialColumnName: "value",
subjectNouns: [Gloda.NOUN_IDENTITY],
objectNoun: Gloda.NOUN_STRING,
+ canQuery: true,
}); // tested-by: test_attributes_fundamental
/* ***** Contact Meta ***** */
// Freeform tags; not explicit like thunderbird's fundamental tags.
// we differentiate for now because of fundamental implementation
// differences.
this._attrFreeTag = Gloda.defineAttribute({
- provider: this,
- extensionName: Gloda.BUILT_IN,
- attributeType: Gloda.kAttrExplicit,
- attributeName: "freetag",
- bind: true,
- bindName: "freeTags",
- singular: false,
- subjectNouns: [Gloda.NOUN_CONTACT],
- objectNoun: Gloda.lookupNoun("freetag"),
- parameterNoun: null,
- }); // not-tested
+ provider: this,
+ extensionName: Gloda.BUILT_IN,
+ attributeType: Gloda.kAttrExplicit,
+ attributeName: "freetag",
+ bind: true,
+ bindName: "freeTags",
+ singular: false,
+ subjectNouns: [Gloda.NOUN_CONTACT],
+ objectNoun: Gloda.lookupNoun("freetag"),
+ parameterNoun: null,
+ canQuery: true,
+ }); // not-tested
// we need to find any existing bound freetag attributes, and use them to
// populate to FreeTagNoun's understanding
for (let freeTagName in this._attrFreeTag.parameterBindings) {
this._log.debug("Telling FreeTagNoun about: " + freeTagName);
FreeTagNoun.getFreeTag(freeTagName);
}
},
--- a/mailnews/db/gloda/test/unit/base_index_messages.js
+++ b/mailnews/db/gloda/test/unit/base_index_messages.js
@@ -650,16 +650,57 @@ function test_attributes_explicit() {
do_check_eq(gmsg.tags.indexOf(tagOne), -1);
do_check_eq(gmsg.tags.indexOf(tagTwo), -1);
// -- Replied To
// -- Forwarded
}
+
+/**
+ * Test non-query-able attributes
+ */
+function test_attributes_cant_query() {
+ let [folder, msgSet] = make_folder_with_sets([{count: 1}]);
+ yield wait_for_message_injection();
+ yield wait_for_gloda_indexer(msgSet, {augment: true});
+ let gmsg = msgSet.glodaMessages[0];
+
+ // -- Star
+ mark_sub_test_start("Star");
+ msgSet.setStarred(true);
+ yield wait_for_gloda_indexer(msgSet);
+ do_check_eq(gmsg.starred, true);
+
+ msgSet.setStarred(false);
+ yield wait_for_gloda_indexer(msgSet);
+ do_check_eq(gmsg.starred, false);
+
+ // -- Read / Unread
+ mark_sub_test_start("Read/Unread");
+ msgSet.setRead(true);
+ yield wait_for_gloda_indexer(msgSet);
+ do_check_eq(gmsg.read, true);
+
+ msgSet.setRead(false);
+ yield wait_for_gloda_indexer(msgSet);
+ do_check_eq(gmsg.read, false);
+
+ let readDbAttr = Gloda.getAttrDef(Gloda.BUILT_IN, "read");
+ let readId = readDbAttr.id;
+
+ yield sqlExpectCount(0, "SELECT COUNT(*) FROM messageAttributes WHERE attributeID = ?1",
+ readId);
+
+ // -- Replied To
+
+ // -- Forwarded
+}
+
/* ===== Fulltext Indexing ===== */
/**
* Make sure that we are using the saneBodySize flag. This is basically the
* test_sane_bodies test from test_mime_emitter but we pull the indexedBodyText
* off the message to check and also make sure that the text contents slice
* off the end rather than the beginning.
*/
@@ -1182,16 +1223,17 @@ var tests = [
test_event_driven_indexing_does_not_mess_with_filthy_folders,
test_threading,
test_attachment_flag,
test_attributes_fundamental,
test_attributes_fundamental_from_disk,
test_moved_message_attributes,
test_attributes_explicit,
+ test_attributes_cant_query,
test_streamed_bodies_are_size_capped,
test_imap_add_unread_to_folder,
test_message_moving,
test_message_deletion,
test_moving_to_trash_marks_deletion,
--- a/mailnews/db/gloda/test/unit/base_query_messages.js
+++ b/mailnews/db/gloda/test/unit/base_query_messages.js
@@ -490,17 +490,17 @@ function test_query_conversations_by_sub
* Test subject searching using the conversation unique subject term.
*
* @tests gloda.noun.message.attr.subjectMatches
* @tests gloda.datastore.sqlgen.kConstraintFulltext
*/
function test_query_messages_by_subject_text() {
// we only need to use one conversation
let convNum = 0;
-dump("convNum: " + convNum + " blah: " + world.conversationLists[convNum] + "\n");
+ // dump("convNum: " + convNum + " blah: " + world.conversationLists[convNum] + "\n");
let query = Gloda.newQuery(Gloda.NOUN_MESSAGE);
let convSubjectTerm = uniqueTermGenerator(
UNIQUE_OFFSET_SUBJECT + UNIQUE_OFFSET_CONV + convNum);
query.subjectMatches(convSubjectTerm);
queryExpect(query, world.conversationLists[convNum]);
return false; // async pend on queryExpect
}
--- a/mailnews/db/gloda/test/unit/test_query_core.js
+++ b/mailnews/db/gloda/test/unit/test_query_core.js
@@ -177,90 +177,98 @@ function setup_test_noun_and_attributes(
Gloda.defineAttribute({
provider: WidgetProvider, extensionName: EXT_NAME,
attributeType: Gloda.kAttrFundamental,
attributeName: "inum",
singular: true,
special: Gloda.kSpecialColumn,
specialColumnName: "intCol",
subjectNouns: [WidgetNoun.id],
- objectNoun: Gloda.NOUN_NUMBER
+ objectNoun: Gloda.NOUN_NUMBER,
+ canQuery: true,
});
Gloda.defineAttribute({
provider: WidgetProvider, extensionName: EXT_NAME,
attributeType: Gloda.kAttrFundamental,
attributeName: "date",
singular: true,
special: Gloda.kSpecialColumn,
specialColumnName: "dateCol",
subjectNouns: [WidgetNoun.id],
- objectNoun: Gloda.NOUN_DATE
+ objectNoun: Gloda.NOUN_DATE,
+ canQuery: true,
});
Gloda.defineAttribute({
provider: WidgetProvider, extensionName: EXT_NAME,
attributeType: Gloda.kAttrFundamental,
attributeName: "str",
singular: true,
special: Gloda.kSpecialString,
specialColumnName: "strCol",
subjectNouns: [WidgetNoun.id],
- objectNoun: Gloda.NOUN_STRING
+ objectNoun: Gloda.NOUN_STRING,
+ canQuery: true,
});
// --- fulltext attributes
Gloda.defineAttribute({
provider: WidgetProvider, extensionName: EXT_NAME,
attributeType: Gloda.kAttrFundamental,
attributeName: "text1",
singular: true,
special: Gloda.kSpecialFulltext,
specialColumnName: "fulltextOne",
subjectNouns: [WidgetNoun.id],
- objectNoun: Gloda.NOUN_FULLTEXT
+ objectNoun: Gloda.NOUN_FULLTEXT,
+ canQuery: true,
});
Gloda.defineAttribute({
provider: WidgetProvider, extensionName: EXT_NAME,
attributeType: Gloda.kAttrFundamental,
attributeName: "text2",
singular: true,
special: Gloda.kSpecialFulltext,
specialColumnName: "fulltextTwo",
subjectNouns: [WidgetNoun.id],
- objectNoun: Gloda.NOUN_FULLTEXT
+ objectNoun: Gloda.NOUN_FULLTEXT,
+ canQuery: true,
});
Gloda.defineAttribute({
provider: WidgetProvider, extensionName: EXT_NAME,
attributeType: Gloda.kAttrFundamental,
attributeName: "fulltextAll",
singular: true,
special: Gloda.kSpecialFulltext,
specialColumnName: WidgetNoun.tableName + "Text",
subjectNouns: [WidgetNoun.id],
- objectNoun: Gloda.NOUN_FULLTEXT
+ objectNoun: Gloda.NOUN_FULLTEXT,
+ canQuery: true,
});
// --- external (attribute-storage) attributes
Gloda.defineAttribute({
provider: WidgetProvider, extensionName: EXT_NAME,
attributeType: Gloda.kAttrFundamental,
attributeName: "singleIntAttr",
singular: true,
subjectNouns: [WidgetNoun.id],
- objectNoun: Gloda.NOUN_NUMBER
+ objectNoun: Gloda.NOUN_NUMBER,
+ canQuery: true,
});
Gloda.defineAttribute({
provider: WidgetProvider, extensionName: EXT_NAME,
attributeType: Gloda.kAttrFundamental,
attributeName: "multiIntAttr",
singular: false,
emptySetIsSignificant: true,
subjectNouns: [WidgetNoun.id],
- objectNoun: Gloda.NOUN_NUMBER
+ objectNoun: Gloda.NOUN_NUMBER,
+ canQuery: true,
});
}
/* ===== Tests ===== */
const ALPHABET = "abcdefghijklmnopqrstuvwxyz";
function test_lots_of_string_constraints() {
let stringConstraints = [];