Bug 678405: trim messageattributes table. r=asuth
authorJonathan Protzenko <jonathan.protzenko@gmail.com>
Mon, 19 Sep 2011 22:07:50 +0200
changeset 8695 87b0510db268daac8fed6814edb89f9c0a0810bb
parent 8694 123782592812d3047e4a2d84ce14c7e3e099b669
child 8696 d7ed2d9fa348a2ce5ec4ed625b9a622de315f833
push id6686
push userjonathan.protzenko@gmail.com
push dateSun, 23 Oct 2011 15:13:26 +0000
treeherdercomm-central@d7ed2d9fa348 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersasuth
bugs678405
Bug 678405: trim messageattributes table. r=asuth
mailnews/db/gloda/modules/fundattr.js
mailnews/db/gloda/modules/gloda.js
mailnews/db/gloda/modules/index_ab.js
mailnews/db/gloda/test/unit/base_index_messages.js
mailnews/db/gloda/test/unit/base_query_messages.js
mailnews/db/gloda/test/unit/test_query_core.js
--- 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 = [];