Bug 743235 - Search emails and IM conversations at once with gloda, r+ui-r=bwinton, r=asuth.
authorFlorian Quèze <florian@queze.net>
Wed, 09 May 2012 15:06:31 +0200
changeset 12075 f31b8ad31c08269274a7c70e18aa97ec37674bd0
parent 12074 e2a736a2a7b0b61152bf8ee11809c877a4dde39c
child 12076 3a60b791073307b5f91ad663ad53ac581694fddb
push id599
push usermconley@mozilla.com
push dateMon, 16 Jul 2012 20:33:12 +0000
treeherdercomm-beta@c3489d5b7b65 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersasuth
bugs743235
Bug 743235 - Search emails and IM conversations at once with gloda, r+ui-r=bwinton, r=asuth.
mail/base/content/glodaFacetBindings.xml
mail/base/content/glodaFacetTab.js
mail/base/content/glodaFacetView.js
mail/base/content/search.xml
mail/components/im/content/chat-messenger-overlay.xul
mail/components/im/content/chat.css
mail/components/im/content/imsearch.xml
mail/components/im/jar.mn
mail/components/im/modules/index_im.js
mail/locales/en-US/chrome/messenger/chat.dtd
mail/locales/en-US/chrome/messenger/glodaFacetView.properties
mailnews/db/gloda/modules/datamodel.js
mailnews/db/gloda/modules/datastore.js
mailnews/db/gloda/modules/facet.js
mailnews/db/gloda/modules/fundattr.js
mailnews/db/gloda/modules/gloda.js
mailnews/db/gloda/modules/query.js
--- a/mail/base/content/glodaFacetBindings.xml
+++ b/mail/base/content/glodaFacetBindings.xml
@@ -1400,18 +1400,23 @@
                                       outOfPluralFormat)
                                  .replace("#1", totalCount.toLocaleString());
         countNode.textContent = groupingFormat.replace("#1", topMessagesStr)
                                               .replace("#2", outOfStr);
 
         // -- Show All
         let showNode = document.getAnonymousElementByAttribute(
                           this, "anonid", "showall");
