status commit on refactor, pre-merge.
authorAndrew Sutherland <asutherland@asutherland.org>
Fri, 17 Oct 2008 23:58:21 -0700
changeset 975 d8efe76d7bf37411412cb0d654e377d36f67253b
parent 974 06fabe695ede97f7960935c4515e2e867a3eefec
child 976 7c775651aae3eae5df5106c3e8b09a9d311d7e6e
push idunknown
push userunknown
push dateunknown
status commit on refactor, pre-merge.
components/glautocomp.js
components/jsmimeemitter.js
content/glodacomplete.xml
modules/collection.js
modules/datamodel.js
modules/datastore.js
modules/explattr.js
modules/fundattr.js
modules/gloda.js
modules/query.js
--- a/components/glautocomp.js
+++ b/components/glautocomp.js
@@ -47,28 +47,28 @@ var LOG = null;
 
 var Gloda = null;
 var GlodaUtils = null;
 var MultiSuffixTree = null;
 var FreeTagNoun = null;
 
 function ResultRowSingle(aItem, aCriteriaType, aCriteria) {
   this.nounID = aItem.NOUN_ID;
-  this.nounMeta = Gloda._nounIDToMeta[this.nounID];
+  this.nounDef = Gloda._nounIDToDef[this.nounID];
   this.criteriaType = aCriteriaType;
   this.criteria = aCriteria;
   this.item = aItem;
 }
 ResultRowSingle.prototype = {
   multi: false
 };
 
 function ResultRowMulti(aNounID, aCriteriaType, aCriteria, aQuery) {
   this.nounID = aNounID;
-  this.nounMeta = Gloda._nounIDToMeta[aNounID];
+  this.nounDef = Gloda._nounIDToDef[aNounID];
   this.criteriaType = aCriteriaType;
   this.criteria = aCriteria;
   this.collection = aQuery.getCollection(this);
   this.renderer = null;
 }
 ResultRowMulti.prototype = {
   multi: true,
   onItemsAdded: function(aItems) {
@@ -153,17 +153,17 @@ nsAutoCompleteGlodaResult.prototype = {
       return thing.name || thing.subject;
   },
   // rich uses this to be the "type"
   getStyleAt: function(aIndex) {
     let row = this._results[aIndex];
     if (row.multi)
       return "gloda-multi";
     else
-      return "gloda-single-" + row.nounMeta.name;
+      return "gloda-single-" + row.nounDef.name;
   },
   // rich uses this to be the icon
   getImageAt: function(aIndex) {
     let thing = this._results[aIndex];
     if (!thing.value)
       return null;
 
     let md5hash = GlodaUtils.md5HashString(thing.value);
--- a/components/jsmimeemitter.js
+++ b/components/jsmimeemitter.js
@@ -84,102 +84,101 @@ function MimeMessageEmitter() {
   this._curMsg = null;
   
   this._messageIndex = 0;
   this._allSubMessages = [];
 }
 
 MimeMessageEmitter.prototype = {
   classDescription: "JS Mime Message Emitter",
-  classID: Components.ID("{80578315-7021-40f9-9717-413cacf2fa7d}"),
-  contractID: "@mozilla.org/steeldestined/jsmimeemitter;1",
+  classID: Components.ID("{8cddbbbc-7ced-46b0-a936-8cddd1928c24}"),
+  contractID: "@mozilla.org/gloda/jsmimeemitter;1",
   
   _partRE: new RegExp("^[^?]+\?(?:[^&]+&)*part=([^&]+)(?:&[^&]+)*$"),
   
   _xpcom_categories: [{
     category: "mime-emitter",
     entry:
       "@mozilla.org/messenger/mimeemitter;1?type=application/x-js-mime-message",
   }],
   
   QueryInterface: XPCOMUtils.generateQI([Ci.nsIMimeEmitter]),
 
-  Initialize: function mime_emitter_Initialize(aUrl, aChannel, aFormat) {
+  initialize: function mime_emitter_initialize(aUrl, aChannel, aFormat) {
     this._url = aUrl;
     this._curMsg = this._parentMsg = this._rootMsg = new this._mimeMsg.MimeMessage();
     
     this._mimeMsg.MsgHdrToMimeMessage.RESULT_RENDEVOUZ[aUrl.spec] =
       this._rootMsg;
     
     this._channel = aChannel;
   },
   
-  Complete: function mime_emitter_Complete() {
+  complete: function mime_emitter_complete() {
     // null out everything we can.  secretive cycles are eating us alive.
     this._url = null;
     this._channel = null;
     
     this._inputStream = null;
     this._outputStream = null;
     
     this._outputListener = null;
 
     this._curMsg = this._parentMsg = this._messageStack = this._rootMsg = null;
     this._messageIndex = null;
     this._allSubMessages = null;
   },
   
-  SetPipe: function mime_emitter_SetPipe(aInputStream, aOutputStream) {
+  setPipe: function mime_emitter_setPipe(aInputStream, aOutputStream) {
     this._inputStream = aInputStream;
     this._outputStream = aOutputStream;
   },
-  // can we use getters/setters to replace explicit functions on the interface?
-  SetOutputListener: function mime_emitter_SetOutputListener(aListener) {
+  set outputListener(aListener) {
     this._outputListener = aListener;
   },
-  GetOutputListener: function mime_emitter_GetOutputListener() {
+  get outputListener() {
     return this._outputListener;
   }, 
   
   // ----- Header Routines
-  StartHeader: function mime_emitter_StartHeader(aIsRootMailHeader,
+  startHeader: function mime_emitter_startHeader(aIsRootMailHeader,
       aIsHeaderOnly, aMsgID, aOutputCharset) {
     
     if (aIsRootMailHeader) {
-      this.UpdateCharacterSet(aOutputCharset);
+      this.updateCharacterSet(aOutputCharset);
       // nothing to do curMsg-wise, already initialized.
     }
     else {
       this._curMsg = new this._mimeMsg.MimeMessage();
       this._parentMsg.messages.push(this._curMsg);
       this._allSubMessages.push(this._curMsg);
     }
   },
-  AddHeaderField: function mime_emitter_AddHeaderField(aField, aValue) {
+  addHeaderField: function mime_emitter_addHeaderField(aField, aValue) {
     let lowerField = aField.toLowerCase();
     if (lowerField in this._curMsg.headers)
       this._curMsg.headers[lowerField].push(aValue);
     else
       this._curMsg.headers[lowerField] = [aValue];
   },
   addAllHeaders: function mime_emitter_addAllHeaders(aAllHeaders, aHeaderSize) {
     // This is called by the parsing code after the calls to AddHeaderField (or
     //  AddAttachmentField if the part is an attachment), and seems to serve
     //  a specialized, quasi-redundant purpose.  (nsMimeBaseEmitter creates a
     //  nsIMimeHeaders instance and hands it to the nsIMsgMailNewsUrl.)
     // nop
   },
-  WriteHTMLHeaders: function mime_emitter_WriteHTMLHeaders() {
+  writeHTMLHeaders: function mime_emitter_writeHTMLHeaders() {
     // It does't look like this should even be part of the interface; I think
     //  only the nsMimeHtmlDisplayEmitter::EndHeader call calls this signature.
     // nop
   },
-  EndHeader: function mime_emitter_EndHeader() {
+  endHeader: function mime_emitter_endHeader() {
   },
-  UpdateCharacterSet: function mime_emitter_UpdateCharacterSet(aCharset) {
+  updateCharacterSet: function mime_emitter_updateCharacterSet(aCharset) {
     // for non US-ASCII, ISO-8859-1, or UTF-8 charsets (case-insensitive),
     //  nsMimeBaseEmitter grabs the channel's content type, nukes the "charset="
     //  parameter if it exists, and tells the channel the updated content type
     //  and new character set.
     
     // Disabling for now; we get a NS_ERROR_NOT_IMPLEMENTED from the channel
     //  when we try and set the contentCharset... and I'm not totally up on the
     //  intent of why we were doing this in the first place.
@@ -241,17 +240,17 @@ MimeMessageEmitter.prototype = {
     }
   },
   
   // ----- Attachment Routines
   // The attachment processing happens after the initial streaming phase (during
   //  which time we receive the messages, both bodies and headers).  Our caller
   //  traverses the libmime child object hierarchy, emitting an attachment for
   //  each leaf object or sub-message.
-  StartAttachment: function mime_emitter_StartAttachment(aName, aContentType,
+  startAttachment: function mime_emitter_startAttachment(aName, aContentType,
       aUrl, aNotDownloaded) {
     
     // we need to strip our magic flags from the URL
     aURl = aUrl.replace("header=filter&emitter=js&", "");
     
     // the url should contain a part= piece that tells us the part name, which
     //  we then use to figure out where.
     let partMatch = this._partRE.exec(aUrl);
@@ -268,53 +267,53 @@ MimeMessageEmitter.prototype = {
       // create the attachment
       part = new this._mimeMsg.MimeMessageAttachment(partName,
           aName, aContentType, aUrl, aNotDownloaded);
     }
     
     this._putPart(part.partName.substring(2), "1",
                   part, this._rootMsg);
   },
-  AddAttachmentField: function mime_emitter_AddAttachmentField(aField, aValue) {
+  addAttachmentField: function mime_emitter_addAttachmentField(aField, aValue) {
     // this only gives us X-Mozilla-PartURL, which is the same as aUrl we
     //  already got previously, so need to do anything with this.
   },
-  EndAttachment: function mime_emitter_EndAttachment() {
+  endAttachment: function mime_emitter_endAttachment() {
     // don't need to do anything here, since we don't care about the headers.
   },
-  EndAllAttachments: function mime_emitter_EndAllAttachments() {
+  endAllAttachments: function mime_emitter_endAllAttachments() {
     // nop
   },
   
   // ----- Body Routines
-  StartBody: function mime_emitter_StartBody(aIsBodyOnly, aMsgID, aOutCharset) {
+  startBody: function mime_emitter_startBody(aIsBodyOnly, aMsgID, aOutCharset) {
     this._messageStack.push(this._curMsg);
     this._parentMsg = this._curMsg;
   },
   
-  WriteBody: function mime_emitter_WriteBody(aBuf, aSize, aOutAmountWritten) {
+  writeBody: function mime_emitter_writeBody(aBuf, aSize, aOutAmountWritten) {
     this._curMsg.body += aBuf;
   },
   
-  EndBody: function mime_emitter_EndBody() {
+  endBody: function mime_emitter_endBody() {
     this._messageStack.pop();
     this._parentMsg = this._messageStack[this._messageStack.length - 1];
   },
   
   // ----- Generic Write (confusing)
   // (binary data writing...)
-  Write: function mime_emitter_Write(aBuf, aSize, aOutAmountWritten) {
+  write: function mime_emitter_write(aBuf, aSize, aOutAmountWritten) {
     // we don't actually ever get called because we don't have the attachment
     //  binary payloads pass through us, but we do the following just in case
     //  we did get called (otherwise the caller gets mad and throws exceptions).
     aOutAmountWritten.value = aSize;
   },
   
   // (string writing)
-  UtilityWrite: function mime_emitter_UtilityWrite(aBuf) {
-    this.Write(aBuf, aBuf.length, {});
+  utilityWrite: function mime_emitter_utilityWrite(aBuf) {
+    this.write(aBuf, aBuf.length, {});
   },
 };
 
 var components = [MimeMessageEmitter];
 function NSGetModule(compMgr, fileSpec) {
   return XPCOMUtils.generateModule(components);
 }
--- a/content/glodacomplete.xml
+++ b/content/glodacomplete.xml
@@ -553,34 +553,34 @@
         <parameter name="aObj"/>
         <body>
           var node = document.createElementNS(
             "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul",
             "richlistitem");
           
           node.obj = aObj;
           node.setAttribute("type",
-                            "gloda-" + this.row.nounMeta.name + "-chunk");
+                            "gloda-" + this.row.nounDef.name + "-chunk");
           
           this._identityHolder.appendChild(node);
         </body>
       </method>
       
       <method name="_adjustAcItem">
         <body>
           <![CDATA[
           // clear out any lingering children
           while (this._identityHolder.hasChildNodes())
             this._identityHolder.removeChild(this._identityHolder.firstChild);
           
           var row = this.row;
           if (row == null)
             return;
           
-          this._explanation.value = row.nounMeta.name + "s " +
+          this._explanation.value = row.nounDef.name + "s " +
             row.criteriaType + "ed " + row.criteria;
           
           // render anyone already in there
           for each (let item in row.collection.items) {
             this.renderItem(item);
           }
           // listen up, yo.
           row.renderer = this;
--- a/modules/collection.js
+++ b/modules/collection.js
@@ -66,17 +66,17 @@ var GlodaCollectionManager = {
 
   /**
    * Registers the existence of a collection with the collection manager.  This
    *  is done using a weak reference so that the collection can go away if it
    *  wants to.
    */
   registerCollection: function gloda_colm_registerCollection(aCollection) {
     let collections;
-    let nounID = aCollection.query._nounMeta.id;
+    let nounID = aCollection.query._nounDef.id;
     if (!(nounID in this._collectionsByNoun))
       collections = this._collectionsByNoun[nounID] = [];
     else {
       // purge dead weak references while we're at it
       collections = this._collectionsByNoun[nounID].filter(function (aRef) {
         return aRef.get(); });
       this._collectionsByNoun[nounID] = collections;
     }
@@ -94,18 +94,18 @@ var GlodaCollectionManager = {
     for (let iColl = 0; iColl < weakCollections.length; iColl++) {
       let collection = weakCollections[iColl].get();
       if (collection)
         collections.push(collection);
     }
     return collections;
   },
   
-  defineCache: function gloda_colm_defineCache(aNounMeta, aCacheSize) {
-    this._cachesByNoun[aNounMeta.id] = new GlodaLRUCacheCollection(aNounMeta,
+  defineCache: function gloda_colm_defineCache(aNounDef, aCacheSize) {
+    this._cachesByNoun[aNounDef.id] = new GlodaLRUCacheCollection(aNounDef,
                                                                    aCacheSize);
   },
   
   /**
    * Attempt to locate an instance of the object of the given noun type with the
    *  given id.  Counts as a cache hit if found.  (And if it was't in a cache,
    *  but rather a collection, it is added to the cache.)
    */
@@ -376,24 +376,24 @@ var GlodaCollectionManager = {
  * @class A current view of the set of first-class nouns meeting a given query.
  *  Assuming a listener is present, events are
  *  generated when new objects meet the query, existing objects no longer meet
  *  the query, or existing objects have experienced a change in attributes that
  *  does not affect their ability to be present (but the listener may care about
  *  because it is exposing those attributes).
  * @constructor 
  */
-function GlodaCollection(aNounMeta, aItems, aQuery, aListener) {
-  // if aNounMeta is null, we are just being invoked for subclassing
-  if (aNounMeta === undefined)
+function GlodaCollection(aNounDef, aItems, aQuery, aListener) {
+  // if aNounDef is null, we are just being invoked for subclassing
+  if (aNounDef === undefined)
     return;
 
-  this._nounMeta = aNounMeta;
+  this._nounDef = aNounDef;
   // should we also maintain a unique value mapping...
-  if (this._nounMeta.usesUniqueValue)
+  if (this._nounDef.usesUniqueValue)
     this._uniqueValueMap = {};
 
   this.items = [];
   this._idMap = {};
   
   // force the listener to null for our call to _onItemsAdded; no events for
   //  the initial load-out.
   this._listener = null;
@@ -481,18 +481,18 @@ GlodaCollection.prototype = {
       this._listener.onQueryCompleted(this);
   }
 };
 
 /**
  * Create an LRU cache collection for the given noun with the given size.
  * @constructor
  */
-function GlodaLRUCacheCollection(aNounMeta, aCacheSize) {
-  GlodaCollection.call(this, aNounMeta, null, null, null);
+function GlodaLRUCacheCollection(aNounDef, aCacheSize) {
+  GlodaCollection.call(this, aNounDef, null, null, null);
   
   this._head = null; // aka oldest!
   this._tail = null; // aka newest!
   this._size = 0;
   // let's keep things sane, and simplify our logic a little...
   if (aCacheSize < 32)
     aCacheSize = 32;
   this._maxCacheSize = aCacheSize;
@@ -543,17 +543,17 @@ GlodaLRUCacheCollection.prototype.add = 
     // nuke from our id map
     delete this._idMap[item.id];
     if (this._uniqueValueMap)
       delete this._uniqueValueMap[item.uniqueValue];
     
     // flush dirty items to disk (they may not have this attribute, in which
     //  case, this returns false, which is fine.)
     if (item.dirty) {
-      this._nounMeta.objUpdate.call(this._nounMeta.datastore, item);
+      this._nounDef.objUpdate.call(this._nounDef.datastore, item);
       delete item.dirty;
     }
     
     this._size--;
   }
 };
 
 GlodaLRUCacheCollection.prototype.hit = function cache_hit(aItem) {
@@ -603,19 +603,19 @@ GlodaLRUCacheCollection.prototype.delete
 }
 
 /**
  * If any of the cached items are dirty, commit them, and make them no longer
  *  dirty.
  */
 GlodaLRUCacheCollection.prototype.commitDirty = function cache_commitDirty() {
   // we can only do this if there is an update method available...
-  if (!this._nounMeta.objUpdate)
+  if (!this._nounDef.objUpdate)
     return;
 
   for each (let [iItem, item] in Iterator(this._idMap)) {
     if (item.dirty) {
       LOG.debug("flushing dirty: " + item);
-      this._nounMeta.objUpdate.call(this._nounMeta.datastore, item);
+      this._nounDef.objUpdate.call(this._nounDef.datastore, item);
       delete item.dirty;
     }
   }
 }
--- a/modules/datamodel.js
+++ b/modules/datamodel.js
@@ -45,58 +45,44 @@ const Cr = Components.results;
 const Cu = Components.utils;
 
 Cu.import("resource://gloda/modules/log4moz.js");
 const LOG = Log4Moz.Service.getLogger("gloda.datamodel");
 
 Cu.import("resource://gloda/modules/utils.js");
 
 /**
- * @class Represents a gloda attribute definition.
+ * @class Represents a gloda attribute definition's DB form.  This class
+ *  stores the information in the database relating to this attribute
+ *  definition.  Access its attrDef attribute to get at the realy juicy data.
+ *  This main interesting thing this class does is serve as the keeper of the
+ *  mapping from parameters to attribute ids in the database if this is a 
+ *  parameterized attribute.
  */
-function GlodaAttributeDef(aDatastore, aID, aCompoundName, aProvider, aAttrType,
-                           aPluginName, aAttrName, aSubjectTypes,
-                           aObjectType, aObjectNounDef) {
+function GlodaAttributeDBDef(aDatastore, aID, aCompoundName, aAttrType,
+                           aPluginName, aAttrName) {
   this._datastore = aDatastore;
   this._id = aID;
   this._compoundName = aCompoundName;
-  this._provider = aProvider;
   this._attrType = aAttrType;
   this._pluginName = aPluginName;
   this._attrName = aAttrName;
-  this._subjectTypes = aSubjectTypes;
-  this._objectType = aObjectType;
-  this._objectNounDef = aObjectNounDef;
-
-  this.boundName = null;
-  this._singular = null;
-
-  this._special = 0; // not special
-  this._specialColumnName = null;
+  
+  this.attrDef = null;
 
   /** Map parameter values to the underlying database id. */
   this._parameterBindings = {};
 }
 
 GlodaAttributeDef.prototype = {
   get id() { return this._id; },
-  get provider() { return this._provider; },
   get attributeName() { return this._attrName; },
 
-  get objectNoun() { return this._objectType; },
-  get objectNounDef() { return this._objectNounDef; },
-
-  get isBound() { return this.boundName !== null; },
-  get singular() { return this._singular; },
-
-  get special() { return this._special; },
-  get specialColumnName() { return this._specialColumnName; },
+  get parameterBindings() { return this._parameterBindings; },
   
-  get parameterBindings() { return this._parameterBindings; },
-
   /**
    * Bind a parameter value to the attribute definition, allowing use of the
    *  attribute-parameter as an attribute.
    *
    * @return
    */
   bindParameter: function gloda_attr_bindParameter(aValue) {
     // people probably shouldn't call us with null, but handle it
--- a/modules/datastore.js
+++ b/modules/datastore.js
@@ -134,47 +134,47 @@ PostCommitHandler.prototype = {
     GlodaDatastore._asyncCompleted();
   }
 };
 
 /**
  * @class Handles the results from a GlodaDatastore.queryFromQuery call.
  * @constructor
  */
-function QueryFromQueryCallback(aStatement, aNounMeta, aCollection) {
+function QueryFromQueryCallback(aStatement, aNounDef, aCollection) {
   this.statement = aStatement;
-  this.nounMeta = aNounMeta;
+  this.nounDef = aNounDef;
   this.collection = aCollection;
   
   this.referencesByNounID = {};
 
   GlodaDatastore._pendingAsyncStatements++;
 }
 
 QueryFromQueryCallback.prototype = {
   handleResult: function gloda_ds_qfq_handleResult(aResultSet) {
     let newItems = [];
     let row;
-    let nounMeta = this.nounMeta;
+    let nounDef = this.nounDef;
     while (row = aResultSet.getNextRow()) {
-      let item = nounMeta.objFromRow.call(nounMeta.datastore, row);
+      let item = nounDef.objFromRow.call(nounDef.datastore, row);
       GlodaDatastore.loadNounItem(item, this.referencesByNounID);
       newItems.push(item);
     }
     // have the collection manager attempt to replace the instances we just
     //  created with pre-existing instances.  there is some waste here...
     // XXX consider having collection manager take row objects with the
     //  knowledge of what index is the 'id' index and knowing what objFromRow
     //  method to call if it needs to realize the row.
     // queries have the potential to easily exceed the size of our cache, and
     //  will cause needless churn if so.  as such, indicate that we never want
     //  to have our items added to the cache.  after all, as long as our
     //  collection is alive, they can just be found there anyways.  (and when
     //  found there, they may be promoted to the cache anyways.)
-    GlodaCollectionManager.cacheLoadUnify(nounMeta.id, newItems, false);
+    GlodaCollectionManager.cacheLoadUnify(nounDef.id, newItems, false);
 
     // just directly tell the collection about the items.  we know the query
     //  matches (at least until we introduce predicates that we cannot express
     //  in SQL.)
     this.collection._onItemsAdded(newItems);
   },
 
   handleError: function gloda_ds_qfq_handleError(aError) {
@@ -287,21 +287,22 @@ QueryFromQueryCallback.prototype = {
 var GlodaDatastore = {
   _log: null,
 
   /* see Gloda's documentation for these constants */
   kSpecialColumn: 1,
   kSpecialString: 2,
   kSpecialFulltext: 3,
   
-  kMagicAttrIDs: -1,
-  
-  kConstraintEquals: 0,
+  kConstraintIdIn: 0,
   kConstraintIn: 1,
   kConstraintRanges: 2,
+  kConstraintEquals: 3,
+  kConstraintStringLike: 4,
+  kConstraintFulltext: 5,
 
   /* ******************* SCHEMA ******************* */
 
   _schemaVersion: 10,
   _schema: {
     tables: {
 
       // ----- Messages
@@ -954,20 +955,20 @@ var GlodaDatastore = {
    *  decrement the value when the statement completes.
    */
   trackAsync: function() {
     this._pendingAsyncStatements++;
     return this._asyncTrackerListener;
   },
 
   /* ********** Attribute Definitions ********** */
-  /** Maps (attribute def) compound names to the GlodaAttributeDef objects. */
-  _attributes: {},
+  /** Maps (attribute def) compound names to the GlodaAttributeDBDef objects. */
+  _attributeDBDefs: {},
   /** Map attribute ID to the definition and parameter value that produce it. */
-  _attributeIDToDef: {},
+  _attributeIDToDBDefAndParam: {},
   /**
    * We maintain the attributeDefinitions next id counter mainly because we can.
    *  Since we mediate the access, there's no real risk to doing so, and it
    *  allows us to keep the writes on the async connection without having to
    *  wait for a completion notification.
    */
   _nextAttributeId: 1,
 
@@ -1010,19 +1011,19 @@ var GlodaDatastore = {
 
     iads.executeAsync(this.trackAsync());
 
     return attributeId;
   },
 
   /**
    * Sync-ly look-up all the attribute definitions, populating our authoritative
-   *  _attributes and _attributeIDToDef maps.  (In other words, once this method
-   *  is called, those maps should always be in sync with the underlying
-   *  database.)
+   *  _attributeDBDefss and _attributeIDToDBDefAndParam maps.  (In other words,
+   *  once this method is called, those maps should always be in sync with the
+   *  underlying database.)
    */
   getAllAttributes: function gloda_ds_getAllAttributes() {
     let stmt = this._createSyncStatement(
       "SELECT id, attributeType, extensionName, name, parameter \
          FROM attributeDefinitions", true);
 
     // map compound name to the attribute
     let attribs = {};
@@ -1040,48 +1041,46 @@ var GlodaDatastore = {
       let rowParameter = this._getVariant(stmt, 4);
 
       let compoundName = rowExtensionName + ":" + rowName;
 
       let attrib;
       if (compoundName in attribs) {
         attrib = attribs[compoundName];
       } else {
-        attrib = new GlodaAttributeDef(this, /* aID */ null,
-          compoundName, /* aProvider */ null, rowAttributeType,
-          rowExtensionName, rowName, /* subject types */ null,
-          /* obj type */ null, /* noun def */ null);
+        attrib = new GlodaAttributeDBDef(this, /* aID */ null,
+          compoundName, rowAttributeType, rowExtensionName, rowName);
         attribs[compoundName] = attrib;
       }
       // if the parameter is null, the id goes on the attribute def, otherwise
       //  it is a parameter binding and goes in the binding map.
       if (rowParameter == null) {
         attrib._id = rowId;
         idToAttribAndParam[rowId] = [attrib, null];
       } else {
         attrib._parameterBindings[rowParameter] = rowId;
         idToAttribAndParam[rowId] = [attrib, rowParameter];
       }
     }
     stmt.finalize();
 
     this._log.info("done loading all attribute defs");
 
-    this._attributes = attribs;
-    this._attributeIDToDef = idToAttribAndParam;
+    this._attributeDBDefs = attribs;
+    this._attributeIDToDBDefAndParam = idToAttribAndParam;
   },
 
   /**
    * Helper method for GlodaAttributeDef to tell us when their bindParameter
    *  method is called and they have created a new binding (using
    *  GlodaDatastore._createAttributeDef).  In theory, that method could take
    *  an additional argument and obviate the need for this method.
    */
   reportBinding: function gloda_ds_reportBinding(aID, aAttrDef, aParamValue) {
-    this._attributeIDToDef[aID] = [aAttrDef, aParamValue];
+    this._attributeIDToDBDefAndParam[aID] = [aAttrDef, aParamValue];
   },
 
   /* ********** Folders ********** */
   /** next folder (row) id to issue, populated by _getAllFolderMappings. */
   _nextFolderId: 1,
 
   get _insertFolderLocationStatement() {
     let statement = this._createAsyncStatement(
@@ -2038,20 +2037,20 @@ var GlodaDatastore = {
     // A list of [attribute def object, (attr) parameter value, attribute value]
     let attribParamVals = []
 
     let smas = this._selectMessageAttributesByMessageIDStatement;
 
     smas.bindInt64Parameter(0, aMessage.id);
     while (this._syncStep(smas)) {
       let attributeID = smas.getInt64(0);
-      if (!(attributeID in this._attributeIDToDef)) {
+      if (!(attributeID in this._attributeIDToDBDefAndParam)) {
         this._log.error("Attribute ID " + attributeID + " not in our map!");
       }
-      let attribAndParam = this._attributeIDToDef[attributeID];
+      let attribAndParam = this._attributeIDToDBDefAndParam[attributeID];
       let val = smas.getDouble(1);
       attribParamVals.push([attribAndParam[0], attribAndParam[1], val]);
     }
     smas.reset();
 
     return attribParamVals;
   },
 
@@ -2060,28 +2059,28 @@ var GlodaDatastore = {
   },
   _numberQuoter: function(aNum) {
     return aNum;
   },
 
   /* ===== Generic Attribute Support ===== */
   adjustAttributes: function gloda_ds_adjustAttributes(aItem, aAddDBAttributes,
       aRemoveDBAttributes) {
-    let nounMeta = aItem.NOUN_META;
-    let dbMeta = nounMeta._dbMeta;
+    let nounDef = aItem.NOUN_DEF;
+    let dbMeta = nounDef._dbMeta;
     if (dbMeta.insertAttrStatement === undefined) {
       dbMeta.insertAttrStatement = this._createAsyncStatement(
-        "INSERT INTO " + nounMeta.attrTableName +
-        " (" + nounMeta.attrIDColumnName + ", attributeID, value) " +
+        "INSERT INTO " + nounDef.attrTableName +
+        " (" + nounDef.attrIDColumnName + ", attributeID, value) " +
         " VALUES (?1, ?2, ?3)");
       // we always create this at the same time (right here), no need to check
       dbMeta.deleteAttrStatement = this._createAsyncStatement(
-        "DELETE FROM " + nounMeta.attrTableName + " WHERE " +
+        "DELETE FROM " + nounDef.attrTableName + " WHERE " +
         " attributeID = ?1 AND value = ?2 AND " +
-        nounMeta.attrIDColumnName + " = ?3");
+        nounDef.attrIDColumnName + " = ?3");
     }
 
     let ias = dbMeta.insertAttrStatement;
     let das = dbMeta.deleteAttrStatement;
     this._beginTransaction();
     try {
       for (let iAttr = 0; iAttr < aAddDBAttributes.length; iAttr++) {
         let attribValueTuple = aAddDBAttributes[iAttr];
@@ -2119,22 +2118,22 @@ var GlodaDatastore = {
     }
     catch (ex) {
       this._rollbackTransaction();
       throw ex;
     }
   },
 
   clearAttributes: function gloda_ds_clearAttributes(aItem) {
-    let nounMeta = aItem.NOUN_META;
+    let nounDef = aItem.NOUN_DEF;
     let dbMeta = nounMeta._dbMeta;
     if (dbMeta.clearAttrStatement === undefined) {
       dbMeta.clearAttrStatement = this._createAsyncStatement(
-        "DELETE FROM " + nounMeta.attrTableName + " WHERE " +
-        nounMeta.attrIDColumnName + " = ?1");
+        "DELETE FROM " + nounDef.attrTableName + " WHERE " +
+        nounDef.attrIDColumnName + " = ?1");
     }
   
     if (aItem.id != null) {
       dbMeta.clearAttrStatement.bindInt64Parameter(0, aItem.id);
       dbMeta.clearAttrStatement.executeAsync(this.trackAsync());
     }
   },
 
@@ -2144,169 +2143,238 @@ var GlodaDatastore = {
    *  for this reason.
    */
   get _escapeLikeStatement() {
     let statement = this._createAsyncStatement("SELECT 0");
     this.__defineGetter__("_escapeLikeStatement", function() statement);
     return this._escapeLikeStatement;
   },
 
+  _convertToDBValuesAndGroupByAttributeID:
+    function gloda_ds__convertToDBValuesAndGroupByAttributeID(aAttrDef,
+                                                              aValues) {
+    let objectNounDef = aAttrDef.objectNounDef;
+    if (!aAttrDef.usesParameter) {
+      let dbValues = [];
+      for (let iValue = 0; iValue < aValues.length; iValue++) {
+        dbValues.push(objectNounDef.toParamAndValue(aValues[iValue])[1]);
+      }
+      yield [aAttrDef.id, dbValues];
+      return;
+    }
+    
+    let curParam, attrID, dbValues;
+    let attrDBDef = aAttrDef.dbDef;
+    for (let iValue = 0; iValue < aValues.length; iValue++) {
+      let [dbParam, dbValue] = objectNounDef.toParamAndValue(aValues[iValue]);
+      if (curParam === undefined) {
+        curParam = dbParam;
+        attrID = attrDBDef.bindParameter(curParam);
+        dbValues = [dbValue];
+      }
+      else if (curParam == dbParam) {
+        dbValues.push(dbValue);
+      }
+      else {
+        yield [attrID, dbValues];
+        curParam = dbParam;
+        attrID = attrDBDef.bindParameter(curParam);
+        dbValues = [dbValue];
+      }
+    }
+    if (dbValues !== undefined)
+      yield [attrID, dbValues];
+  },
+
+  _convertRangesToDBStringsAndGroupByAttributeID:
+    function gloda_ds__convertRangesToDBStringsAndGroupByAttributeID(aAttrDef,
+      aValues, aValueColumnName) {
+    let objectNounDef = aAttrDef.objectNounDef;
+    if (!aAttrDef.usesParameter) {
+      let dbStrings = [];
+      for (let iValue = 0; iValue < aValues.length; iValue++) {
+        let [lowerVal, upperVal] = aValues[iValue];
+        // they both can't be null.  that is the law.
+        if (lowerVal == null)
+          dbStrings.push(aValueColumnName + " <= " +
+                         objectNounDef.toParamAndValue(upperVal)[1]);
+        else if (upperVal == null)
+          dbStrings.push(aValueColumnName + " >= " +
+                         objectNounDef.toParamAndValue(lowerVal)[1]);
+        else // no one is null!
+          dbStrings.push(aValueColumnName + " BETWEEN " +
+                         objectNounDef.toParamAndValue(lowerVal)[1] + " AND " +
+                         objectNounDef.toParamAndValue(upperVal)[1]);
+      }
+      yield [aAttrDef.id, dbStrings];
+      return;
+    }
+    
+    let curParam, attrID, dbStrings;
+    let attrDBDef = aAttrDef.dbDef;
+    for (let iValue = 0; iValue < aValues.length; iValue++) {
+      let [lowerVal, upperVal] = aValues[iValue];
+
+      let dbString, dbParam, lowerDBVal, upperDBVal;
+      // they both can't be null.  that is the law.
+      if (lowerVal == null) {
+        [dbParam, upperDBVal] = objectNounDef.toParamAndValue(upperVal);
+        dbString = aValueColumnName + " <= " + upperDBVal;
+      }
+      else if (upperVal == null) {
+        [dbParam, lowerDBVal] = objectNounDef.toParamAndValue(lowerVal);
+        dbString = aValueColumnName + " >= " + lowerDBVal; 
+      }
+      else { // no one is null!
+        [dbParam, lowerDBVal] = objectNounDef.toParamAndValue(lowerVal);
+        dbString = aValueColumnName + " BETWEEN " + lowerDBVal + " AND " +
+                   objectNounDef.toParamAndValue(upperVal)[1];
+      }
+
+      if (curParam === undefined) {
+        curParam = dbParam;
+        attrID = attrDBDef.bindParameter(curParam);
+        dbStrings = [dbString];
+      }
+      else if (curParam == dbParam) {
+        dbStrings.push(dbString);
+      }
+      else {
+        yield [attrID, dbStrings];
+        curParam = dbParam;
+        attrID = attrDBDef.bindParameter(curParam);
+        dbStrings = [dbString];
+      }
+    }
+    if (dbStrings !== undefined)
+      yield [attrID, dbStrings];
+  },
+
   /**
    * Perform a database query given a GlodaQueryClass instance that specifies
    *  a set of constraints relating to the noun type associated with the query.
    *  A GlodaCollection is returned containing the results of the look-up.
    *  By default the collection is "live", and will mutate (generating events to
    *  its listener) as the state of the database changes.
    * This functionality is made user/extension visible by the Query's
    *  getCollection (asynchronous) and getAllSync (synchronous).
    */
   queryFromQuery: function gloda_ds_queryFromQuery(aQuery, aListener,
       bSynchronous, aListenerData) {
     // when changing this method, be sure that GlodaQuery's testMatch function
     //  likewise has its changes made.
-    let nounMeta = aQuery._nounMeta;
+    let nounDef = aQuery._nounDef;
 
     let whereClauses = [];
     let unionQueries = [aQuery].concat(aQuery._unions);
     let boundArgs = [];
 
     for (let iUnion = 0; iUnion < unionQueries.length; iUnion++) {
       let curQuery = unionQueries[iUnion];
       let selects = [];
 
       for (let iConstraint = 0; iConstraint < curQuery._constraints.length;
            iConstraint++) {
-        let attr_ors = curQuery._constraints[iConstraint];
-
-        let lastAttributeID = null;
-        let attrValueTests = [];
-        let valueTests = null;
-
-        // our implementation requires that everyone in attr_ors has the same
-        //  attribute.
-        let presumedAttr = attr_ors[0][0];
-
-        // -- handle full-text specially here, it's different than the other
-        //  cases...
-        if (presumedAttr.special == kSpecialFulltext) {
-          let matchStr = [APV[2] for each
-            ([iAPV, APV] in Iterator(attr_ors))].join(" OR ");
-          matchStr.replace("'", "''");
-
-          // for example, the match
-          let ftSelect = "SELECT docid FROM " + nounMeta.tableName + "Text" +
-            " WHERE " + presumedAttr.specialColumnName + " MATCH '" +
-            matchStr + "'";
-          selects.push(ftSelect);
-
-          // bypass the logic used by the other cases
-          continue;
+        let constraint = curQuery._constraints[iConstraint];
+        let [constraintType, attrDef] = constraint;
+        let constraintValues = constraint.slice(2);
+        
+        let idColumnName, tableColumnName;
+        if (constraintType == this.kConstraintIdIn) {
+          // we don't need any of the next cases' setup code, and we especially
+          //  would prefer that attrDef isn't accessed since it's null for us.
         }
-
-        let tableName, idColumnName, valueColumnName, valueQuoter;
-        if (presumedAttr.special == kSpecialColumn ||
-            presumedAttr.special == kSpecialString) {
-          tableName = nounMeta.tableName;
+        else if (attrDef.special) {
+          tableName = nounDef.tableName;
           idColumnName = "id"; // canonical id for a table is "id".
-          valueColumnName = presumedAttr.specialColumnName;
-          if (presumedAttr.special == kSpecialString)
-            valueQuoter = this._stringSQLQuoter;
-          else
-            valueQuoter = this._numberQuoter;
+          valueColumnName = attrDef.specialColumnName;
         }
         else {
-          tableName = nounMeta.attrTableName;
-          idColumnName = nounMeta.attrIDColumnName;
+          tableName = nounDef.attrTableName;
+          idColumnName = nounDef.attrIDColumnName;
           valueColumnName = "value";
-          valueQuoter = this._numberQuoter;
+        }
+        
+        let select = null, bindArgs = null;
+        if (constraintType === this.kConstraintIdIn) {
+          // this is somewhat of a trick.  this does mean that this can be the
+          //  only constraint.  Namely, our idiom is:
+          // SELECT * FROM blah WHERE id IN (a INTERSECT b INTERSECT c)
+          //  but if we only have 'a', then that becomes "...IN (a)", and if
+          //  'a' is not a select but a list of id's... tricky, no?  
+          select = constraintValue.join(",");
         }
-
-        // we want a net 'or' for everyone in here, where 'everyone' is presumed
-        //  to have been generated from a single attribute.  Since a single
-        //  attribute can actually map to multiple attribute id's because of the
-        //  parameters, we actually need to make this slightly more complicated
-        //  than it could be.  We want to OR together the clauses for testing
-        //  each attributeID, where within each clause we OR the value.
-        // ex: (attributeID=1 AND (value=1 OR value=2)) OR (attributeID=2 AND
-        //      (value=7))
-        // note that we don't consolidate things into an IN clause (although
-        //  we could) and it's okay because the optimizer makes all such things
-        //  equal.
-        for (let iOrIndex = 0; iOrIndex < attr_ors.length; iOrIndex++) {
-          let APV = attr_ors[iOrIndex];
-
-          let attributeID;
-          if (APV[1] != null)
-            attributeID = APV[0].bindParameter(APV[1]);
-          else
-            attributeID = APV[0].id;
-          if (attributeID != lastAttributeID) {
-            valueTests = [];
-            if (APV[0].special == kSpecialColumn ||
-                APV[0].special == kSpecialString)
-              attrValueTests.push(["", valueTests]);
-            else
-              attrValueTests.push(["attributeID = " + attributeID, valueTests]);
-            lastAttributeID = attributeID;
+        else if (constraintType === this.kConstraintIn) {
+          let clauses = [];
+          for each ([attrID, values] in
+              this._convertToDBValuesAndGroupByAttributeID(attrDef,
+                                                           constraintValues)) {
+            clauses.push("(attributeID = " + attrID +
+                         " AND " + valueColumnName + " IN (" +
+                         values.join(",") + "))");
           }
-
-          // straight value match?
-          if (APV.length == 3) {
-            if (APV[2] != null)
-              valueTests.push(valueColumnName + " = " + valueQuoter(APV[2]));
+          select = "SELECT " + idColumnName + " FROM " + tableName +
+            " WHERE " + clauses.join(" OR ");
+        }
+        else if (constraintType === this.kConstraintRanges) {
+          let clauses = [];
+          for each ([attrID, dbStrings] in
+              this._convertRangesToDBStringsAndGroupByAttributeID(attrDef,
+                              constraintValues, valueColumnName)) {
+            clauses.push("(attributeID = " + attrID +
+                         " AND (" + dbStrings.join(" OR ") + "))");
           }
-          // (quoting is not required for ranges because we only support ranges
-          //  for numbers.  as such, no use of valueQuoter in here.)
-          else { // APV.length == 4, so range match
-            // - numeric case (no quoting in here)
-            if (presumedAttr.special != kSpecialString) {
-              if (APV[2] === null) // so just <=
-                valueTests.push(valueColumnName + " <= " + APV[3]);
-              else if (APV[3] === null) // so just >=
-              // BETWEEN is optimized to >= and <=, or we could just do that
-              //  ourself (in other words, this shouldn't hurt our use of indices)
-                valueTests.push(valueColumnName + " >= " + APV[2]);
-              else
-                valueTests.push(valueColumnName + " BETWEEN " + APV[2] +
-                                  " AND " + APV[3]);
-            }
-            // - string case (LIKE)
-            else {
-              // this will result in a warning in debug builds.  as we move to
-              //  supporting async operation, we should also move to binding all
-              //  arguments for dynamic queries too.
-              likePayload = '';
-              for each (let [iValuePart, valuePart] in Iterator(APV[2])) {
-                if (typeof valuePart == "string")
-                  likePayload += this._escapeLikeStatement.escapeStringForLIKE(
-                    valuePart, "/");
-                else
-                  likePayload += "%";
-              }
-              valueTests.push(valueColumnName + " LIKE ? ESCAPE '/'");
-              boundArgs.push(likePayload);
-            }
+          select = "SELECT " + idColumnName + " FROM " + tableName +
+            " WHERE " + clauses.join(" OR ");
+        }
+        else if (constraintType === this.kConstraintEquals) {
+          let clauses = [];
+          for each ([attrID, values] in
+              this._convertToDBValuesAndGroupByAttributeID(attrDef,
+                                                           constraintValues)) {
+            clauses.push("(attributeID = " + attrID +
+                         " AND (" + [valueColumnName + " = ?" for each
+                         (value in values)].join("OR") + ")");
+            boundArgs.push.apply(boundArgs, values);
           }
+          select = "SELECT " + idColumnName + " FROM " + tableName +
+            " WHERE " + clauses.join(" OR ");
         }
-        let select = "SELECT " + idColumnName + " FROM " + tableName +
-          " WHERE " +
-          [("(" + avt[0] +
-            (avt[1].length ? ((avt[0] ? " AND " : "") + "(" 
-                 + avt[1].join(" OR ") + ")") :
-               "")
-            + ")")
-           for each ([i, avt] in Iterator(attrValueTests))].join(" OR ");
-        selects.push(select);
+        else if (constraintType === this.kConstraintStringLike) {
+          likePayload = '';
+          for each (let [iValuePart, valuePart] in Iterator(constraintValues) {
+            if (typeof valuePart == "string")
+              likePayload += this._escapeLikeStatement.escapeStringForLIKE(
+                valuePart, "/");
+            else
+              likePayload += "%";
+          }
+          select = "SELECT " + idColumnName + " FROM " + tableName +
+            " WHERE " + valueColumnName + " LIKE ? ESCAPE '/'";
+          boundArgs.push(likePayload);
+        }
+        else if (constraintType === this.kConstraintFulltext) {
+          let matchStr = constraintValues[0];
+          select = "SELECT docid FROM " + nounDef.tableName + "Text" +
+            " WHERE " + attrDef.specialColumnName + " MATCH ?";
+          boundArgs.push(matchStr);
+        }
+        
+        if (select)
+          selects.push(select);
+        else
+          this._log.warning("Unable to translate constraint of type " + 
+            constraintType + " on attribute bound as " + aAttrDef.boundName);
       }
 
       if (selects.length)
         whereClauses.push("id IN (" + selects.join(" INTERSECT ") + " )");
     }
 
-    let sqlString = "SELECT * FROM " + nounMeta.tableName;
+    let sqlString = "SELECT * FROM " + nounDef.tableName;
     if (whereClauses.length)
       sqlString += " WHERE " + whereClauses.join(" OR ");
     
     if (aQuery._order.length) {
       let orderClauses = [];
       for (let [, colName] in Iterator(aQuery._order)) {
          if (colName[0] == "-")
            orderClauses.push(colName.substring(1) + " DESC");
@@ -2327,63 +2395,63 @@ var GlodaDatastore = {
     if (bSynchronous) {
       let statement = this._createSyncStatement(sqlString, true);
       for (let [iBinding, bindingValue] in Iterator(boundArgs)) {
         this._bindVariant(statement, iBinding, bindingValue);
       }
 
       let items = [];
       while (this._syncStep(statement)) {
-        items.push(nounMeta.objFromRow.call(nounMeta.datastore, statement));
+        items.push(nounDef.objFromRow.call(nounDef.datastore, statement));
       }
       statement.finalize();
 
       // have the collection manager attempt to replace the instances we just
       //  created with pre-existing instances.  if the instance didn't exist,
       //  cache the newly observed ones.  We are trading off wastes here; we don't
       //  want to have to ask the collection manager about every row, and we don't
       //  want to invent some alternate row storage.
-      GlodaCollectionManager.cacheLoadUnify(nounMeta.id, items);
-      collection = new GlodaCollection(nounMeta, items, aQuery, aListener);
+      GlodaCollectionManager.cacheLoadUnify(nounDef.id, items);
+      collection = new GlodaCollection(nounDef, items, aQuery, aListener);
       if (aListenerData !== undefined)
         collection.data = aListenerData;
 
       GlodaCollectionManager.registerCollection(collection);
     }
     else { // async!
       let statement = this._createAsyncStatement(sqlString, true);
       for (let [iBinding, bindingValue] in Iterator(boundArgs)) {
         this._bindVariant(statement, iBinding, bindingValue);
       }
 
-      collection = new GlodaCollection(nounMeta, [], aQuery, aListener);
+      collection = new GlodaCollection(nounDef, [], aQuery, aListener);
       if (aListenerData !== undefined)
         collection.data = aListenerData;
       GlodaCollectionManager.registerCollection(collection);
 
-      statement.executeAsync(new QueryFromQueryCallback(statement, nounMeta,
+      statement.executeAsync(new QueryFromQueryCallback(statement, nounDef,
         collection));
       statement.finalize();
     }
     return collection;
   },
 
   loadNounItem: function gloda_ds_loadNounItem(aItem, aReferencesByNounID) {
     let jsonDict = this._json.decode(aItem._jsonText);
     delete aItem._jsonText;
     
-    let attribIDToDef = this._attributeIDToDef;
+    let attribIDToDBDefAndParam = this._attributeIDToDBDefAndParam;
     
     let deps = {};
     let hasDeps = false;
     
     // Iterate over the attributes on the item
     for each (let [attribId, jsonValue] in Iterator(jsonDict)) {
       // find the attribute definition that corresponds to this key
-      let attrib = attribIDToDef[attribId][0];
+      let attrib = attribIDToDBDefAndParam[attribId][0];
       // the attribute should only fail to exist if an extension was removed
       if (attrib === undefined)
         continue;
       
       let objectNounDef = attrib.objectNounDef;
       
       // if it has a tableName member, then it's a persistent object that needs
       //  to be loaded, which also means we need to hold it in a collection
--- a/modules/explattr.js
+++ b/modules/explattr.js
@@ -94,46 +94,43 @@ var GlodaExplicitAttr = {
   
   defineAttributes: function() {
     // Tag
     this._attrTag = Gloda.defineAttribute({
                         provider: this,
                         extensionName: Gloda.BUILT_IN,
                         attributeType: Gloda.kAttrExplicit,
                         attributeName: "tag",
-                        bind: true,
                         bindName: "tags",
                         singular: false,
                         subjectNouns: [Gloda.NOUN_MESSAGE],
                         objectNoun: Gloda.NOUN_TAG,
                         parameterNoun: null,
                         // Property change notifications that we care about:
                         propertyChanges: ["keywords"],
                         }); // not-tested
 
     // Star
     this._attrStar = Gloda.defineAttribute({
                         provider: this,
                         extensionName: Gloda.BUILT_IN,
                         attributeType: Gloda.kAttrExplicit,
                         attributeName: "star",
-                        bind: true,
                         bindName: "starred",
                         singular: true,
                         subjectNouns: [Gloda.NOUN_MESSAGE],
                         objectNoun: Gloda.NOUN_BOOLEAN,
                         parameterNoun: null,
                         }); // tested-by: test_attributes_explicit
     // Read/Unread
     this._attrRead = Gloda.defineAttribute({
                         provider: this,
                         extensionName: Gloda.BUILT_IN,
                         attributeType: Gloda.kAttrExplicit,
                         attributeName: "read",
-                        bind: true,
                         singular: true,
                         subjectNouns: [Gloda.NOUN_MESSAGE],
                         objectNoun: Gloda.NOUN_BOOLEAN,
                         parameterNoun: null,
                         }); // tested-by: test_attributes_explicit
     
   },
   
--- a/modules/fundattr.js
+++ b/modules/fundattr.js
@@ -92,95 +92,88 @@ var GlodaFundAttr = {
   defineAttributes: function() {
     /* ***** Conversations ***** */
     // conversation: subjectMatches
     this._attrConvSubject = Gloda.defineAttribute({
       provider: this,
       extensionName: Gloda.BUILT_IN,
       attributeType: Gloda.kAttrDerived,
       attributeName: "subjectMatches",
-      bind: false,
       singular: true,
       special: Gloda.kSpecialFulltext,
       specialColumnName: "subject",
       subjectNouns: [Gloda.NOUN_CONVERSATION],
       objectNoun: Gloda.NOUN_FULLTEXT,
       });
   
     /* ***** Messages ***** */
     // folder
     this._attrFolder = Gloda.defineAttribute({
       provider: this,
       extensionName: Gloda.BUILT_IN,
       attributeType: Gloda.kAttrFundamental,
       attributeName: "folderURI",
-      bind: false,
       singular: true,
       special: Gloda.kSpecialColumn,
       specialColumnName: "folderID",
       subjectNouns: [Gloda.NOUN_MESSAGE],
       objectNoun: Gloda.NOUN_FOLDER,
       }); // tested-by: test_attributes_fundamental
     
     // bodyMatches. super-synthetic full-text matching...
     this._attrBody = Gloda.defineAttribute({
       provider: this,
       extensionName: Gloda.BUILT_IN,
       attributeType: Gloda.kAttrDerived,
       attributeName: "bodyMatches",
-      bind: false,
       singular: true,
       special: Gloda.kSpecialFulltext,
       specialColumnName: "body",
       subjectNouns: [Gloda.NOUN_MESSAGE],
       objectNoun: Gloda.NOUN_FULLTEXT,
       }); // not-tested
   
     // --- Fundamental
     // From
     this._attrFrom = Gloda.defineAttribute({
                         provider: this,
                         extensionName: Gloda.BUILT_IN,
                         attributeType: Gloda.kAttrFundamental,
                         attributeName: "from",
-                        bind: true,
                         singular: true,
                         subjectNouns: [Gloda.NOUN_MESSAGE],
                         objectNoun: Gloda.NOUN_IDENTITY,
                         }); // tested-by: test_attributes_fundamental
     // To
     this._attrTo = Gloda.defineAttribute({
                         provider: this,
                         extensionName: Gloda.BUILT_IN,
                         attributeType: Gloda.kAttrFundamental,
                         attributeName: "to",
-                        bind: true,
                         singular: false,
                         subjectNouns: [Gloda.NOUN_MESSAGE],
                         objectNoun: Gloda.NOUN_IDENTITY,
                         }); // tested-by: test_attributes_fundamental
     // Cc
     this._attrCc = Gloda.defineAttribute({
                         provider: this,
                         extensionName: Gloda.BUILT_IN,
                         attributeType: Gloda.kAttrFundamental,
                         attributeName: "cc",
-                        bind: true,
                         singular: false,
                         subjectNouns: [Gloda.NOUN_MESSAGE],
                         objectNoun: Gloda.NOUN_IDENTITY,
                         }); // not-tested
 
     // Date.  now lives on the row.
     this._attrDate = Gloda.defineAttribute({
                         provider: this,
                         extensionName: Gloda.BUILT_IN,
                         attributeType: Gloda.kAttrFundamental,
                         attributeName: "date",
-                        bind: false,
                         singular: true,
                         special: Gloda.kSpecialColumn,
                         specialColumnName: "date",
                         subjectNouns: [Gloda.NOUN_MESSAGE],
                         objectNoun: Gloda.NOUN_DATE,
                         }); // tested-by: test_attributes_fundamental
 
     // --- Optimization
@@ -188,62 +181,57 @@ var GlodaFundAttr = {
     //   this that it seems to justify the cost, especially given the frequent
     //   use case.  (In fact, post-filtering for the specific from/to/cc is
     //   probably justifiable rather than losing this attribute...)
     this._attrInvolves = Gloda.defineAttribute({
       provider: this,
       extensionName: Gloda.BUILT_IN,
       attributeType: Gloda.kAttrOptimization,
       attributeName: "involves",
-      bind: true,
       singular: false,
       subjectNouns: [Gloda.NOUN_MESSAGE],
       objectNoun: Gloda.NOUN_IDENTITY,
       }); // not-tested
 
     // From Me To
     this._attrFromMeTo = Gloda.defineAttribute({
       provider: this,
       extensionName: Gloda.BUILT_IN,
       attributeType: Gloda.kAttrOptimization,
       attributeName: "fromMeTo",
-      bind: false,
       singular: false,
       subjectNouns: [Gloda.NOUN_MESSAGE],
       objectNoun: Gloda.NOUN_IDENTITY,
       }); // not-tested
     // From Me Cc
     this._attrFromMeCc = Gloda.defineAttribute({
       provider: this,
       extensionName: Gloda.BUILT_IN,
       attributeType: Gloda.kAttrOptimization,
       attributeName: "fromMeCc",
-      bind: false,
       singular: false,
       subjectNouns: [Gloda.NOUN_MESSAGE],
       objectNoun: Gloda.NOUN_IDENTITY,
       }); // not-tested
     // To Me
     this._attrToMe = Gloda.defineAttribute({
       provider: this,
       extensionName: Gloda.BUILT_IN,
       attributeType: Gloda.kAttrFundamental,
       attributeName: "toMe",
-      bind: false,
       singular: false,
       subjectNouns: [Gloda.NOUN_MESSAGE],
       objectNoun: Gloda.NOUN_IDENTITY,
       }); // not-tested
     // Cc Me
     this._attrCcMe = Gloda.defineAttribute({
       provider: this,
       extensionName: Gloda.BUILT_IN,
       attributeType: Gloda.kAttrFundamental,
       attributeName: "ccMe",
-      bind: false,
       singular: false,
       subjectNouns: [Gloda.NOUN_MESSAGE],
       objectNoun: Gloda.NOUN_IDENTITY,
       }); // not-tested
 
 
     // -- Mailing List
     // Non-singular, but a hard call.  Namely, it is obvious that a message can
@@ -261,17 +249,16 @@ var GlodaFundAttr = {
     //  Additionally, our implicit-to logic needs to work on messages that
     //  weren't relayed by the list-serve, especially messages sent to the list
     //  by the user.
     this._attrList = Gloda.defineAttribute({
                         provider: this,
                         extensionName: Gloda.BUILT_IN,
                         attributeType: Gloda.kAttrFundamental,
                         attributeName: "mailing-list",
-                        bind: true,
                         bindName: "mailingLists",
                         singular: false,
                         subjectNouns: [Gloda.NOUN_MESSAGE],
                         objectNoun: Gloda.NOUN_DATE,
                         }); // not-tested, not-implemented
   },
   
   /**
--- a/modules/gloda.js
+++ b/modules/gloda.js
@@ -257,25 +257,25 @@ var Gloda = {
     query.value.apply(query.value, [address for (address in addresses)]);
     let collection = query.getCollection(aCallbackHandle);
     yield this.kWorkAsync;
 
     // put the identities in the appropriate result lists
     for each (let [, identity] in Iterator(collection.items)) {
       let nameAndResultLists = addresses[identity.value];
       // index 0 is the name, skip it
-      for (let iResList = 1; iResList < nameAndResultLists.length; iResList++)
+      for (let iResList = 1; iResList < nameAndResultLists.length; iResList++) {
         nameAndResultLists[iResList].push(identity);
       }
       delete addresses[identity.value];
     }
     
     // create the identities that did not exist yet
     for each (let [address, nameAndResultLists] in Iterator(addresses)) {
-      let name = nameAndResultsLists[0]; 
+      let name = nameAndResultsLists[0];
 
       // try and find an existing address book contact.
       let card = GlodaUtils.getCardForEmail();
       // XXX when we have the address book GUID stuff, we need to use that to
       //  find existing contacts... (this will introduce a new query phase
       //  where we batch all the GUIDs for an async query)
       // XXX when the address book supports multiple e-mail addresses, we
       //  should also just create identities for any that don't yet exist
@@ -292,17 +292,17 @@ var Gloda = {
       // we must create the identity.  use a blank description because there's
       //  nothing to differentiate it from other identities, as this contact
       //  only has one initially (us).
       // XXX when we have multiple e-mails and there is a meaning associated
       //  with each e-mail, try and use that to populate the description. 
       let identity = GlodaDatastore.createIdentity(contact.id, contact,
         "email", mailAddr, /* description */ "", /* relay? */ false);
       
-      for (let iResList = 1; iResList < nameAndResultLists.length; iResList++)
+      for (let iResList = 1; iResList < nameAndResultLists.length; iResList++) {
         nameAndResultLists[iResList].push(identity);
       }
     }
 
     yield aCallbackHandle.doneWithResult(resultLists);
   },
 
   /**
@@ -585,25 +585,25 @@ var Gloda = {
   /** Next Noun ID to hand out, these don't need to be persisted (for now). */
   _nextNounID: 1000,
 
   /**
    * Maps noun names to noun IDs.
    */
   _nounNameToNounID: {},
   /**
-   * Maps noun IDs to noun meta dictionaries.  (Noun meta dictionaries being
-   *  the dictionary provided to us at the time a noun was defined, plus some
+   * Maps noun IDs to noun definition dictionaries.  (Noun definition
+   *  dictionaries provided to us at the time a noun was defined, plus some
    *  additional stuff we put in there.)
    */
-  _nounIDToMeta: {},
+  _nounIDToDef: {},
   
   _managedToJSON: function gloda_ns_managedToJSON(aItem) {
     return aItem.id;
-  }
+  },
 
   /**
    * Define a noun.  Takes a dictionary with the following keys/values:
    *
    * @param name The name of the noun.  This is not a display name (anything
    *     being displayed needs to be localized, after all), but simply the
    *     canonical name for debugging purposes and for people to pass to
    *     lookupNoun.  The suggested convention is lower-case-dash-delimited,
@@ -631,50 +631,57 @@ var Gloda = {
    * @param fromParamAndValue A function that takes a parameter value and the
    *     object value and should return an instantiated noun instance.
    * @param toParamAndValue A function that takes an instantiated noun
    *     instance and returns a 2-element list of [parameter, value] where
    *     parameter may only be non-null if you passed a usesParameter of true.
    *     Parameter may be of any type (BLOB), and value must be numeric (pass
    *     0 if you don't need the value).
    */
-  defineNoun: function gloda_ns_defineNoun(aNounMeta, aNounID) {
-    this._log.info("Defining noun: " + aNounMeta.name);
+  defineNoun: function gloda_ns_defineNoun(aNounDef, aNounID) {
+    this._log.info("Defining noun: " + aNounDef.name);
     if (aNounID === undefined)
       aNounID = this._nextNounID++;
-    aNounMeta.id = aNounID;
+    aNounDef.id = aNounID;
     // if it has a table, you can query on it.  seems straight-forward.
-    if (aNounMeta.tableName) {
-      [aNounMeta.queryClass, aNounMeta.explicitQueryClass,
-       aNounMeta.wildcardQueryClass] =
-          GlodaQueryClassFactory(aNounMeta);
-      aNounMeta._dbMeta = {};
-      aNounMeta.class.prototype.NOUN_META = aNounMeta;
-      aNounMeta.toJSON = this._managedToJSON;
+    if (aNounDef.tableName) {
+      [aNounDef.queryClass, aNounDef.explicitQueryClass,
+       aNounDef.wildcardQueryClass] =
+          GlodaQueryClassFactory(aNounDef);
+      aNounDef._dbMeta = {};
+      aNounDef.class.prototype.NOUN_DEF = aNounDef;
+      aNounDef.toJSON = this._managedToJSON;
     }
-    if (aNounMeta.cache) {
-      let cacheCost = aNounMeta.cacheCost || 1024;
-      let cacheBudget = aNounMeta.cacheBudget || 128 * 1024;
+    if (aNounDef.cache) {
+      let cacheCost = aNounDef.cacheCost || 1024;
+      let cacheBudget = aNounDef.cacheBudget || 128 * 1024;
       let cacheSize = Math.floor(cacheBudget / cacheCost);
       if (cacheSize)
-        GlodaCollectionManager.defineCache(aNounMeta, cacheSize);
+        GlodaCollectionManager.defineCache(aNounDef, cacheSize);
     }
-    if (aNounMeta.allowsArbitraryAttrs) {
-      aNounMeta.attribsByBoundName = {};
+    if (aNounDef.allowsArbitraryAttrs) {
+      aNounDef.attribsByBoundName = {};
     }
-    this._nounNameToNounID[aNounMeta.name] = aNounID;
-    this._nounIDToMeta[aNounID] = aNounMeta;
-    aNounMeta.actions = [];
+    this._nounNameToNounID[aNounDef.name] = aNounID;
+    this._nounIDToDef[aNounID] = aNounDef;
+    aNounDef.actions = [];
+    
+    this._attrProviderOrderByNoun[aNounDef.id] = [];
+    this._attrProvidersByNoun[aNounDef.id] = {};
     
-    this._attrProviderOrderByNoun[aNounMeta.id] = [];
-    this._attrProvidersByNoun[aNounMeta.id] = {};
-    
-    if (aNounMeta.tableName) {
-      
-    }
+    // - define the 'id' constrainer
+    let idConstrainer = function() {
+      let constraint = [GlodaDatastore.kConstraintIdIn, null];
+      for (let iArg = 0; iArg < arguments.length; iArg++) {
+        constraint.push(arguments[iArg]);
+      }
+      this._constraints.push(constraint);
+      return this;
+    };
+    subjectNounDef.queryClass.prototype.id = idConstrainer;
   },
 
   /**
    * Lookup a noun (ID) suitable for passing to defineAttribute's various
    *  noun arguments.  Throws an exception if the noun with the given name
    *  cannot be found; the assumption is that you can't live without the noun.
    */
   lookupNoun: function gloda_ns_lookupNoun(aNounName) {
@@ -703,17 +710,17 @@ var Gloda = {
    *  with facts like "this message is read".  We currently implement the 'fact'
    *  by defining an attribute with a 'boolean' noun type.  To deal with this,
    *  in various places we pass-in the attribute as well as the noun value.
    *  Since the relationships for booleans and integers in these cases is
    *  standard and well-defined, this works out pretty well, but suggests we
    *  need to think things through.
    *
    * @param aNounID The ID of the noun you want to define an action on.
-   * @param aAction Meta The dictionary describing the noun.  The dictionary
+   * @param aActionMeta The dictionary describing the noun.  The dictionary
    *     should have the following fields:
    * - actionType: a string indicating the type of action.  Currently, only
    *   "filter" is a legal value.
    * - actionTarget: the noun ID of the noun type on which this action is
    *   applicable.  For example,
    *
    * The following should be present for actionType=="filter";
    * - shortName: The name that should be used to display this constraint.  For
@@ -721,29 +728,29 @@ var Gloda = {
    *   using shortName as the label.
    * - makeConstraint: A function that takes the attribute that is the source
    *   of the noun and the noun instance as arguments, and returns APV-style
    *   constraints.  Since the APV-style query mechanism is now deprecated,
    *   this signature is deprecated.  Probably the way to update this would be
    *   to pass in the query instance that constraints should be contributed to.
    */
   defineNounAction: function gloda_ns_defineNounAction(aNounID, aActionMeta) {
-    let nounMeta = this._nounIDToMeta[aNounID];
-    nounMeta.actions.push(aActionMeta);
+    let nounDef = this._nounIDToDef[aNounID];
+    nounDef.actions.push(aActionMeta);
   },
 
   /**
    * Retrieve all of the actions (as defined using defineNounAction) for the
    *  given noun type (via noun ID) with the given action type (ex: filter).
    */
   getNounActions: function gloda_ns_getNounActions(aNounID, aActionType) {
-    let nounMeta = this._nounIDToMeta[aNounID];
-    if (!nounMeta)
+    let nounDef = this._nounIDToDef[aNounID];
+    if (!nounDef)
       return [];
-    return [action for each ([i, action] in Iterator(nounMeta.actions))
+    return [action for each ([i, action] in Iterator(nounDef.actions))
             if (!aActionType || (action.actionType == aActionType))];
   },
 
   /** Attribute providers in the sequence to process them. */
   _attrProviderOrderByNoun: {},
   /** Maps attribute providers to the list of attributes they provide */
   _attrProviders: {},
   /**
@@ -910,18 +917,40 @@ var Gloda = {
     //  as the value.
     this.defineNoun({
       name: "parameterized-identity",
       class: null,
       allowsArbitraryAttrs: false,
       computeDelta: function(aCurValues, aOldValues) {
         let oldMap = {};
         for each (let [, tupe] in Iterator(aOldValues)) {
-          let [originIdentity, 
+          let [originIdentity, targetIdentity] = tupe;
+          let targets = oldMap[originIdentity];
+          if (targets === undefined)
+            targets = oldMap[originIdentity] = {};
+          targets[targetIdentity] = true;
         }
+        
+        let added = [], removed = [];
+        for each (let [, tupe] in Iterator(aCurValues)) {
+          let [originIdentity, targetIdentity] = tupe;
+          let targets = oldMap[originIdentity];
+          if ((targets === undefined) || !(targetIdentity in targets))
+            added.push(tupe);
+          else
+            delete targets[targetIdentity];
+        }
+        
+        for each (let [originIdentity, targets] in Iterator(oldMap)) {
+          for (let targetIdentity in targets) {
+            removed.push([originIdentity, targetIdentity]);
+          }
+        }
+        
+        return [added, removed];
       },
       contributeObjDependencies: function(aJsonValues, aReferencesByNounID) {
         // nothing to do with a zero-length list
         if (aJsonValues.length == 0)
           return false;
       
         let references = aReferencesByNounID[this.NOUN_IDENTITY];
         if (references === undefined)
@@ -965,82 +994,71 @@ var Gloda = {
    *  attribute storage, as well as creating the appropriate query object
    *  constraint helper functions.  This name is somewhat of a misnomer because
    *  special attributes are not 'bound' (because specific/non-generic per-class
    *  code provides the properties) but still depend on this method to
    *  establish their constraint helper methods.
    *
    * @XXX potentially rename to not suggest binding is required.
    */
-  _bindAttribute: function gloda_ns_bindAttr(aAttr, aSubjectType, aObjectType,
-                                             aSingular, aDoBind, aBindName) {
-    if (!(aSubjectType in this._nounIDToMeta))
+  _bindAttribute: function gloda_ns_bindAttr(aAttrDef, aSubjectType) {
+    if (!(aSubjectType in this._nounIDToDef))
       throw Error("Invalid subject type: " + aSubjectType);
 
-    let objNounMeta = this._nounIDToMeta[aObjectType];
-    let subjectNounMeta = this._nounIDToMeta[aSubjectType];
+    let objNounDef = this._nounIDToDef[aAttrDef.objectNoun];
+    let subjectNounDef = this._nounIDToDef[aSubjectType];
 
     // -- the on-object bindings
     if (aDoBind) {
       aAttr.boundName = aBindName;
     }
 
     // -- the query constraint helpers
-    if (subjectNounMeta.queryClass !== undefined) {
+    if (subjectNounDef.queryClass !== undefined) {
       let constrainer = function() {
-        // all the arguments provided end up being ORed together
-        let our_ors = [];
+        let constraint = [GlodaDatastore.kConstraintIn, aAttrDef];
         for (let iArg = 0; iArg < arguments.length; iArg++) {
-          let argument = arguments[iArg];
-          our_ors.push([aAttr].concat(nounMeta.toParamAndValue(argument)));
+          constraint.push(arguments[iArg]);
         }
-        // but the constraints are ANDed together
-        this._constraints.push(our_ors);
+        this._constraints.push(constraint);
         return this;
       };
 
-      subjectNounMeta.queryClass.prototype[aBindName] = constrainer;
+      subjectNounDef.queryClass.prototype[aBindName] = constrainer;
 
       // - ranged value helper: fooRange
-      if (nounMeta.continuous) {
+      if (objNounDef.continuous) {
+        // takes one or more tuples of [lower bound, upper bound]
         let rangedConstrainer = function() {
-          // all the arguments provided end up being ORed together
-          let our_ors = [];
-          for (let iArg = 0; iArg < arguments.length; iArg +=2 ) {
-            let pv1 = nounMeta.toParamAndValue(arguments[iArg]);
-            let pv2 = nounMeta.toParamAndValue(arguments[iArg+1]);
-            our_ors.push([aAttr, pv1[0], pv1[1], pv2[1]]);
+          let constraint = [GlodaDatastore.kConstraintRanges, aAttrDef];
+          for (let iArg = 0; iArg < arguments.length; iArg++ ) {
+            constraint.push(arguments[iArg]);
           }
-          // but the constraints are ANDed together
-          this._constraints.push(our_ors);
+          this._constraints.push(constraints);
           return this;
         }
 
-        subjectNounMeta.queryClass.prototype[aBindName + "Range"] =
+        subjectNounDef.queryClass.prototype[aBindName + "Range"] =
           rangedConstrainer;
       }
 
       // - string LIKE helper for special on-row attributes: fooLike
+      // (it is impossible to store a string as an indexed attribute, which is
+      //  why we do this for on-row only.)
       if (aAttr.special == this.kSpecialString) {
         let likeConstrainer = function() {
-          let our_ors = [];
+          let constraint = [GlodaDatastore.kConstraintStringLike, aAttrDef];
           for (let iArg = 0; iArg < arguments.length; iArg++) {
-            let argument = arguments[iArg];
-            let this_or = [aAttr].concat(nounMeta.toParamAndValue(argument));
-            // we are pushing it up to a length of 4 to signify that this is a
-            //  LIKE query rather than an exact match.  this results in a
-            //  similar decision process to the numeric case.
-            this_or.push("LIKE");
-            our_ors.push(this_or);
+            constraint.push(arguments[iArg]);
           }
-          this._constraints.push(our_ors);
+          this._constraints.push(constraints);
           return this;
         }
 
-        subjectNounMeta.queryClass.prototype[aBindName + "Like"] =
+        subjectNounDef.queryClass.prototype[aBindName + "Like"] =
           likeConstrainer;
       }
     }
 
     aAttr._singular = aSingular;
   },
 
   /**
@@ -1092,116 +1110,102 @@ var Gloda = {
         !("singular" in aAttrDef) ||
         !("subjectNouns" in aAttrDef) ||
         !("objectNoun" in aAttrDef))
       // perhaps we should have a list of required attributes, perchance with
       //  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.")
 
+    // return if the attribute has already been defined
+    if (aAttrDef.dbDef) {
+      return aAttrDef;
+    }
+
     // provider tracking
     if (!(aAttrDef.provider.providerName in this._attrProviders)) {
       this._attrProviders[aAttrDef.provider.providerName] = [];
     }
 
+    let compoundName = aAttrDef.extensionName + ":" + aAttrDef.attributeName;
+    let attrDBDef;
+    if (compoundName in GlodaDatastore._attributeDBDefs) {
+      // the existence of the GlodaAttributeDef means that either it has
+      //  already been fully defined, or has been loaded from the database but
+      //  not yet 'bound' to a provider (and had important meta-info that
+      //  doesn't go in the db copied over)
+      attrDBDef = GlodaDatastore._attributeDBDefs[compoundName];
+    }
+    // we need to create the attribute definition in the database
+    else {
+      let attrID = null;
+      attrID = GlodaDatastore._createAttributeDef(aAttrDef.attributeType,
+                                                  aAttrDef.extensionName,
+                                                  aAttrDef.attributeName,
+                                                  null);
+    
+      attrDBDef = new GlodaAttributeDBDef(GlodaDatastore, attrID, compoundName,
+        aAttrDef.attributeType, aAttrDef.extensionName, aAttrDef.attributeName);
+      GlodaDatastore._attributeDBDefs[compoundName] = attrDBDef;
+      GlodaDatastore._attributeIDToDBDefAndParam[attrID] = [attrDBDef, null];
+    }
+    
+    aAttrDef.dbDef = attrDBDef;
+    attrDBDef.attrDef = aAttrDef;
+
     let bindName;
     if ("bindName" in aAttrDef)
       bindName = aAttrDef.bindName;
     else
       bindName = aAttrDef.attributeName;
+    aAttrDef.boundName = bindName;
 
-    let compoundName = aAttrDef.extensionName + ":" + aAttrDef.attributeName;
-    let attr = null;
-    if (compoundName in GlodaDatastore._attributes) {
-      // the existence of the GlodaAttributeDef means that either it has
-      //  already been fully defined, or has been loaded from the database but
-      //  not yet 'bound' to a provider (and had important meta-info that
-      //  doesn't go in the db copied over)
-      attr = GlodaDatastore._attributes[compoundName];
-      if (attr.provider !== null) {
-        return attr;
-      }
+    for (let iSubject = 0; iSubject < aAttrDef.subjectNouns.length;
+           iSubject++) {
+      let subjectType = aAttrDef.subjectNouns[iSubject];
+      this._bindAttribute(attr, subjectType, aAttrDef.objectNoun,
+                          aAttrDef.singular, aAttrDef.bind, bindName);
 
-      // we are behind the abstraction veil and can set these things
-      // (these would otherwise be passed in to the GlodaAttributeDef
-      //  constructor.  they are not like the HATHATHAT guys below)
-      attr._provider = aAttrDef.provider;
-      attr._subjectTypes = aAttrDef.subjectNouns;
-      attr._objectType = aAttrDef.objectNoun;
-      // things after here also need to be set below the new GlodaAttributeDef
-      //  clause below... HATHATHAT
-      attr._special = aAttrDef.special || this.kSpecialNotAtAll;
-      attr._specialColumnName = aAttrDef.specialColumnName || null;
-
-      for (let iSubject = 0; iSubject < aAttrDef.subjectNouns.length;
-           iSubject++) {
-        let subjectType = aAttrDef.subjectNouns[iSubject];
-        this._bindAttribute(attr, subjectType, aAttrDef.objectNoun,
-                            aAttrDef.singular, aAttrDef.bind, bindName);
-
-        // update the provider maps...
-        if (this._attrProviderOrderByNoun[subjectType]
-                .indexOf(aAttrDef.provider) == -1) {
-          this._attrProviderOrderByNoun[subjectType].push(aAttrDef.provider);
-          this._attrProvidersByNoun[subjectType][aAttrDef.provider] = [];
-        }
-        this._attrProvidersByNoun[subjectType][aAttrDef.provider].push(aAttrDef);
-        
-        let subjectNounDef = this._nounIDToMeta[subjectType];
-        subjectNounDef.attribsByBoundName[bindName] = attr;
-        
+      // update the provider maps...
+      if (this._attrProviderOrderByNoun[subjectType]
+              .indexOf(aAttrDef.provider) == -1) {
+        this._attrProviderOrderByNoun[subjectType].push(aAttrDef.provider);
+        this._attrProvidersByNoun[subjectType][aAttrDef.provider] = [];
       }
-
+      this._attrProvidersByNoun[subjectType][aAttrDef.provider].push(aAttrDef);
+      
+      let subjectNounDef = this._nounIDToDef[subjectType];
+      subjectNounDef.attribsByBoundName[bindName] = attr;
+      
+      
+      if (objectNounDef.tableName ||
+          objectNounDef.contributeObjDependencies) {
+        subjectNounDef.hasObjDependencies = true;
+      }
+      
       this._attrProviders[aAttrDef.provider.providerName].push(attr);
       return attr;
     }
-
-    let objectNounMeta = this._nounIDToMeta[aAttrDef.objectNoun];
-
-    let attrID = null;
-    attrID = GlodaDatastore._createAttributeDef(aAttrDef.attributeType,
-                                                aAttrDef.extensionName,
-                                                aAttrDef.attributeName,
-                                                null);
-
-    attr = new GlodaAttributeDef(GlodaDatastore, attrID, compoundName,
-                                 aAttrDef.provider, aAttrDef.attributeType,
-                                 aAttrDef.extensionName, aAttrDef.attributeName,
-                                 aAttrDef.subjectNouns, aAttrDef.objectNoun);
-    // things here match the HATHATHAT clause above.  clearly, this should also
-    //  be resolved more satisfactorily.
-    attr._special = aAttrDef.special || this.kSpecialNotAtAll;
-    attr._specialColumnName = aAttrDef.specialColumnName || null;
-
-    GlodaDatastore._attributes[compoundName] = attr;
-
-    for (let iSubject = 0; iSubject < aAttrDef.subjectNouns.length;
-         iSubject++) {
-      let subjectType = aAttrDef.subjectNouns[iSubject];
-      this._bindAttribute(attr, subjectType, aAttrDef.objectNoun,
-                          aAttrDef.singular, aAttrDef.bind, bindName);
-    }
-
+    
     this._attrProviders[aAttrDef.provider.providerName].push(attr);
-    GlodaDatastore._attributeIDToDef[attrID] = [attr, null];
     return attr;
   },
 
   /**
    * Retrieve the attribute provided by the given extension with the given
    *  attribute name.  The original idea was that plugins would effectively
    *  name-space attributes, helping avoid collisions.  Since we are leaning
    *  towards using binding heavily, this doesn't really help, as the collisions
    *  will just occur on the attribute name instead.  Also, this can turn
    *  extensions into liars as name changes/moves to core/etc. happen.
    * @TODO consider removing the extension name argument parameter requirement
    */
   getAttrDef: function gloda_ns_getAttrDef(aPluginName, aAttrName) {
     let compoundName = aPluginName + ":" + aAttrName;
-    return GlodaDatastore._attributes[compoundName];
+    return GlodaDatastore._attributeDBDefs[compoundName];
   },
 
   /**
    * Define a SQL table for plug-ins.  This is intended to be used by
    *  extensions/plug-ins whose storage needs exceed those provided by the
    *  attribute parameter (on the attribute definition)/attribute value (on the
    *  attribute instance) idiom.  (This includes extensions whose parameter
    *  usage would exceed acceptable cardinality.)  They can create a table
@@ -1256,29 +1260,29 @@ var Gloda = {
    *  the people who are >= 25 and <= 100.  Likewise age(25, null) would just
    *  return all the people who are 25 or older.  And age(25,30,35,40) would
    *  return people who are either 25-30 or 35-30.
    * There are also full-text constraint columns.  In a nutshell, their
    *  arguments are the strings that should be passed to the SQLite FTS3
    *  MATCH clause.
    */
   newQuery: function gloda_ns_newQuery(aNounID) {
-    let nounMeta = this._nounIDToMeta[aNounID];
-    return new nounMeta.queryClass();
+    let nounDef = this._nounIDToDef[aNounID];
+    return new nounDef.queryClass();
   },
 
   /**
    * Create a collection/query for the given noun-type that only matches the
    *  provided items.  This is to be used when you have an explicit set of items
    *  that you would still like to receive updates for.
    */
   explicitCollection: function gloda_ns_explicitCollection(aNounID, aItems) {
-    let nounMeta = this._nounIDToMeta[aNounID];
-    let collection = new GlodaCollection(nounMeta, aItems, null, null)
-    let query = new nounMeta.explicitQueryClass(collection);
+    let nounDef = this._nounIDToDef[aNounID];
+    let collection = new GlodaCollection(nounDef, aItems, null, null)
+    let query = new nounDef.explicitQueryClass(collection);
     collection.query = query;
     GlodaCollectionManager.registerCollection(collection);
     return collection;
   },
 
   /**
    * Debugging 'wildcard' collection creation support.  A wildcard collection
    *  will 'accept' any new item instances presented to the collection manager
@@ -1286,19 +1290,19 @@ var Gloda = {
    *  as they are indexed, existing items as they are loaded from the database,
    *  etc.
    * Because the items are added to the collection without limit, this will
    *  result in a leak if you don't do something to clean up after the
    *  collection.  (Forgetting about the collection will suffice, as it is still
    *  weakly held.)
    */
   _wildcardCollection: function gloda_ns_explicitCollection(aNounID, aItems) {
-    let nounMeta = this._nounIDToMeta[aNounID];
-    let collection = new GlodaCollection(nounMeta, aItems, null, null)
-    let query = new nounMeta.wildcardQueryClass(collection);
+    let nounDef = this._nounIDToDef[aNounID];
+    let collection = new GlodaCollection(nounDef, aItems, null, null)
+    let query = new nounDef.wildcardQueryClass(collection);
     collection.query = query;
     GlodaCollectionManager.registerCollection(collection);
     return collection;
   },
 
   /**
    * Process the given GlodaMessage, determining all the attributes it should
    *  possess.  This should not be publicly exposed here for multiple reasons.
@@ -1335,17 +1339,17 @@ var Gloda = {
       let attribDesc = allAttribs[iAttrib];
 
       // is it an (attributedef / attribute def id, value) tuple?
       if (attribDesc.length == 2) {
         // if it's already an attrib id, we can use the tuple outright
         if (typeof attribDesc[0] == "number") {
           outAttribs.push(attribDesc);
           let [attribDef, attribParam] =
-            GlodaDatastore._attributeIDToDef[attribDesc[0]];
+            GlodaDatastore._attributeIDToDBDefAndParam[attribDesc[0]];
           memAttribs.push([attribDef, attribParam, attribDesc[1]]);
         }
         else {
           outAttribs.push([attribDesc[0].id, attribDesc[1]]);
           // the parameter is null if they just pass an attribute def
           memAttribs.push([attribDesc[0], null, attribDesc[1]]);
         }
       }
@@ -1381,17 +1385,17 @@ var Gloda = {
    *
    * The result of the processing ends up with attributes in 3 different forms:
    * - Database attribute rows (to be added and removed).
    * - In-memory representation.
    * - JSON-able representation.
    */
   grokNounItem: function gloda_ns_grokNounItem(aItem, aRawReps, aIsNew,
       aCallbackHandle) {
-    let itemNounDef = this._nounIDToMeta[aItem.NOUN_ID];
+    let itemNounDef = this._nounIDToDef[aItem.NOUN_ID];
     let attribsByBoundName = itemNounDef.attribsByBoundName;
     
     let addDBAttribs = [];
     let removeDBAttribs = [];
     
     let jsonDict = {};
     
     let aOldItem = aItem;
@@ -1476,17 +1480,17 @@ var Gloda = {
           let valuesAdded = [];
           for each (let [iCurValue, curValue] in Iterator(value)) {
             if (curValue in oldValueMap)
               delete oldValueMap[curValue];
             else
               valuesAdded.push(curValue);
           }
           // anything still on oldValueMap was removed.
-          let valuesRemoved = [val for val in Iterator(oldValueMap, true)];
+          let valuesRemoved = [val for (val in Iterator(oldValueMap, true))];
           // convert the values to database-style attribute rows
           addDBAttribs.push.apply(addDBAttribs,
             attrib.convertValuesToDBAttributes(valuesAdded));
           removeDBAttribs.push.apply(removeDBAttribs,
             attrib.convertValuesToDBAttributes(valuesRemoved));
         }
       
         // delete the old values to mark that we have processed them
@@ -1550,17 +1554,17 @@ var Gloda = {
       let attribDesc = allAttribs[iAttrib];
 
       // is it an (attributedef / attribute def id, value) tuple?
       if (attribDesc.length == 2) {
         // if it's already an attrib id, we can use the tuple outright
         if (typeof attribDesc[0] == "number") {
           outAttribs.push(attribDesc);
           let [attribDef, attribParam] =
-            GlodaDatastore._attributeIDToDef[attribDesc[0]];
+            GlodaDatastore._attributeIDToDBDefAndParam[attribDesc[0]];
           memAttribs.push([attribDef, attribParam, attribDesc[1]]);
         }
         else {
           outAttribs.push([attribDesc[0].id, attribDesc[1]]);
           // the parameter is null if they just pass an attribute def
           memAttribs.push([attribDesc[0], null, attribDesc[1]]);
         }
       }
--- a/modules/query.js
+++ b/modules/query.js
@@ -100,22 +100,22 @@ GlodaQueryClass.prototype = {
    * Return a collection asynchronously populated by this collection.  You must
    *  provide a listener to receive notifications from the collection as it
    *  receives updates.  The listener object should implement onItemsAdded,
    *  onItemsModified, and onItemsRemoved methods, all of which take a single
    *  argument which is the list of items which have been added, modified, or
    *  removed respectively.
    */
   getCollection: function gloda_query_getCollection(aListener, aData) {
-    return this._nounMeta.datastore.queryFromQuery(this, aListener, false,
+    return this._nounDef.datastore.queryFromQuery(this, aListener, false,
       aData);
   },
   
   getAllSync: function gloda_query_getAllSync(aListener) {
-    return this._nounMeta.datastore.queryFromQuery(this, aListener, true);
+    return this._nounDef.datastore.queryFromQuery(this, aListener, true);
   },
   
   /**
    * Test whether the given first-class noun instance satisfies this query.
    * 
    */
   test: function gloda_query_test(aObj) {
     // when changing this method, be sure that GlodaDatastore's queryFromQuery
@@ -242,37 +242,37 @@ GlodaWildcardQueryClass.prototype = {
     return true;
   }
 };
 
 /**
  * Factory method to effectively create per-noun subclasses of GlodaQueryClass,
  *  GlodaExplicitQueryClas, and GlodaWildcardQueryClass.  For GlodaQueryClass
  *  this allows us to add per-noun helpers.  For the others, this is merely a
- *  means of allowing us to attach the (per-noun) nounMeta to the 'class'.
+ *  means of allowing us to attach the (per-noun) nounDef to the 'class'.
  */
-function GlodaQueryClassFactory(aNounMeta) {
+function GlodaQueryClassFactory(aNounDef) {
   let newQueryClass = function() {
     GlodaQueryClass.call(this);
   }; 
   
   newQueryClass.prototype = new GlodaQueryClass;
   newQueryClass.prototype._queryClass = newQueryClass;
-  newQueryClass.prototype._nounMeta = aNounMeta;
+  newQueryClass.prototype._nounDef = aNounDef;
   
   let newExplicitClass = function(aCollection) {
     GlodaExplicitQueryClass.call(this);
     this.collection = aCollection;
   };
   newExplicitClass.prototype = new GlodaExplicitQueryClass();
   newExplicitClass.prototype._queryClass = newExplicitClass;
-  newExplicitClass.prototype._nounMeta = aNounMeta;
+  newExplicitClass.prototype._nounDef = aNounDef;
 
   let newWildcardClass = function(aCollection) {
     GlodaWildcardQueryClass.call(this);
     this.collection = aCollection;
   };
   newWildcardClass.prototype = new GlodaWildcardQueryClass();
   newWildcardClass.prototype._queryClass = newWildcardClass;
-  newWildcardClass.prototype._nounMeta = aNounMeta;
+  newWildcardClass.prototype._nounDef = aNounDefww;
   
   return [newQueryClass, newExplicitClass, newWildcardClass];
 }