text: /**
   * A Schema is a collection of related {@link Ext.data.Model entities} and their respective
   * {@link Ext.data.schema.Association associations}.
   * 
   * # Schema Instances
   * 
   * By default a single instance of this class is created which serves as the schema for all
   * entities that do not have an explicit `{@link Ext.data.Model#cfg-schema schema}` config
   * either specified or inherited. This is sufficient in most cases.
   * 
   * When an entity does specify a `{@link Ext.data.Model#cfg-schema schema}`, however, that
   * looks up (or creates) an instance for that entity class which is then inherited.
   * 
   * **Important:** All related entities *must* belong to a single schema instance in order
   * to properly link up their associations.
   * 
   * ## Configuring Schemas
   * 
   * The best way to control the configuration of your `schema` is to define a base class for
   * all of your entities and use the `{@link Ext.data.Model#cfg-schema schema}` config like
   * this:
   * 
   *      Ext.define('MyApp.model.Base', {
   *          extend: 'Ext.data.Model',
   *
   *          // This configures the default schema because we don't assign an "id":
   *          schema: {
   *              // configs go here
   *          }
   *      });
   * 
   * **Note:** Only one explicit configuration can be applied to the default schema. In most
   * applications this will not be an issue.
   *
   * By using a base class for your entities you can ensure that the default schema is fully
   * configured before declaration of your classes proceeds. This is especially helpful if
   * you need to set the `namespace` for your schema (see below).
   *
   * ## Relative Naming
   * 
   * When describing associations between entities, it is desirable to use shorthand names
   * that do not contain the common namespace portion. This is called the `entityName` as
   * opposed to its class name. By default, the `entityName` is the full class name. However,
   * if a namespace is used, the common portion can be discarded and we can derive a shorter name.
   * In the following code, `"MyApp.model.Foo"` has an `entityName` of `"Foo"` and the schema has
   * a `namespace` of "MyApp.model".
   * 
   * If you use deeper nesting for entities, you may need to set the `namespace` config to
   * account for this. For example:
   * 
   *      Ext.define('MyApp.model.Base', {
   *          extend: 'Ext.data.Model',
   *
   *          schema: {
   *              namespace: 'MyApp.model'
   *          }
   *      });
   *
   * Your derived classes now will generate proper default `entityName` values even if they
   * have further namespaces. For example, "MyApp.model.foo.Thing" will produce "foo.Thing"
   * as the `entityName` given the above as a base class.
   *
   * # Association Naming
   * 
   * There are various terms involved when describing associations. Perhaps the simplest
   * example that will clarify these terms is that of the common many-to-many association
   * of User and Group.
   * 
   *   * `entityName` - The names "User" and "Group" are the `entityName` values associated
   *   with these two classes. These are derived from their full classnames (perhaps
   *   something like "App.model.User" and "App.model.Group").
   *   
   *   * `associationName` - When talking about associations, especially the many-to-many
   *   variety, it is important to give them names. Associations are not owned by either of
   *   the entities involved, so this name is similar to an `entityName`. In the case of
   *   "User" and "Group", the default `associationName` would be "GroupUsers".
   *   
   *   * `left` and `right` - Associations describe a relationship between two entities. To
   *   talk about specific associations we would use the `entityName` of the parties (such
   *   as "User" or "Group"). When discussing associations in the abstract, however, it is
   *   very helpful to be able to talk about the entities in an association in a general way.
   *   In the case of the "GroupUsers" association, "User" is said to be the `left` while
   *   "Group" is said to be the `right`. In a many-to-many association the selection of
   *   `left` and `right` is arbitrary. When a foreign-key is involved, the `left` entity
   *   is the one containing the foreign-key.
   *
   * ## Custom Naming Conventions
   * 
   * One of the jobs the the `Schema` is to manage name generation (such as `entityName`).
   * This job is delegated to a class called the `namer`. If you need to generate names in
   * other ways, you can provide a custom `namer` for your classes:
   *
   *      Ext.define('MyApp.model.Base', {
   *          extend: 'Ext.data.Model',
   *
   *          schema: {
   *              namespace: 'MyApp.model',
   *              namer: 'custom'
   *          }
   *      });
   *
   * This will create a class using the alias "namer.custom". For example:
   *
   *      Ext.define('MyApp.model.CustomNamer', {
   *          extend: 'Ext.data.schema.Namer',
   *
   *          alias: 'namer.custom',
   *          ...
   *      });
   *
   * For details see the documentation for {@link Ext.data.schema.Namer Namer}.
   */
  Ext.define('Ext.data.schema.Schema', {
      mixins: [
          'Ext.mixin.Factoryable'
      ],
  
      requires: [
          'Ext.util.ObjectTemplate',
  
          'Ext.data.schema.OneToOne',
          'Ext.data.schema.ManyToOne',
          'Ext.data.schema.ManyToMany',
          'Ext.data.schema.Namer'
      ],
  
      alias: 'schema.default', // also configures Factoryable
  
      aliasPrefix: 'schema.',
  
      isSchema: true,
  
      /**
       * @property {String} type
       * The name of the schema's type. This should be the suffix of the `alias` for this
       * class following the "schema." prefix. For example, if the `alias` for a schema is
       * "schema.foo" then `type` should "foo". If an `alias` is specified on the derived
       * class, this property is set automatically.
       * @readonly
       */
      type: 'default',
  
      statics: {
          /**
           * @property {Object} instances
           * A collection of `Schema` instances keyed by its `type`.
           * 
           *      var mySchema = Ext.data.schema.Schema.instances.mySchema;
           *
           * If the `Schema` may not have been created yet, use the {@link #get} method to
           * create the instance on first request:
           * 
           *      var mySchema = Ext.data.schema.Schema.get('mySchema');
           * 
           * @readonly
           * @private
           */
          instances: {},
  
          //<debug>
          // Method used for testing to clear cache for custom instances.
          clearInstance: function(id) {
              var schema = this.instances[id]; 
              delete this.instances[id];
              if (schema) {
                  schema.clear(true);
                  schema.destroy();
              }
          },
          //</debug>
  
          /**
           * Returns the `Schema` instance given its `id` or config object. If only the `id`
           * is specified, that `Schema` instance is looked up and returned. If there is no
           * instance already created, the `id` is assumed to be the `type`. For example:
           *
           *      schema: 'foo'
           *
           * Would be created from the alias `"schema.foo"` and assigned the `id` of "foo"
           * as well.
           *
           * @param {String/Object} config The id, type or config object of the schema.
           * @param {String} [config.type] The type alias of the schema. A "schema." prefix
           * is added to this string, if provided, to complete the alias. This should match
           * match the "alias" of some class derived from `Ext.data.schema.Schema`.
           * @return {Ext.data.schema.Schema} The previously existing or newly created
           * instance.
           */
          get: function (config) {
              var Schema = this,
                  cache = Schema.instances,
                  id = 'default',
                  isString = config && Ext.isString(config),
                  instance, newConfig;
  
              if (config) {
                  if (config.isSchema) {
                      return config;
                  }
                  id = isString ? config : (config.id || id);
              }
  
              if (!(instance = cache[id])) {
                  cache[id] = instance = Schema.create(config);
                  instance.id = id;
              } else if (config && !isString) {
                  //<debug>
                  if (id !== 'default') {
                      Ext.raise('Only the default Schema instance can be reconfigured');
                  }
                  //</debug>
  
                  // When a Model contains a "schema" config object it is allowed to set the
                  // configuration of the default schema. This is the default behavior of
                  // this config on a model unless there is an "id" specified on it. So
                  // the trick is that we already have an instance so we want to merge the
                  // incoming config with the initial config of the default schema and then
                  // make that the effective initial config.
                  newConfig = Ext.merge({}, instance.config);
                  Ext.merge(newConfig, config);
                  instance.setConfig(newConfig);
                  instance.config = newConfig;
  
                  //<debug>
                  instance.setConfig = function () {
                      Ext.raise('The schema can only be reconfigured once');
                  };
                  //</debug>
              }
  
              return instance;
          },
  
          lookupEntity: function (entity) {
              var ret = null,
                  instances = this.instances,
                  match, name, schema;
  
              if (entity) {
                  if (entity.isEntity) {
                      ret = entity.self; // a record
                  } else if (Ext.isFunction(entity)) {
                      // A function (assume that a constructor is the Class).
                      ret = entity;
                  } else if (Ext.isString(entity)) {
                      ret = Ext.ClassManager.get(entity);
  
                      // If we've found a singleton or non-Entity class by that name, ignore it.
                      if (ret && (!ret.prototype || !ret.prototype.isEntity)) {
                          ret = null;
                      }
                      if (!ret) {
                          for (name in instances) {
                              schema = instances[name];
                              match = schema.getEntity(entity);
                              if (match) {
                                  if (ret) {
                                      Ext.raise('Ambiguous entity name "' + entity +
                                          '". Defined by schema "' + ret.schema.type +
                                          '" and "' + name + '"');
                                  }
                                  ret = match;
                              }
                          }
                      }
  
                      if (!ret) {
                          Ext.raise('No such Entity "' + entity + '".');
                      }
                  }
              }
  
              return ret;
          }
      },
  
      /**
       * @property {Number} assocCount The number of {@link Ext.data.schema.Association associations}
       * in this `schema`.
       * @readonly
       */
      assocCount: 0,
  
      /**
       * @property {Number} entityCount The number of {@link Ext.data.Model entities} in this
       * `schema`.
       * @readonly
       */
      entityCount: 0,
  
      config: {
          /**
           * @cfg {Object} defaultIdentifier
           * This config is used to initialize the `{@link Ext.data.Model#identifier}` config
           * for classes that do not define one.
           */
          defaultIdentifier: null,
  
          /**
          * @cfg {Number} keyCheckDelay
          * The time to wait (in ms) before checking for null foreign keys on records that
          * will cause them to be dropped. This is useful for allowing records to be moved to a different
          * source.
          * @private
          * @since 5.0.1
          */
          keyCheckDelay: 10,
  
          /**
           * @cfg {String/Object/Ext.data.schema.Namer} namer
           * Specifies or configures the name generator for the schema.
           */
          namer: 'default',
  
          /**
           * @cfg {String} namespace
           * The namespace for entity classes in this schema.
           */
          namespace: null,
  
          /**
           * @cfg {Object/Ext.util.ObjectTemplate} proxy
           * This is a template used to produce `Ext.data.proxy.Proxy` configurations for
           * Models that do not define an explicit `{@link Ext.data.Model#cfg-proxy proxy}`.
           *
           * This template is processed with the Model class as the data object which means
           * any static properties of the Model are available. The most useful of these are
           *
           *  * `prefix` - The `urlPrefix` property of this instance.
           *  * `entityName` - The {@link Ext.data.Model#entityName name} of the Model
           *      (for example, "User").
           *  * `schema` - This instance.
           */
          proxy: {
              type: 'ajax',
              url: '{prefix}/{entityName}'
          },
  
          /**
           * @cfg {String} [urlPrefix=""]
           * This is the URL prefix used for all requests to the server. It could be something
           * like "/~api". This value is included in the `proxy` template data as "prefix".
           */
          urlPrefix: ''
      },
  
      onClassExtended: function (cls, data) {
          var alias = data.alias;
  
          if (alias && !data.type) {
              if (!Ext.isString(alias)) {
                  alias = alias[0];
              }
  
              cls.prototype.type = alias.substring(this.prototype.aliasPrefix.length);
          }
      },
  
      constructor: function (config) {
          this.initConfig(config);
          this.clear();
      },
  
      //-------------------------------------------------------------------------
      // Config
      // <editor-fold>
  
      applyDefaultIdentifier: function (identifier) {
          return identifier && Ext.Factory.dataIdentifier(identifier);
      },
  
      applyNamer: function (namer) {
          var ret = Ext.data.schema.Namer.create(namer);
          ret.schema = this;
          return ret;
      },
  
      applyNamespace: function (namespace) {
          if (namespace) {
              var end = namespace.length - 1;
              if (namespace.charAt(end) !== '.') {
                  namespace += '.';
              }
          }
  
          return namespace;
      },
  
      applyProxy: function (proxy) {
          return Ext.util.ObjectTemplate.create(proxy);
      },
  
      // </editor-fold>
  
      //-------------------------------------------------------------------------
      // Public
  
      eachAssociation: function (fn, scope) {
          var associations = this.associations,
              name;
  
          for (name in associations) {
              if (associations.hasOwnProperty(name)) {
                  if (fn.call(scope, name, associations[name]) === false) {
                      break;
                  }
              }
          }
      },
  
      eachEntity: function (fn, scope) {
          var entities = this.entities,
              name;
  
          for (name in entities) {
              if (entities.hasOwnProperty(name)) {
                  if (fn.call(scope, name, entities[name].cls) === false) {
                      break;
                  }
              }
          }
      },
  
      /**
       * Returns an `Association` by name.
       * @param {String} name The name of the association.
       * @return {Ext.data.schema.Association} The association instance.
       */
      getAssociation: function (name) {
          var entry = this.associations[name];
          return entry || null;
      },
  
      /**
       * Returns an entity by name.
       * @param {String} name The name of the entity
       * @return {Ext.data.Model} The entity class.
       */
      getEntity: function (name) {
          var entry = this.entityClasses[name] || this.entities[name];
          return (entry && entry.cls) || null;
      },
      
      /**
       * Get the entity name taking into account the {@link #namespace}.
       * @param {String/Ext.data.Model} cls The model class or name of the class.
       * @return {String} The entity name
       */
      getEntityName: function (cls) {
          var ns = this.getNamespace(),
              index, name;
              
          if (typeof cls === 'string') {
              name = cls;
          } else {
              name = cls.$className || null;
          }
  
          if (name) { // if (not anonymous class)
              if (ns) {
                  index = ns.length;
                  if (name.substring(0, index) !== ns) {
                      return name;
                  }
              }
  
              if (index) {
                  name = name.substring(index);
              }
          }
  
          return name;
      },
      
      /**
       * Checks if the passed entity has attached associations that need to be read when
       * using nested loading.
       * 
       * @param {String/Ext.Class/Ext.data.Model} name The name, instance, or Model class.
       * @return {Boolean} `true` if there are associations attached to the entity.
       */
      hasAssociations: function(name) {
          name = name.entityName || name;
          return !!this.associationEntityMap[name];  
      },
      
      /**
       * Checks if an entity is defined
       * @param {String/Ext.data.Model} entity The name or model
       * @return {Boolean} True if this entity is defined
       */
      hasEntity: function (entity) {
          var name = this.getEntityName(entity);
          return !!(this.entities[name] || this.entityClasses[name]);
      },
  
      //-------------------------------------------------------------------------
      // Protected
  
      /**
       * Adds an entry from a {@link Ext.data.schema.ManyToMany matrix config} declared by an
       * entity.
       * 
       * This is the ideal method to override in a derived class if the standard, default
       * naming conventions need to be adjusted. In the override, apply whatever logic is
       * appropriate to determine the missing values and pass along the proper results to
       * this method in the `callParent`.
       * 
       * @param {Ext.Class} entityType A class derived from `Ext.data.Model`.
       *
       * @param {String} matrixName The name of the matrix association.
       *
       * @param {String} [relation] A base name for the matrix. For information about the
       * meaning of this see {@link Ext.data.schema.Schema#ManyToMany}.
       * 
       * @param {Object} left The descriptor for the "left" of the matrix.
       * @param {String} left.type The type of the entity on the "left" of the matrix.
       * 
       * @param {String} [left.field] The name of the field in the matrix table for the "left"
       * side entity. If not provided, this defaults to the `left.type` name
       * {@link Ext.util.Inflector#singularize singularized} and uncapitalized followed by
       * "Id". For example, "userId" for a `left.type` of "Users".
       * 
       * @param {String} [left.role] The name of the relationship from the `left.type` to the
       * `right.type`. If not provided, this defaults to the `left.type` name
       * {@link Ext.util.Inflector#pluralize pluralized} and uncapitalized. For example,
       * "users" for a `left.type` of "User".
       * 
       * @param {Object} right The descriptor for the "right" of the matrix.
       * @param {String} right.type The type of the entity on the "right" of the matrix.
       * 
       * @param {String} [right.field] The name of the field in the matrix table for the
       * "right" side entity. If not provided, this defaults in the same way as `left.field`
       * except this is based on `right.type`.
       * 
       * @param {String} [right.role] The name of the relationship from the `right.type` to
       * the `left.type`. If not provided, this defaults in the same way as `left.role`
       * except this is based on `right.type`.
       * 
       * @protected
       */
      addMatrix: function (entityType, matrixName, relation, left, right) {
          var me = this,
              namer = me.getNamer(),
              associations = me.associations,
              entities = me.entities,
              leftType   = left.type,
              rightType  = right.type,
              leftField  = left.field  || namer.apply('idField', leftType),
              rightField = right.field || namer.apply('idField', rightType),
              leftRole   = left.role   || namer.matrixRole(relation, leftType),
              rightRole  = right.role  || namer.matrixRole(relation, rightType),
              matrix, leftEntry, rightEntry;
  
          leftEntry = entities[leftType] || 
                     (entities[leftType] = { cls: null, name: leftType, associations: {} });
  
          rightEntry = entities[rightType] ||
                      (entities[rightType] = { cls: null, name: rightType, associations: {} });
  
          ++me.assocCount;
          associations[matrixName] = matrix = new Ext.data.schema.ManyToMany({
              name: matrixName,
              schema: me,
              definedBy: entityType,
              left: {
                  cls: leftEntry.cls,
                  type: leftType,
                  role: leftRole,
                  field: leftField,
                  associationKey: left.associationKey
              },
              right: {
                  cls: rightEntry.cls,
                  type: rightType,
                  role: rightRole,
                  field: rightField,
                  associationKey: right.associationKey
              }
          });
  
          leftEntry.associations[matrix.right.role] = matrix.right;
          rightEntry.associations[matrix.left.role] = matrix.left;
  
          if (leftEntry.cls) {
              me.associationEntityMap[leftEntry.cls.entityName] = true;
          }
  
          if (rightEntry.cls) {
              me.associationEntityMap[rightEntry.cls.entityName] = true;
          }
  
          me.decorateModel(matrix);
      },
  
      /**
       * Adds a {@link Ext.data.Field#reference reference} field association for an entity
       * to this `schema`.
       * 
       * This is the ideal method to override in a derived class if the standard, default
       * naming conventions need to be adjusted. In the override, apply whatever logic is
       * appropriate to determine the missing values and pass along the proper results to
       * this method in the `callParent`.
       * 
       * @param {Ext.Class} entityType A class derived from `Ext.data.Model`.
       * 
       * @param {Ext.data.field.Field} referenceField The `field` with the `reference` config.
       * 
       * @param {Object} [descr] The `reference` descriptor from the `referenceField` if one
       * was given in the field definition.
       *
       * @param {String} [descr.association] The name of the association. If empty or null, this
       * will be derived from `entityType`, `role`, `inverse` and
       * `referenceField.unique`.
       *
       * @param {String} [descr.role] The name of the relationship from `entityType` to the target
       * `type`. If not specified, the default is the `referenceField.name` (minus any "Id"
       * suffix if present).
       *
       * @param {String} [descr.inverse] The name of the relationship from the target `type`
       * to the `entityType`. If not specified, this is derived from the
       * {@link Ext.data.Model#entityName entityName} of the `entityType`
       * ({@link Ext.util.Inflector#singularize singularized} or
       * {@link Ext.util.Inflector#pluralize pluralized} based on `referenceField.unique`).
       *
       * @param {String} descr.type The {@link Ext.data.Model#entityName entityName} of the
       * target of the reference.
       *
       * @param {Boolean} [unique=false] Indicates if the reference is one-to-one.
       * @param {Boolean} [dupeCheck] (private)
       * 
       * @protected
       */
      addReference: function (entityType, referenceField, descr, unique, dupeCheck) {
          var me = this,
              namer = me.getNamer(),
              entities = me.entities,
              associations = me.associations,
              entityName  = entityType.entityName,
              association = descr.association,
              child       = descr.child,
              parent      = descr.parent,
              rightRole   = descr.role,
              // Allow { child: 'OrderItem' } or the reverse (for one-to-one mostly):
              rightType   = descr.type || parent || child,
              leftVal     = descr.inverse,
              left        = Ext.isString(leftVal) ? { role: leftVal } : leftVal,
              leftRole    = left && left.role,
              entry, T;
  
          if (!rightRole) {
              // In a FK association, the left side has the key in a field named something
              // like "orderId". The default implementation of "fieldRole" namer is to drop
              // the id suffix which gives is the role of the right side.
              if (!referenceField || descr.legacy) {
                  rightRole = namer.apply('uncapitalize', rightType);
              } else {
                  rightRole = namer.apply('fieldRole', referenceField.name);
              }
          }
  
          if (!leftRole) {
              leftRole = namer.inverseFieldRole(entityName, unique, rightRole, rightType);
          }
  
          if (!association) {
              if (unique) {
                  association = namer.oneToOne(entityType, leftRole, rightType, rightRole);
              } else {
                  association = namer.manyToOne(entityType, leftRole, rightType, rightRole);
              }
          }
  
          if (dupeCheck && association in associations) {
              if (dupeCheck(associations[association], association, leftRole, rightRole) !== false) {
                  return;
              }
          }
  
          //<debug>
          if (association in associations) {
              Ext.raise('Duplicate association: "' + association + '" declared by ' +
                      entityName + (referenceField ? ('.' + referenceField.name) : '') + ' (collides with ' +
                      associations[association].definedBy.entityName + ')');
          }
          if (referenceField && referenceField.definedBy === entities[rightType]) {
              Ext.raise('ForeignKey reference should not be owned by the target model');
          }
          //</debug>
  
          // Lookup the entry for the target of the reference. Since it may not as yet be
          // defined, we may need to create the entry.
          entry = entities[rightType] ||
                 (entities[rightType] = { cls: null, name: rightType, associations: {} });
  
          // as a field w/reference we are always "left":
          T = unique ? Ext.data.schema.OneToOne : Ext.data.schema.ManyToOne;
          association = new T({
              name: association,
              // Note: "parent" or "child" can be strings so don't assume otherwise
              owner: child ? 'left' : (parent ? 'right' : null),
              definedBy: entityType,
              schema: me,
              field: referenceField,
              nullable: referenceField ? !!referenceField.allowBlank : true,
              left: {
                  cls: entityType,
                  type: entityName,
                  role: leftRole,
                  extra: left
              },
              right: {
                  cls: entry.cls,
                  type: rightType,
                  role: rightRole,
                  extra: descr
              },
              meta: descr
          });
  
          // Add the left and right association "sides" to the appropriate collections, but
          // remember that the right-side entity class may not yet be declared (that's ok as
          // we store the associations in the entry):
          entityType.associations[rightRole] = association.right;
          entry.associations[leftRole] = association.left;
          if (referenceField) {
              // Store the role on the FK field. This "upgrades" legacy associations to the
              // new "field.reference" form.
              referenceField.reference = association.right;
              entityType.references.push(referenceField);
          }
  
          ++me.assocCount;
          me.associationEntityMap[entityName] = true;
          if (entry.cls) {
              me.associationEntityMap[entry.cls.entityName] = true;
          }
          associations[association.name] = association;
          
          if (association.right.cls) {
              me.decorateModel(association);
          }
      },
  
      //-------------------------------------------------------------------------
  
      privates: {
          /**
           * Adds an {@link Ext.data.Model entity} to this `schema`.
           * @param {Ext.Class} entityType A class derived from {@link Ext.data.Model}.
           * @private
           */
          addEntity: function (entityType) {
              var me = this,
                  entities = me.entities,
                  entityName = entityType.entityName,
                  entry = entities[entityName],
                  fields = entityType.fields,
                  associations, field, i, length, name;
  
              if (!entry) {
                  entities[entityName] = entry = {
                      name: entityName,
                      associations: {}
                  };
              }
              //<debug>
              else if (entry.cls) {
                  Ext.raise('Duplicate entity name "' + entityName + '": ' +
                          entry.cls.$className + ' and ' + entityType.$className);
              }
              //</debug>
              else {
                  associations = entry.associations;
                  for (name in associations) {
                      // the associations collection describes the types to which this entity is
                      // related, but the inverse descriptors need this entityType:
                      associations[name].inverse.cls = entityType;
  
                      me.associationEntityMap[entityName] = true;
  
                      // We already have an entry, which means other associations have likely been added
                      // for us, so go ahead and do the inverse decoration
                      me.decorateModel(associations[name].association);
                  }
              }
  
              entry.cls = entityType;
              entityType.prototype.associations = entityType.associations = entry.associations;
              me.entityClasses[entityType.$className] = entry;
  
              ++me.entityCount;
  
              for (i = 0, length = fields.length; i < length; ++i) {
                  field = fields[i];
                  if (field.reference) {
                      me.addReferenceDescr(entityType, field);
                  }
              }
          },
  
          /**
           * Adds the matrix associations of an {@link Ext.data.Model entity} to this `schema`.
           * @param {Ext.Class} entityType A class derived from {@link Ext.data.Model Entity}.
           * @param {Object/String[]} matrices The manyToMany matrices for the class.
           * @private
           */
          addMatrices: function (entityType, matrices) {
              var me = this,
                  i, length, matrixName;
  
              if (Ext.isString(matrices)) {
                  me.addMatrixDescr(entityType, null, matrices);
              } else if (matrices[0]) { // if (isArray)
                  for (i = 0, length = matrices.length; i < length; ++i) {
                      me.addMatrixDescr(entityType, null, matrices[i]);
                  }
              } else {
                  for (matrixName in matrices) {
                      me.addMatrixDescr(entityType, matrixName, matrices[matrixName]);
                  }
              }
          },
  
          /**
           * Adds an entry from a {@link Ext.data.schema.ManyToMany matrix config} declared by an
           * {@link Ext.data.Model entity}.
           *
           * @param {Ext.Class} entityType A class derived from {@link Ext.data.Model Entity}.
           * @param {String} [matrixName] The name of the matrix association.
           * @param {String/Object} matrixDef A {@link Ext.data.schema.ManyToMany matrix config}
           * declared by an {@link Ext.data.Model entity}.
           * @private
           */
          addMatrixDescr: function (entityType, matrixName, matrixDef) {
              var me = this,
                  entityName = entityType.entityName,
                  associations = me.associations,
                  namer = me.getNamer(),
                  left = matrixDef.left,
                  right = matrixDef.right,
                  last, relation;
  
              if (Ext.isString(matrixDef)) {
                  if (matrixDef.charAt(0) === '#') {  // "#User" (entity is on the left)
                      /*
                       *  Ext.define('User', {
                       *      extend: 'Ext.data.Model',
                       *      manyToMany: '#Group'
                       *  });
                       */
                      left  = { type: entityName };  // User
                      right = { type: matrixDef.substring(1) };  // Group
                  }
                  else if (matrixDef.charAt(last = matrixDef.length - 1) === '#') { // "User#"
                      /*
                       *  Ext.define('Group', {
                       *      extend: 'Ext.data.Model',
                       *      manyToMany: 'User#'
                       *  });
                       */
                      left  = { type: matrixDef.substring(0, last) }; // User
                      right = { type: entityName };  // Group
                  }
                  else if (namer.apply('multiRole', entityName) <
                           namer.apply('multiRole', matrixDef)) {
                      /*
                       *  Ext.define('Group', {
                       *      extend: 'Ext.data.Model',
                       *      manyToMany: 'User'
                       *  });
                       */
                      left  = { type: entityName };  // Group
                      right = { type: matrixDef };  // User
                  }
                  else {
                      /*
                       *  Ext.define('User', {
                       *      extend: 'Ext.data.Model',
                       *      manyToMany: 'Group'
                       *  });
                       */
                      left  = { type: matrixDef };  // Group
                      right = { type: entityName };  // User
                  }
              } else {
                  //<debug>
                  Ext.Assert.isString(matrixDef.type, 'No "type" for manyToMany in ' + entityName);
                  //</debug>
  
                  relation = matrixDef.relation;
  
                  if (left || (!right && namer.apply('multiRole', entityName) <
                                         namer.apply('multiRole', matrixDef.type))) {
                      if (!left || left === true) {
                          /*
                           *  Ext.define('User', {
                           *      extend: 'Ext.data.Model',
                           *      manyToMany: {
                           *          type: 'Group',
                           *          left: true
                           *      }
                           *  });
                           */
                          left = { type: entityName }; // User
                      } else {
                          /*
                           *  Ext.define('User', {
                           *      extend: 'Ext.data.Model',
                           *      manyToMany: {
                           *          type: 'Group',
                           *          left: {
                           *              role: 'useroids'
                           *          }
                           *      }
                           *  });
                           */
                          left = Ext.apply({ type: entityName }, left); // User
                      }
                      right = matrixDef;  // Group
                  } else {
                      if (!right || right === true) {
                          /*
                           *  Ext.define('Group', {
                           *      extend: 'Ext.data.Model',
                           *      manyToMany: {
                           *          type: 'User',
                           *          right: true
                           *      }
                           *  });
                           */
                          right = { type: entityName }; // Group
                      } else {
                          /*
                           *  Ext.define('Group', {
                           *      extend: 'Ext.data.Model',
                           *      manyToMany: {
                           *          type: 'User',
                           *          right: {
                           *              role: 'groupoids'
                           *          }
                           *      }
                           *  });
                           */
                          right = Ext.apply({ type: entityName }, right); // Group
                      }
                      left = matrixDef; // User
                  }
              }
  
              if (!matrixName) {
                  matrixName = namer.manyToMany(relation, left.type, right.type);
              }
  
              if (!(matrixName in associations)) {
                  me.addMatrix(entityType, matrixName, relation, left, right);
              }
              //<debug>
              //
              // In the case of a matrix association, both sides may need to declare it to allow
              // them to be used w/o the other present. In development mode, we want to check
              // that they declare the same thing!
              //
              else {
                  var entry = associations[matrixName],
                      before = [entry.kind, entry.left.type, entry.left.role, entry.left.field,
                                entry.right.type, entry.right.role, entry.right.field].join('|');
  
                  // Call back in to bypass this check and realize the new association:
                  delete associations[matrixName];
                  me.addMatrix(entityType, matrixName, relation, left, right);
                  var after = associations[matrixName];
  
                  // Restore the originals so we match production behavior (for testing)
                  associations[matrixName] = entry;
                  entry.left.cls.associations[entry.right.role] = entry.right;
                  entry.right.cls.associations[entry.left.role] = entry.left;
                  --me.assocCount;
  
                  // Now we can compare the old and the new to see if they are the same.
                  after = [after.kind, after.left.type, after.left.role, after.left.field,
                           after.right.type, after.right.role, after.right.field].join('|');
  
                  if (before != after) {
                      Ext.log.warn(matrixName + '(' + entry.definedBy.entityName + '): ' + before);
                      Ext.log.warn(matrixName + '(' + entityName + '): ' + after);
                      Ext.raise('Conflicting association: "' + matrixName + '" declared by ' +
                          entityName + ' was previously declared by ' + entry.definedBy.entityName);
                  }
              }
              //</debug>
          },
  
          /**
           * Adds a {@link Ext.data.Field#reference reference} {@link Ext.data.Field field}
           * association for an entity to this `schema`. This method decodes the `reference`
           * config of the `referenceField` and calls {@link #addReference}.
           *
           * @param {Ext.Class} entityType A class derived from {@link Ext.data.Model Model}.
           * @param {Ext.data.Field} referenceField The `field` with the `reference` config.
           * @private
           */
          addReferenceDescr: function (entityType, referenceField) {
              var me = this,
                  descr = referenceField.$reference;
  
              if (Ext.isString(descr)) {
                  descr = {
                      type: descr
                  };
              } else {
                  descr = Ext.apply({}, descr);
              }
  
              me.addReference(entityType, referenceField, descr, referenceField.unique);
          },
  
          addBelongsTo: function(entityType, assoc) {
              this.addKeylessSingle(entityType, assoc, false);
          },
  
          addHasOne: function(entityType, assoc) {
              this.addKeylessSingle(entityType, assoc, true);
          },
  
          addKeylessSingle: function(entityType, assoc, unique) {
              var foreignKey, referenceField;
  
              assoc = Ext.apply({}, this.checkLegacyAssociation(entityType, assoc));
              assoc.type = this.getEntityName(assoc.child || assoc.parent || assoc.type);
  
              foreignKey = assoc.foreignKey || (assoc.type.toLowerCase() + '_id');
              referenceField = entityType.getField(foreignKey);
              assoc.fromSingle = true;
              if (referenceField) {
                  referenceField.$reference = assoc;
                  referenceField.unique = true;
                  assoc.legacy = true;
                  //<debug>
                  Ext.log.warn('Using foreignKey is deprecated, use a keyed association. See Ext.data.field.Field.reference');
                  //</debug>
              }
              this.addReference(entityType, referenceField, assoc, unique);
          },
  
          addHasMany: function (entityType, assoc) {
              var me = this,
                  entities = me.entities,
                  pending = me.pending,
                  cls, name, referenceField, target, 
                  foreignKey, inverseOptions, child, declaredInverse;
  
              assoc = Ext.apply({}, this.checkLegacyAssociation(entityType, assoc));
  
              assoc.type = this.getEntityName(assoc.child || assoc.parent || assoc.type);
  
              name = assoc.type;
              target = entities[name];
              cls = target && target.cls;
              if (cls) {
                  name = entityType.entityName;
                  foreignKey = assoc.foreignKey || (name.toLowerCase() + '_id');
                  delete assoc.foreignKey;
  
                  // The assoc is really the inverse, so we only set the minimum.
                  // We copy the inverse from assoc and apply it over assoc!
                  declaredInverse = Ext.apply({}, assoc.inverse);
                  delete assoc.inverse;
                  inverseOptions = Ext.apply({}, assoc);
                  delete inverseOptions.type;
  
                  assoc = Ext.apply({
                      type: name,
                      inverse: inverseOptions
                  }, declaredInverse);
  
                  child = inverseOptions.child;
                  if (child) {
                      delete inverseOptions.child;
                      assoc.parent = name;
                  }
  
                  referenceField = cls.getField(foreignKey);
                  if (referenceField) {
                      referenceField.$reference = assoc;
                      assoc.legacy = true;
                      //<debug>
                      Ext.log.warn('Using foreignKey is deprecated, use a keyed association. See Ext.data.field.Field.reference');
                      //</debug>
                  }
  
                  // We already have the entity, we can process it
                  me.addReference(cls, referenceField, assoc, false
                  //<debug>
                  , function(association, name, leftRole, rightRole) {
                      // Check to see if the user has used belongsTo/hasMany in conjunction.
                      var result = !!association.meta.fromSingle && cls === association.left.cls,
                          l, r;
  
                      if (result) {
                          l = cls.entityName;
                          r = entityType.entityName;
                          Ext.raise('hasMany ("' + r + '") and belongsTo ("' + l + '") should not be used in conjunction to declare a relationship. Use only one.');
                      }
  
                      return result;
                  }
                  //</debug>
                  );
              } else {
                  // Pending, push it in the queue for when we load it
                  if (!pending[name]) {
                      pending[name] = [];
                  }
                  pending[name].push([entityType, assoc]);
              }
          },
  
          checkLegacyAssociation: function(entityType, assoc) {
              if (Ext.isString(assoc)) {
                  assoc = {
                      type: assoc
                  };
              } else {
                  assoc = Ext.apply({}, assoc);
              }
  
              if (assoc.model) {
                  assoc.type = assoc.model;
                  // TODO: warn
                  delete assoc.model;
              }
  
              var name = assoc.associatedName || assoc.name;
              if (name) {
                  // TODO: warn
                  delete assoc.associatedName;
                  delete assoc.name;
                  assoc.role = name;
              }
  
              return assoc;
          },
  
          afterKeylessAssociations: function(cls) {
              var pending = this.pending,
                  name = cls.entityName,
                  mine = pending[name],
                  i, len;
  
              if (mine) {
                  for (i = 0, len = mine.length; i < len; ++i) {
                      this.addHasMany.apply(this, mine[i]);
                  }
                  delete pending[name];
              }
          },
  
          clear: function(clearNamespace) {
              // for testing
              var me = this,
                  timer = me.timer;
  
              delete me.setConfig;
  
              if (timer) {
                  window.clearTimeout(timer);
                  me.timer = null;
              }
  
              me.associations = {};
              me.associationEntityMap = {};
              me.entities = {};
              me.entityClasses = {};
              me.pending = {};
              me.assocCount = me.entityCount = 0;
              if (clearNamespace) {
                  me.setNamespace(null);
              }
          },
  
          constructProxy: function (Model) {
              var me = this,
                  data = Ext.Object.chain(Model),
                  proxy = me.getProxy();
  
              data.schema = me;
              data.prefix = me.getUrlPrefix();
  
              return proxy.apply(data);
          },
  
          applyDecoration: function (role) {
              var me = this,
                  // To decorate a role like "users" (of a User / Group matrix) we need to add
                  // getter/setter methods to access the "users" collection ... to Group! All
                  // other data about the "users" role and the User class belong to the given
                  // "role" but the receiver class is the inverse.
                  cls = role.inverse.cls,
                  namer = me.getNamer(),
                  getterName, setterName, proto;
  
              // The cls may not be loaded yet, so we need to check if it is before
              // we can decorate it.
              if (cls && !role.decorated) {
                  role.decorated = true;
  
                  proto = cls.prototype;
  
                  if (!(getterName = role.getterName)) {
                      role.getterName = getterName = namer.getterName(role);
                  }
                  proto[getterName] = role.createGetter();
  
                  // Not all associations will create setters
                  if (role.createSetter) {
                      if (!(setterName = role.setterName)) {
                          role.setterName = setterName = namer.setterName(role);
                      }
                      proto[setterName] = role.createSetter();
                  }
              }
          },
  
          decorateModel: function (association) {
              this.applyDecoration(association.left);
              this.applyDecoration(association.right);
          },
  
          processKeyChecks: function(processAll) {
              var me = this,
                  keyCheckQueue = me.keyCheckQueue,
                  timer = me.timer,
                  len, i, item;
  
              if (timer) {
                  window.clearTimeout(timer);
                  me.timer = null;
              }
  
              if (!keyCheckQueue) {
                  return;
              }
  
              // It's possible that processing a drop may cause another drop
              // to occur. If we're trying to forcibly resolve the state, then
              // we need to trigger all the drops at once. With processAll: false,
              // the loop will jump out after the first iteration.
              do {
                  keyCheckQueue = me.keyCheckQueue;
                  me.keyCheckQueue = [];
  
                  for (i = 0, len = keyCheckQueue.length; i < len; ++i) {
                      item = keyCheckQueue[i];
                      item.role.checkKeyForDrop(item.record);
                  }
              } while (processAll && me.keyCheckQueue.length);
          },
  
          queueKeyCheck: function(record, role) {
              var me = this,
                  keyCheckQueue = me.keyCheckQueue,
                  timer = me.timer;
  
              if (!keyCheckQueue) {
                  me.keyCheckQueue = keyCheckQueue = [];
              }
              keyCheckQueue.push({
                  record: record,
                  role: role
              });
  
              if (!timer) {
                  me.timer = timer = Ext.defer(me.processKeyChecks, me.getKeyCheckDelay(), me);
              }
          },
  
          rankEntities: function () {
              var me = this,
                  entities = me.entities,
                  entityNames = Ext.Object.getKeys(entities),
                  length = entityNames.length,
                  entityType, i;
  
              me.nextRank = 1;
  
              // We do an alpha sort to make the results more stable.
              entityNames.sort();
  
              for (i = 0; i < length; ++i) {
                  entityType = entities[entityNames[i]].cls;
  
                  if (!entityType.rank) {
                      me.rankEntity(entityType);
                  }
              }
  
              //<debug>
              me.topoStack = null; // cleanup diagnostic stack
              //</debug>
          },
  
          rankEntity: function (entityType) {
              var associations = entityType.associations,
                  associatedType, role, roleName;
  
              //<debug>
              var topoStack = this.topoStack || (this.topoStack = []),
                  entityName = entityType.entityName;
  
              topoStack.push(entityName);
  
              if (entityType.rank === 0) {
                  Ext.raise(entityName + " has circular foreign-key references: " +
                                  topoStack.join(" --> "));
              }
  
              entityType.rank = 0; // mark as "adding" so we can detect cycles
              //</debug>
  
              for (roleName in associations) {
                  role = associations[roleName];
                  // The role describes the thing to which entityType is associated, so we
                  // want to know about *this* type and whether it has a foreign-key to the
                  // associated type. The left side is the FK owner so if the associated
                  // type is !left then entityType is left.
                  //
                  if (!role.left && role.association.field) {
                      // This entityType has a foreign-key to the associated type, so add
                      // that type first.
                      associatedType = role.cls;
                      if (!associatedType.rank) {
                          this.rankEntity(associatedType);
                      }
                  }
              }
  
              entityType.rank = this.nextRank++;
  
              //<debug>
              topoStack.pop();
              //</debug>
          }
      } // private
  });
  