+        const GlodaMessage = Gloda.lookupNounDef("message").clazz;
+        let visble = aMessages.some(function(m) m instanceof GlodaMessage);
+        showNode.style.display = visble ? "inline" : "none";
         showNode.textContent = glodaFacetStrings.get(
-          "glodaFacetView.results.message.openAsList.label");
+          "glodaFacetView.results.message.openEmailAsList.label");
+        showNode.setAttribute("title", glodaFacetStrings.get(
+          "glodaFacetView.results.message.openEmailAsList.tooltip"));
         showNode.onkeypress = function(event) {
           if (event.charCode == KeyEvent.DOM_VK_SPACE) {
             FacetContext.showActiveSetInTab()
             event.preventDefault();
           }
         }
 
         let sortLabelNode = document.getAnonymousElementByAttribute(
@@ -1542,37 +1547,24 @@
         let message = this.message;
         let dis = this;
         function anonElem(aAnonId) {
           return document.getAnonymousElementByAttribute(dis, "anonid",
                                                          aAnonId);
         }
         let subject = anonElem("subject");
         // -- eventify
-        if (FacetContext.searcher instanceof GlodaIMSearcher) {
-          subject.onclick = function(aEvent) {
-            FacetContext.showIMConversationInTab(message,
-                                                 aEvent.button == 1);
-          }
-          subject.onkeypress = function(aEvent) {
-            if (aEvent.keyCode == aEvent.DOM_VK_RETURN)
-              FacetContext.showIMConversationInTab(message,
-                                                   aEvent.shiftKey == true);
-          }
+        subject.onclick = function(aEvent) {
+          FacetContext.showConversationInTab(message,
+                                             aEvent.button == 1);
         }
-        else {
-          subject.onclick = function(aEvent) {
+        subject.onkeypress = function(aEvent) {
+          if (aEvent.keyCode == aEvent.DOM_VK_RETURN)
             FacetContext.showConversationInTab(message,
-                                               aEvent.button == 1);
-          }
-          subject.onkeypress = function(aEvent) {
-            if (aEvent.keyCode == aEvent.DOM_VK_RETURN)
-              FacetContext.showConversationInTab(message,
-                                                 aEvent.shiftKey == true);
-          }
+                                               aEvent.shiftKey == true);
         }
 
         // -- Content Poking
         if (message.subject.trim() == "")
           subject.textContent = glodaFacetStrings.get("glodaFacetView.result.message.noSubject");
         else
           subject.textContent = message.subject;
         let authorNode = anonElem("author");
@@ -1663,17 +1655,22 @@
         }
 
         // - Body
         if (message.indexedBodyText) {
           let bodyText = message.indexedBodyText;
 
           let matches = [];
           if ("stashedColumns" in FacetContext.collection) {
-            let offsets = FacetContext.collection.stashedColumns[message.id][0];
+            let collection;
+            if (message instanceof Gloda.lookupNounDef("im-conversation").clazz)
+              collection = FacetContext.IMCollection;
+            else
+              collection = FacetContext.collection;
+            let offsets = collection.stashedColumns[message.id][0];
             let offsetNums = [parseInt(x) for each (x in offsets.split(" "))];
             for (let i = 0; i < offsetNums.length; i += 4) {
               // i is the column index. The indexedBodyText is in the column 0.
               // Ignore matches for other columns.
               if (offsetNums[i] != 0)
                 continue;
 
               // i+1 is the term index, indicating which queried term was found.
--- a/mail/base/content/glodaFacetTab.js
+++ b/mail/base/content/glodaFacetTab.js
@@ -40,16 +40,21 @@ var glodaFacetTabType = {
 
       aTab.title = this.strings.get("glodaFacetView.tab.query.label");
       aTab.searchString = null;
     }
     else if ("searcher" in aArgs) {
       aTab.searcher = aArgs.searcher;
       aTab.collection = aTab.searcher.getCollection();
       aTab.query = aTab.searcher.query;
+      if ("IMSearcher" in aArgs) {
+        aTab.IMSearcher = aArgs.IMSearcher;
+        aTab.IMCollection = aArgs.IMSearcher.getCollection();
+        aTab.IMQuery = aTab.IMSearcher.query;
+      }
 
       let searchString = aTab.searcher.searchString;
       aTab.title = aTab.searchInputValue = aTab.searchString =
         searchString;
     }
     else if ("collection" in aArgs) {
       aTab.collection = aArgs.collection;
 
--- a/mail/base/content/glodaFacetView.js
+++ b/mail/base/content/glodaFacetView.js
@@ -324,25 +324,17 @@ ActiveNonSingularConstraint.prototype = 
   },
   isExcludedGroup: function(aGroupValue) {
     let valId = aGroupValue[this.facetDef.groupIdAttr];
     return (valId in this.excludedGroupIds);
   }
 };
 
 var FacetContext = {
-  get facetDriver() {
-    if (!("GlodaIMSearcher" in window))
-      Cu.import("resource:///modules/search_im.js");
-    let nounName =
-      this.searcher instanceof GlodaIMSearcher ? "im-conversation" : "message";
-    delete this.facetDriver;
-    this.facetDriver = new FacetDriver(Gloda.lookupNounDef(nounName), window);
-    return this.facetDriver;
-  },
+  facetDriver: new FacetDriver(Gloda.lookupNounDef("message"), window),
 
   /**
    * The root collection which our active set is a subset of.  We hold onto this
    *  for garbage collection reasons, although the tab that owns us should also
    *  be holding on.
    */
   _collection: null,
   set collection(aCollection) {
@@ -415,16 +407,18 @@ var FacetContext = {
 
     this.everFaceted = false;
     this._activeConstraints = {};
     if (this.searcher)
       this._sortBy = '-dascore';
     else
       this._sortBy = '-date';
     this.fullSet = this._removeDupes(this._collection.items.concat());
+    if ("IMCollection" in this)
+      this.fullSet = this.fullSet.concat(this.IMCollection.items);
     this.build(this.fullSet);
   },
 
   /**
    * Remove duplicate messages from search results.
    *
    * @param aItems the initial set of messages to deduplicate
    * @return the subset of those, with duplicates removed.
@@ -838,40 +832,33 @@ var FacetContext = {
   /**
    * Show the conversation in a new glodaList tab.
    *
    * @param {GlodaConversation} aConversation The conversation to show.
    * @param {Boolean} [aBackground] Whether it should be in the background.
    */
   showConversationInTab: function(aMessage, aBackground) {
     let tabmail = this.rootWin.document.getElementById("tabmail");
+    if (aMessage instanceof Gloda.lookupNounDef("im-conversation").clazz) {
+      tabmail.openTab("chat", {
+        convType: "log",
+        conv: aMessage,
+        background: aBackground
+      });
+      return;
+    }
     tabmail.openTab("glodaList", {
       conversation: aMessage.conversation,
       message: aMessage,
       title: aMessage.conversation.subject,
       background: aBackground
     });
   },
 
   /**
-   * Show the conversation in a new glodaList tab.
-   *
-   * @param {GlodaIMConversation} aConversation The conversation to show.
-   * @param {Boolean} [aBackground] Whether it should be in the background.
-   */
-  showIMConversationInTab: function(aConversation, aBackground) {
-    let tabmail = this.rootWin.document.getElementById("tabmail");
-    tabmail.openTab("chat", {
-      convType: "log",
-      conv: aConversation,
-      background: aBackground
-    });
-  },
-
-  /**
    * Show the message in a new tab.
    *
    * @param {GlodaMessage} aMessage The message to show.
    * @param {Boolean} [aBackground] Whether it should be in the background.
    */
   showMessageInTab: function(aMessage, aBackground) {
     let tabmail = this.rootWin.document.getElementById("tabmail");
     let msgHdr = aMessage.folderMessage;
@@ -885,17 +872,19 @@ var FacetContext = {
 
   onItemsAdded: function(aItems, aCollection) {
   },
   onItemsModified: function(aItems, aCollection) {
   },
   onItemsRemoved: function(aItems, aCollection) {
   },
   onQueryCompleted: function(aCollection) {
-    this.initialBuild();
+    if (this.tab.query.completed &&
+        (!("IMQuery" in aTab) || this.tab.IMQuery.completed))
+      this.initialBuild();
   }
 };
 
 /**
  * addEventListener betrayals compel us to establish our link with the
  *  outside world from inside.  NeilAway suggests the problem might have
  *  been the registration of the listener prior to initiating the load.  Which
  *  is odd considering it works for the XUL case, but I could see how that might
@@ -912,31 +901,37 @@ function reachOutAndTouchFrame() {
 
   let parentWin = us.parent
                     .QueryInterface(Ci.nsIInterfaceRequestor)
                     .getInterface(Ci.nsIDOMWindow);
   let aTab = FacetContext.tab = parentWin.tab;
   parentWin.tab = null;
   $(window).resize(function() {
     document.getElementById("facet-date").build(true);
-  })
+  });
   // we need to hook the context up as a listener in all cases since
   //  removal notifications are required.
   if ("searcher" in aTab) {
     FacetContext.searcher = aTab.searcher;
     aTab.searcher.listener = FacetContext;
+    if ("IMSearcher" in aTab) {
+      FacetContext.IMSearcher = aTab.IMSearcher;
+      aTab.IMSearcher.listener = FacetContext;
+    }
   }
   else {
     FacetContext.searcher = null;
     aTab.collection.listener = FacetContext;
   }
   FacetContext.collection = aTab.collection;
+  if ("IMCollection" in aTab)
+    FacetContext.IMCollection = aTab.IMCollection;
 
   // if it has already completed, we need to prod things
-  if (aTab.query.completed)
+  if (aTab.query.completed && (!("IMQuery" in aTab) || aTab.IMQuery.completed))
     FacetContext.initialBuild();
 }
 
 function clickOnBody(event) {
   if (event.bubbles) {
     document.getElementById('popup-menu').hide();
   }
   return 0;
--- a/mail/base/content/search.xml
+++ b/mail/base/content/search.xml
@@ -281,19 +281,28 @@
               if (tabmail.currentTabInfo.mode.name == "glodaFacet") {
                 // we'd rather reuse the existing tab (and somehow do something
                 // smart with any preexisting facet choices, but that's a
                 // bit hard right now, so doing the cheap thing and closing
                 // this tab and starting over
                 tabmail.closeTab();
               }
               this.value = ''; // clear our value, to avoid persistence
-              tabmail.openTab("glodaFacet", {
+              let args = {
                 searcher: new GlodaMsgSearcher(null, searchString)
-              });
+              };
+              let prefBranch =
+                Components.classes['@mozilla.org/preferences-service;1'].
+                getService(Components.interfaces.nsIPrefBranch);
+              if (prefBranch.getBoolPref("mail.chat.enabled")) {
+                if (!("GlodaIMSearcher" in window))
+                  Cu.import("resource:///modules/search_im.js");
+                args.IMSearcher = new GlodaIMSearcher(null, searchString);
+              }
+              tabmail.openTab("glodaFacet", args);
             }
           } catch (e) {
             logException(e);
           }
         ]]>
         </body>
       </method>
       <method name="clearSearch">
--- a/mail/components/im/content/chat-messenger-overlay.xul
+++ b/mail/components/im/content/chat-messenger-overlay.xul
@@ -182,17 +182,17 @@
             <textbox id="IMSearchInput" 
                      flex="1"
                      type="glodacomplete"
                      searchbutton="true"
                      enablehistory="false"
                      timeout="200"
                      maxlength="192"
                      placeholder=""
-                     emptytextbase="&searchAllChatMessages.label.base;"
+                     emptytextbase="&search.label.base;"
                      keyLabelNonMac="&search.keyLabel.nonmac;"
                      keyLabelMac="&search.keyLabel.mac;"
                      >
               <!-- Mimic the search/clear buttons of the standard search textbox,
                    but adjust for the reality that clear doesn't make much sense
                    since gloda results only show in a tab and the idiom for closing
                    tabs is closing the tab.  Our binding does process escape to
                    clear the box, if people want to clear it that way...
--- a/mail/components/im/content/chat.css
+++ b/mail/components/im/content/chat.css
@@ -81,17 +81,17 @@ browser[type="content-conversation"] {
   display: none;
 }
 
 .conv-top-info {
   -moz-binding: url("chrome://messenger/content/chat/imconversation.xml#conv-info-large") !important;
 }
 
 #IMSearchInput {
-  -moz-binding: url("chrome://messenger/content/chat/imsearch.xml#IMGlodaSearch");
+  -moz-binding: url("chrome://messenger/content/search.xml#glodaSearch");
 }
 
 
 #statusTypeIcon {
   cursor: pointer;
 }
 
 #statusMessage {
deleted file mode 100644
--- a/mail/components/im/content/imsearch.xml
+++ /dev/null
@@ -1,258 +0,0 @@
-<?xml version="1.0"?>
-
-<!-- ***** BEGIN LICENSE BLOCK *****
-  - Version: MPL 1.1/GPL 2.0/LGPL 2.1
-  -
-  - The contents of this file are subject to the Mozilla Public License Version
-  - 1.1 (the "License"); you may not use this file except in compliance with
-  - the License. You may obtain a copy of the License at
-  - http://www.mozilla.org/MPL/
-  -
-  - Software distributed under the License is distributed on an "AS IS" basis,
-  - WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
-  - for the specific language governing rights and limitations under the
-  - License.
-  -
-  - The Original Code is Mozilla Communicator client code, released
-  - March 31, 1998.
-  -
-  - The Initial Developer of the Original Code is
-  - Netscape Communications Corporation.
-  - Portions created by the Initial Developer are Copyright (C) 1998-1999
-  - the Initial Developer. All Rights Reserved.
-  -
-  - Contributor(s):
-  -   Scott MacGregor <mscott@mozilla.org>
-  -   Andrew Sutherland <asutherland@asutherland.org>
-  -   David Ascher <dascher@mozillamessaging.com>
-  -   Thomas Düllmann <bugzilla2009@duellmann24.net>
-  -
-  - Alternatively, the contents of this file may be used under the terms of
-  - either of the GNU General Public License Version 2 or later (the "GPL"),
-  - or the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
-  - in which case the provisions of the GPL or the LGPL are applicable instead
-  - of those above. If you wish to allow use of your version of this file only
-  - under the terms of either the GPL or the LGPL, and not to allow others to
-  - use your version of this file under the terms of the MPL, indicate your
-  - decision by deleting the provisions above and replace them with the notice
-  - and other provisions required by the GPL or the LGPL. If you do not delete
-  - the provisions above, a recipient may use your version of this file under
-  - the terms of any one of the MPL, the GPL or the LGPL.
-  - ***** END LICENSE BLOCK ***** -->
-
-<!DOCTYPE bindings [
-<!ENTITY % messengerDTD SYSTEM "chrome://messenger/locale/messenger.dtd">
-%messengerDTD;
-]>
-
-<bindings id="SearchBindings"
-   xmlns="http://www.mozilla.org/xbl"
-   xmlns:html="http://www.w3.org/1999/xhtml"
-   xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
-   xmlns:xbl="http://www.mozilla.org/xbl">
-
-  <!--
-    - The glodaSearch binding implements a gloda-backed search mechanism.  The
-    -  actual search logic comes from the glodaFacet tab mode in the
-    -  glodaFacetTabType definition.  This binding serves as a means to display
-    -  and alter the current search query if a "glodaFacet" tab is displayed,
-    -  or enter a search query and spawn a new "glodaFacet" tab if one is
-    -  currently not displayed.
-    -
-    - This widget used to have many weird implementation nuances.  Now we are
-    -  just a little bit of extra stuff on top of the toolkit autocomplete
-    -  implementation.  Our deviations are:
-    -  - We collapse ourselves when gloda is disabled; we track the state.
-    -  -
-    -->
-  <binding id="IMGlodaSearch"
-           extends="chrome://global/content/bindings/autocomplete.xml#autocomplete">
-    <resources>
-      <stylesheet src="chrome://messenger/skin/searchBox.css"/>
-    </resources>
-
-    <handlers>
-      <handler event="drop" phase="capturing"><![CDATA[
-        nsDragAndDrop.drop(event, this.searchInputDNDObserver);
-      ]]></handler>
-
-      <handler event="keypress" keycode="VK_RETURN"><![CDATA[
-        this.doSearch();
-        event.preventDefault();
-        event.stopPropagation();
-      ]]></handler>
-      <handler event="keypress" keycode="VK_ESCAPE"><![CDATA[
-        this.clearSearch();
-        event.preventDefault();
-        event.stopPropagation();
-      ]]></handler>
-    </handlers>
-
-    <implementation implements="nsIObserver">
-      <constructor><![CDATA[
-        const Cc = Components.classes;
-        const Ci = Components.interfaces;
-        const Cu = Components.utils;
-        Cu.import("resource:///modules/errUtils.js");
-        try {
-          this.setAttribute(
-            "placeholder",
-            this.getAttribute("emptytextbase")
-                 .replace("#1", this.getAttribute(
-                                  Application.platformIsMac ?
-                                  "keyLabelMac" : "keyLabelNonMac")));
-
-          var prefBranch =
-              Components.classes['@mozilla.org/preferences-service;1'].
-              getService(Components.interfaces.nsIPrefBranch2);
-
-          if (typeof gSearchInputObserversRegistered != "undefined") {
-            // due to a bug in XBL that means that destructors don't get
-            // called reliably, the customize toolbar path will end up creating
-            // two clones of this widget, and never destroy them.  So the destructor
-            // isn't reliably called, and so we must use a global to avoid
-            // registering ourselves for the same event multiple times (twice
-            // per invocation of the customize-toolbar).
-
-            // We need to test for undefined above because if the widget is
-            // created from the customize toolbar, its namespace won't include
-            // a definition of gSearchInputObserversRegistered -- and we don't
-            // care, since we don't want to register observers in that scope.
-
-            prefBranch.addObserver("mailnews.database.global.indexer.enabled",
-                                   this._prefObserver, false);
-          }
-
-          this.glodaEnabled =
-            prefBranch.getBoolPref("mailnews.database.global.indexer.enabled");
-          this.collapsed = !this.glodaEnabled;
-
-          // make sure we set our emptytext here from the get-go
-        if (this.hasAttribute("placeholder"))
-          this.placeholder = this.getAttribute("placeholder");
-        } catch (e) {
-          logException(e, true);
-        }
-      ]]></constructor>
-
-      <destructor>
-        <![CDATA[
-          var prefBranch =
-              Components.classes['@mozilla.org/preferences-service;1'].
-              getService(Components.interfaces.nsIPrefBranch);
-          prefBranch.removeObserver("mailnews.database.global.indexer.enabled",
-                                    this._prefObserver);
-        ]]>
-      </destructor>
-
-      <field name="_prefObserver">({
-        inputSearch: this,
-        observe: function(subject, topic, data)
-        {
-          if (topic == "nsPref:changed") {
-            subject.QueryInterface(Components.interfaces.nsIPrefBranch);
-            switch (data) {
-            case "mailnews.database.global.indexer.enabled":
-              this.inputSearch.glodaEnabled =
-                gPrefBranch.getBoolPref(
-                  "mailnews.database.global.indexer.enabled");
-              this.inputSearch.collapsed = !this.inputSearch.glodaEnabled;
-              break;
-            }
-          }
-        },
-
-        QueryInterface: function(aIID)
-        {
-          if (aIID.equals(Components.interfaces.nsIObserver) ||
-              aIID.equals(Components.interfaces.nsISupports))
-            return this;
-          throw Components.results.NS_NOINTERFACE;
-        }
-        });
-      </field>
-      <property name="menupopup" readonly="true">
-        <getter><![CDATA[
-          return document.getAnonymousElementByAttribute(
-                   this, 'anonid', 'quick-search-menupopup');
-        ]]></getter>
-      </property>
-
-      <property name="state">
-        <getter><![CDATA[
-          return {
-            'string': this.value
-          };
-        ]]></getter>
-        <setter><![CDATA[
-          this.value = val['string'];
-        ]]></setter>
-      </property>
-
-      // DND Observer
-      <field name="searchInputDNDObserver" readonly="true"><![CDATA[
-      ({
-        inputSearch: this,
-
-        onDrop: function (aEvent, aXferData, aDragSession) {
-          try {
-            if (aXferData.data) {
-              this.inputSearch.focus();
-              this.inputSearch.value = aXferData.data;
-              // XXX for some reason the input field is _cleared_ even though
-              // the search works.
-              this.inputSearch.doSearch();
-            }
-          } catch (e) {
-            logException(e);
-          }
-        },
-
-        getSupportedFlavours: function () {
-          var flavourSet = new FlavourSet();
-          flavourSet.appendFlavour("text/unicode");
-          return flavourSet;
-        }
-      })
-      ]]></field>
-
-      <method name="doSearch">
-        <body><![CDATA[
-          try {
-            if (this.value) {
-              let tabmail = document.getElementById("tabmail");
-              // If the current tab is a gloda search tab, reset the value
-              //  to the initial search value.  Otherwise, clear it.  This
-              //  is the value that 3is going to be saved with the current
-              //  tab when we switch back to it next.
-              let searchString = this.value;
-
-              if (tabmail.currentTabInfo.mode.name == "glodaFacet") {
-                // we'd rather reuse the existing tab (and somehow do something
-                // smart with any preexisting facet choices, but that's a
-                // bit hard right now, so doing the cheap thing and closing
-                // this tab and starting over
-                tabmail.closeTab();
-              }
-              this.value = ''; // clear our value, to avoid persistence
-              if (!("GlodaIMSearcher" in window))
-                Cu.import("resource:///modules/search_im.js");
-              tabmail.openTab("glodaFacet", {
-                searcher: new GlodaIMSearcher(null, searchString)
-              });
-            }
-          } catch (e) {
-            logException(e);
-          }
-        ]]>
-        </body>
-      </method>
-      <method name="clearSearch">
-        <body><![CDATA[
-          this.value = "";
-        ]]></body>
-      </method>
-    </implementation>
-  </binding>
-
-</bindings>
--- a/mail/components/im/jar.mn
+++ b/mail/components/im/jar.mn
@@ -18,17 +18,16 @@ messenger.jar:
     content/messenger/chat/imAccountWizard.js            (content/imAccountWizard.js)
 *   content/messenger/chat/imContextMenu.js              (content/imContextMenu.js)
     content/messenger/chat/imStatusSelector.js           (content/imStatusSelector.js)
     content/messenger/chat/imbuddytooltip.xml            (content/imbuddytooltip.xml)
     content/messenger/chat/imcontact.xml                 (content/imcontact.xml)
 *   content/messenger/chat/imconversation.xml            (content/imconversation.xml)
     content/messenger/chat/imconv.xml                    (content/imconv.xml)
     content/messenger/chat/imgroup.xml                   (content/imgroup.xml)
-    content/messenger/chat/imsearch.xml                  (content/imsearch.xml)
 % skin messenger-messagestyles classic/1.0 %skin/classic/messenger/messages/
 	skin/classic/messenger/messages/Bitmaps/minus-hover.png       (messages/Bitmaps/minus-hover.png)
 	skin/classic/messenger/messages/Bitmaps/minus.png             (messages/Bitmaps/minus.png)
 	skin/classic/messenger/messages/Bitmaps/plus-hover.png        (messages/Bitmaps/plus-hover.png)
 	skin/classic/messenger/messages/Bitmaps/plus.png              (messages/Bitmaps/plus.png)
 	skin/classic/messenger/messages/Footer.html                   (messages/Footer.html)
 	skin/classic/messenger/messages/Incoming/Content.html         (messages/Incoming/Content.html)
 	skin/classic/messenger/messages/Incoming/Context.html         (messages/Incoming/Context.html)
--- a/mail/components/im/modules/index_im.js
+++ b/mail/components/im/modules/index_im.js
@@ -34,35 +34,39 @@
  *
  * ***** END LICENSE BLOCK ***** */
 
 const EXPORTED_SYMBOLS = [];
 
 const {classes: Cc, interfaces: Ci, utils: Cu, Constructor: CC} = Components;
 
 Cu.import("resource:///modules/imXPCOMUtils.jsm");
+Cu.import("resource:///modules/iteratorUtils.jsm");
 Cu.import("resource://gre/modules/FileUtils.jsm");
 Cu.import("resource://gre/modules/NetUtil.jsm");
 Cu.import("resource:///modules/gloda/public.js");
+Cu.import("resource:///modules/gloda/datamodel.js");
 Cu.import("resource:///modules/gloda/indexer.js");
 Cu.import("resource:///modules/imServices.jsm");
 
 const kCacheFileName = "indexedFiles.json";
 
 const FileInputStream = CC("@mozilla.org/network/file-input-stream;1",
                            "nsIFileInputStream",
                            "init");
 const ScriptableInputStream = CC("@mozilla.org/scriptableinputstream;1",
                                  "nsIScriptableInputStream",
                                  "init");
 
 XPCOMUtils.defineLazyGetter(this, "MailFolder", function()
   Cc["@mozilla.org/rdf/resource-factory;1?name=mailbox"].createInstance(Ci.nsIMsgFolder)
 );
 
+var gIMAccounts = {};
+
 function GlodaIMConversation(aTitle, aTime, aPath, aContent)
 {
   // grokNounItem from gloda.js puts automatically the values of all
   // JS properties in the jsonAttributes magic attribute, except if
   // they start with _, so we put the values in _-prefixed properties,
   // and have getters in the prototype.
   this._title = aTitle;
   this._time = aTime;
@@ -71,24 +75,64 @@ function GlodaIMConversation(aTitle, aTi
 }
 GlodaIMConversation.prototype = {
   get title() this._title,
   get time() this._time,
   get path() this._path,
   get content() this._content,
 
   // for glodaFacetBindings.xml compatibility (pretend we are a message object)
+  get account() {
+    let [protocol, username] = this._path.split("/", 2);
+
+    let cacheName = protocol + "/" + username;
+    if (cacheName in gIMAccounts)
+      return gIMAccounts[cacheName];
+
+    // Find the nsIIncomingServer for the current imIAccount.
+    let mgr = Components.classes["@mozilla.org/messenger/account-manager;1"]
+                        .getService(Ci.nsIMsgAccountManager);
+    for each (let account in fixIterator(mgr.accounts, Ci.nsIMsgAccount)) {
+      let incomingServer = account.incomingServer;
+      if (!incomingServer || incomingServer.type != "im")
+        continue;
+      let imAccount = incomingServer.wrappedJSObject.imAccount;
+      if (imAccount.protocol.normalizedName == protocol &&
+          imAccount.normalizedName == username)
+        return (gIMAccounts[cacheName] = new GlodaAccount(incomingServer));
+    }
+    // The IM conversation is probably for an account that no longer exists.
+    return null;
+  },
   get subject() this._title,
   get date() new Date(this._time * 1000),
-  get recipient() null,
-  get from() ({value: "", contact: {name: ""}}),
+  get involves() Gloda.IGNORE_FACET,
+  _recipients: null,
+  get recipients() {
+    if (!this._recipients)
+      this._recipients = [{contact: {name: this._path.split("/", 2)[1]}}];
+    return this._recipients;
+  },
+  _from: null,
+  get from() {
+    if (!this._from) {
+      let from = "";
+      let account = this._account;
+      if (account)
+        from = account.incomingServer.wrappedJSObject.imAccount.protocol.name;
+      this._from = {value: "", contact: {name: from}};
+    }
+    return this._from;
+  },
+  get tags() [],
   get starred() false,
   get attachmentNames() null,
   get indexedBodyText() this._content,
   get read() true,
+  get folder() Gloda.IGNORE_FACET,
 
   // for glodaFacetView.js _removeDupes
   get headerMessageID() this.id
 };
 
 // FIXME
 var WidgetProvider = {
   providerName: "widget",
--- a/mail/locales/en-US/chrome/messenger/chat.dtd
+++ b/mail/locales/en-US/chrome/messenger/chat.dtd
@@ -27,25 +27,14 @@
 
 <!ENTITY openConversationButton.tooltip  "Start a conversation">
 <!ENTITY closeConversationButton.tooltip "Close conversation">
 
 <!ENTITY addBuddyButton.label          "Add Contact">
 <!ENTITY joinChatButton.label          "Join Chat">
 <!ENTITY chatAccountsButton.label      "Show Accounts">
 
-<!-- LOCALIZATION NOTE (searchAllChatMessages.label.base):
-     This is the base of the empty text for the chat search box.  We replace
-     #1 with the contents of the appropriate search.keyLabel.* value for the
-     platform (defined in messenger/messenger.dtd).
-     The goal is to convey to the user that typing in the box will allow them
-     to search for conversations and that there is a hotkey they can press
-     to get to the box faster.  If the global indexer is disabled, the search
-     box will be collapsed and the user will never see this message.
-     -->
-<!ENTITY searchAllChatMessages.label.base "Search all conversations… #1">
-
 <!ENTITY status.available          "Available">
 <!ENTITY status.unavailable        "Unavailable">
 <!ENTITY status.offline            "Offline">
 
 <!ENTITY openLinkCmd.label            "Open Link…">
 <!ENTITY openLinkCmd.accesskey        "O">
--- a/mail/locales/en-US/chrome/messenger/glodaFacetView.properties
+++ b/mail/locales/en-US/chrome/messenger/glodaFacetView.properties
@@ -166,24 +166,23 @@ glodaFacetView.results.header.countLabel
 glodaFacetView.results.header.countLabel.ofN=of #1;of #1
 # LOCALIZATION NOTE(glodaFacetView.results.header.countLabel.grouping):
 #  Combines the pluralized
 #  "glodaFacetView.results.header.countLabel.NMessages" string (as #1) with
 #  the pluralized "glodaFacetView.results.header.countLabel.ofN" (as #2)
 #  to make a single label.
 glodaFacetView.results.header.countLabel.grouping=#1 #2
 
-# LOCALIZATION NOTE(glodaFacetView.results.message.openAsList.label): The
-#  label for the button/link that causes us to display all of the messages in
-#  the active set in a new thread pane display tab, closing the current faceting
-#  tab.
-glodaFacetView.results.message.openAsList.label=Open as list
-# LOCALIZATION NOTE(glodaFacetView.results.message.openAsList.tooltip): The
-#  tooltip to display when hovering over the openAsList label.
-glodaFacetView.results.message.openAsList.tooltip=Show all of the messages in the active set in a new tab, closing this tab.
+# LOCALIZATION NOTE(glodaFacetView.results.message.openEmailAsList.label): The
+#  label for the button/link that causes us to display all of the emails in
+#  the active set in a new thread pane display tab.
+glodaFacetView.results.message.openEmailAsList.label=Open email as list
+# LOCALIZATION NOTE(glodaFacetView.results.message.openEmailAsList.tooltip):
+#  The tooltip to display when hovering over the openEmailAsList label.
+glodaFacetView.results.message.openEmailAsList.tooltip=Show all of the email messages in the active set in a new tab.
 
 # LOCALIZATION NOTE(glodaFacetView.results.message.sort.label): The
 #  label next to the choice of sort order
 glodaFacetView.results.message.sort.label=sort by:
 # LOCALIZATION NOTE(glodaFacetView.results.message.sort.relevance):
 # a clickable label causing the sort to be done by most relevant messages first.
 glodaFacetView.results.message.sort.relevance=relevance
 # LOCALIZATION NOTE(glodaFacetView.results.message.sort.date):
--- a/mailnews/db/gloda/modules/datamodel.js
+++ b/mailnews/db/gloda/modules/datamodel.js
@@ -30,17 +30,17 @@
  * use your version of this file under the terms of the MPL, indicate your
  * decision by deleting the provisions above and replace them with the notice
  * and other provisions required by the GPL or the LGPL. If you do not delete
  * the provisions above, a recipient may use your version of this file under
  * the terms of any one of the MPL, the GPL or the LGPL.
  *
  * ***** END LICENSE BLOCK ***** */
 
-const EXPORTED_SYMBOLS = ["GlodaAttributeDBDef",
+const EXPORTED_SYMBOLS = ["GlodaAttributeDBDef", "GlodaAccount",
                     "GlodaConversation", "GlodaFolder", "GlodaMessage",
                     "GlodaContact", "GlodaIdentity", "GlodaAttachment"];
 
 const Cc = Components.classes;
 const Ci = Components.interfaces;
 const Cr = Components.results;
 const Cu = Components.utils;
 
@@ -190,16 +190,37 @@ function MixIn(aConstructor, aMixIn) {
     if (name.substring(0, 4) == "get_")
       proto.__defineGetter__(name.substring(4), func);
     else
       proto[name] = func;
   }
 }
 
 /**
+ * @class A gloda wrapper around nsIMsgIncomingServer.
+ */
+function GlodaAccount(aIncomingServer) {
+  this._incomingServer = aIncomingServer;
+}
+
+GlodaAccount.prototype = {
+  NOUN_ID: 106,
+  get id() { return this._incomingServer.key; },
+  get name() { return this._incomingServer.prettyName; },
+  get incomingServer() { return this._incomingServer; },
+  toString: function gloda_account_toString() {
+    return "Account: " + this.id;
+  },
+
+  toLocaleString: function gloda_account_toLocaleString() {
+    return this.name;
+  }
+};
+
+/**
  * @class A gloda conversation (thread) exists so that messages can belong.
  */
 function GlodaConversation(aDatastore, aID, aSubject, aOldestMessageDate,
                            aNewestMessageDate) {
   // _datastore is now set on the prototype by GlodaDatastore
   this._id = aID;
   this._subject = aSubject;
   this._oldestMessageDate = aOldestMessageDate;
@@ -234,16 +255,17 @@ GlodaConversation.prototype = {
 function GlodaFolder(aDatastore, aID, aURI, aDirtyStatus, aPrettyName,
                      aIndexingPriority) {
   // _datastore is now set by GlodaDatastore
   this._id = aID;
   this._uri = aURI;
   this._dirtyStatus = aDirtyStatus;
   this._prettyName = aPrettyName;
   this._xpcomFolder = null;
+  this._account = null;
   this._activeIndexing = false;
   this._activeHeaderRetrievalLastStamp = 0;
   this._indexingPriority = aIndexingPriority;
   this._deleted = false;
   this._compacting = false;
 }
 
 GlodaFolder.prototype = {
@@ -429,16 +451,29 @@ GlodaFolder.prototype = {
         // we don't have to do anything here.
         break;
     }
 
     return this._xpcomFolder;
   },
 
   /**
+   * Retrieve a GlodaAccount instance corresponding to this folder.
+   *
+   * @return The GlodaAccount instance.
+   */
+  getAccount: function gloda_folder_getAccount() {
+    if (!this._account) {
+      let msgFolder = this.getXPCOMFolder(this.kActivityFolderOnlyNoData);
+      this._account = new GlodaAccount(msgFolder.server);
+    }
+    return this._account;
+  },
+
+  /**
    * How many milliseconds must a folder have not had any header retrieval
    *  activity before it's okay to lose the database reference?
    */
   ACCEPTABLY_OLD_THRESHOLD: 10000,
 
   /**
    * Cleans up our nsIMsgFolder reference if we have one and it's not "in use".
    * In use, from our perspective, means that it is not being used for indexing
@@ -475,24 +510,16 @@ GlodaFolder.prototype = {
       this._xpcomFolder.msgDatabase = null;
       this._xpcomFolder = null;
       // since the last retrieval time tracks whether we have marked live or
       //  not, this needs to be reset to 0 too.
       this._activeHeaderRetrievalLastStamp = 0;
     }
 
     return true;
-  },
-
-  /**
-   * Return the string associated with this account.
-   */
-  get accountLabel() {
-    let msgFolder = this.getXPCOMFolder(this.kActivityFolderOnlyNoData);
-    return msgFolder.server.prettyName;
   }
 };
 
 /**
  * @class A message representation.
  */
 function GlodaMessage(aDatastore, aID, aFolderID, aMessageKey,
                       aConversationID, aConversation, aDate,
@@ -556,16 +583,29 @@ GlodaMessage.prototype = {
     try {
       if (this._folderID != null)
         return this._datastore._mapFolderID(this._folderID).uri;
     }
     catch (ex) {
     }
     return null;
   },
+  get account() {
+    // XXX due to a deletion bug it is currently possible to get in a state
+    //  where we have an illegal folderID value.  This will result in an
+    //  exception.  As a workaround, let's just return null in that case.
+    try {
+      if (this._folderID == null)
+        return null;
+      let folder = this._datastore._mapFolderID(this._folderID);
+      return folder.getAccount();
+    }
+    catch (ex) { }
+    return null;
+  },
   get conversation() {
     return this._conversation;
   },
 
   toString: function gloda_message_toString() {
     // uh, this is a tough one...
     return "Message:" + this._id;
   },
--- a/mailnews/db/gloda/modules/datastore.js
+++ b/mailnews/db/gloda/modules/datastore.js
@@ -712,16 +712,17 @@ var GlodaDatastore = {
 
   /* see Gloda's documentation for these constants */
   kSpecialNotAtAll: 0,
   kSpecialColumn: 16,
   kSpecialColumnChildren: 16|1,
   kSpecialColumnParent: 16|2,
   kSpecialString: 32,
   kSpecialFulltext: 64,
+  IGNORE_FACET: {},
 
   kConstraintIdIn: 0,
   kConstraintIn: 1,
   kConstraintRanges: 2,
   kConstraintEquals: 3,
   kConstraintStringLike: 4,
   kConstraintFulltext: 5,
 
--- a/mailnews/db/gloda/modules/facet.js
+++ b/mailnews/db/gloda/modules/facet.js
@@ -211,16 +211,18 @@ DiscreteFaceter.prototype = {
     let filter = this.facetDef.filter;
 
     let valStrToVal = {};
     let groups = this.groups = {};
     this.groupCount = 0;
 
     for each (let [, item] in Iterator(aItems)) {
       let val = (attrKey in item) ? item[attrKey] : null;
+      if (val === Gloda.IGNORE_FACET)
+        continue;
 
       // skip items the filter tells us to ignore
       if (filter && !filter(val))
         continue;
 
       // We need to use hasOwnProperty because we cannot guarantee that the
       //  contents of val won't collide with the attributes in Object.prototype.
       if (groups.hasOwnProperty(val))
@@ -253,16 +255,18 @@ DiscreteFaceter.prototype = {
     let idAttr = this.facetDef.groupIdAttr;
 
     let groups = this.groups = {};
     let groupMap = this.groupMap = {};
     this.groupCount = 0;
 
     for each (let [, item] in Iterator(aItems)) {
       let val = (attrKey in item) ? item[attrKey] : null;
+      if (val === Gloda.IGNORE_FACET)
+        continue;
 
       // skip items the filter tells us to ignore
       if (filter && !filter(val))
         continue;
 
       let valId = (val == null) ? null : val[idAttr];
       // We need to use hasOwnProperty because tag nouns are complex objects
       //  with id's that are non-numeric and so can collide with the contents
@@ -322,16 +326,19 @@ DiscreteSetFaceter.prototype = {
     let filter = this.facetDef.filter;
 
     let groups = this.groups = {};
     let valStrToVal = {};
     this.groupCount = 0;
 
     for each (let [, item] in Iterator(aItems)) {
       let vals = (attrKey in item) ? item[attrKey] : null;
+      if (vals === Gloda.IGNORE_FACET)
+        continue;
+
       if (vals == null || vals.length == 0) {
         vals = [null];
       }
       for each (let [, val] in Iterator(vals)) {
         // skip items the filter tells us to ignore
         if (filter && !filter(val))
           continue;
 
@@ -369,16 +376,19 @@ DiscreteSetFaceter.prototype = {
     let idAttr = this.facetDef.groupIdAttr;
 
     let groups = this.groups = {};
     let groupMap = this.groupMap = {};
     this.groupCount = 0;
 
     for each (let [, item] in Iterator(aItems)) {
       let vals = (attrKey in item) ? item[attrKey] : null;
+      if (vals === Gloda.IGNORE_FACET)
+        continue;
+
       if (vals == null || vals.length == 0) {
         vals = [null];
       }
       for each (let [, val] in Iterator(vals)) {
         // skip items the filter tells us to ignore
         if (filter && !filter(val))
           continue;
 
--- a/mailnews/db/gloda/modules/fundattr.js
+++ b/mailnews/db/gloda/modules/fundattr.js
@@ -114,39 +114,33 @@ var GlodaFundAttr = {
     // folder
     this._attrFolder = Gloda.defineAttribute({
       provider: this,
       extensionName: Gloda.BUILT_IN,
       attributeType: Gloda.kAttrFundamental,
       attributeName: "folder",
       singular: true,
       facet: true,
-      extraFacets: [
-        {
-          type: "default",
-          alias: "account",
-          // Group the folders by their account (label)...
-          groupIdAttr: "accountLabel",
-          // sort the groups by string using magic convenience value
-          groupComparator: function(a, b) {
-            return a.accountLabel.localeCompare(b.accountLabel);
-          },
-          queryHelper: "Account",
-          // Display the account label for the facet
-          labelFunc: function(aGlodaFolder) {
-            return aGlodaFolder.accountLabel;
-          }
-        },
-      ],
       special: Gloda.kSpecialColumn,
       specialColumnName: "folderID",
       subjectNouns: [Gloda.NOUN_MESSAGE],
       objectNoun: Gloda.NOUN_FOLDER,
       }); // tested-by: test_attributes_fundamental
-    this._attrFolder = Gloda.defineAttribute({
+    this._attrAccount = Gloda.defineAttribute({
+      provider: this,
+      extensionName: Gloda.BUILT_IN,
+      attributeType: Gloda.kAttrDerived,
+      attributeName: "account",
+      canQuery: "memory",
+      singular: true,
+      facet: true,
+      subjectNouns: [Gloda.NOUN_MESSAGE],
+      objectNoun: Gloda.NOUN_ACCOUNT
+      });
+    this._attrMessageKey = Gloda.defineAttribute({
       provider: this,
       extensionName: Gloda.BUILT_IN,
       attributeType: Gloda.kAttrFundamental,
       attributeName: "messageKey",
       singular: true,
       special: Gloda.kSpecialColumn,
       specialColumnName: "messageKey",
       subjectNouns: [Gloda.NOUN_MESSAGE],
--- a/mailnews/db/gloda/modules/gloda.js
+++ b/mailnews/db/gloda/modules/gloda.js
@@ -691,16 +691,21 @@ var Gloda = {
   kSpecialFulltext: GlodaDatastore.kSpecialFulltext,
 
   /**
    * The extensionName used for the attributes defined by core gloda plugins
    *  such as fundattr.js and explattr.js.
    */
   BUILT_IN: "built-in",
 
+  /**
+   * Special sentinel value that will cause facets to skip a noun instance
+   * when an attribute has this value.
+   */
+  IGNORE_FACET: GlodaDatastore.IGNORE_FACET,
 
   /*
    * The following are explicit noun IDs.  While most extension-provided nouns
    *  will have dynamically allocated id's that are looked up by name, these
    *  id's can be relied upon to exist and be accessible via these
    *  pseudo-constants.  It's not really clear that we need these, although it
    *  does potentially simplify code to not have to look up all of their nouns
    *  at initialization time.
@@ -790,16 +795,20 @@ var Gloda = {
    *  identities.  See datamodel.js for the definition of the GlodaIdentity
    *  class.
    */
   NOUN_IDENTITY: GlodaIdentity.prototype.NOUN_ID, // 104
   /**
    * An attachment to a message. A message may have many different attachments.
    */
   NOUN_ATTACHMENT: GlodaAttachment.prototype.NOUN_ID, // 105
+  /**
+   * An account related to a message. A message can have only one account.
+   */
+  NOUN_ACCOUNT: GlodaAccount.prototype.NOUN_ID, // 106
 
   /**
    * Parameterized identities, for use in the from-me, to-me, cc-me optimization
    *  cases.  Not for reuse without some thought.  These nouns use the parameter
    *  to store the 'me' identity that we are talking about, and the value to
    *  store the identity of the other party.  So in both the from-me and to-me
    *  cases involving 'me' and 'foo@bar', the 'me' identity is always stored via
    *  the attribute parameter, and the 'foo@bar' identity is always stored as
@@ -1236,16 +1245,40 @@ var Gloda = {
       },
       toParamAndValue: function(aFolderOrGlodaFolder) {
         if (aFolderOrGlodaFolder instanceof GlodaFolder)
           return [null, aFolderOrGlodaFolder.id];
         else
           return [null, GlodaDatastore._mapFolder(aFolderOrGlodaFolder).id];
       }}, this.NOUN_FOLDER);
     this.defineNoun({
+      name: "account",
+      clazz: GlodaAccount,
+      allowsArbitraryAttrs: false,
+      isPrimitive: false,
+      equals: function(a, b) {
+        if (a && !b || !a && b)
+          return false;
+        if (!a && !b)
+          return true;
+        return a.id == b.id;
+      },
+      comparator: function gloda_account_comparator(a, b) {
+        if (a == null) {
+          if (b == null)
+            return 0;
+          else
+            return 1;
+        }
+        else if (b == null) {
+          return -1;
+        }
+        return a.name.localeCompare(b.name);
+      }}, this.NOUN_ACCOUNT);
+    this.defineNoun({
       name: "conversation",
       clazz: GlodaConversation,
       allowsArbitraryAttrs: false,
       isPrimitive: false,
       cache: true, cacheCost: 512,
       tableName: "conversations",
       attrTableName: "messageAttributes", attrIDColumnName: "conversationID",
       datastore: GlodaDatastore,
@@ -2056,19 +2089,21 @@ var Gloda = {
 
       // 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) {
+      // the new canQuery property has to be set to true to generate entries
+      // in the messageAttributes table. Any other truthy value (like a non
+      // empty string), will still make the message query-able but without
+      // using the database.
+      if (attrib.canQuery !== true) {
         continue;
       }
 
       // - database index attributes
 
       // perform a delta analysis against the old value, if we have one
       if (oldValue !== undefined) {
         // in the singular case if they don't match, it's one add and one remove
@@ -2163,17 +2198,17 @@ var Gloda = {
       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) {
+      if (attrib.canQuery !== true) {
         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;
--- a/mailnews/db/gloda/modules/query.js
+++ b/mailnews/db/gloda/modules/query.js
@@ -173,16 +173,22 @@ GlodaQueryClass.prototype = {
 
       // assume success until a specific (or) constraint proves us wrong
       let querySatisfied = true;
       for (let iConstraint = 0; iConstraint < curQuery._constraints.length;
            iConstraint++) {
         let constraint = curQuery._constraints[iConstraint];
         let [constraintType, attrDef] = constraint;
         let boundName = attrDef ? attrDef.boundName : "id";
+        if ((boundName in aObj) &&
+            aObj[boundName] === GlodaDatastore.IGNORE_FACET) {
+          querySatisfied = false;
+          break;
+        }
+
         let constraintValues = constraint.slice(2);
 
         if (constraintType === GlodaDatastore.kConstraintIdIn) {
           if (constraintValues.indexOf(aObj.id) == -1) {
             querySatisfied = false;
             break;
           }
         }