'use strict';

var Utils = require('./../utils')
  , Helpers = require('./helpers')
  , _ = require('lodash')
  , Association = require('./base')
  , BelongsTo = require('./belongs-to')
  , HasMany = require('./has-many')
  , HasOne = require('./has-one')
  , CounterCache = require('../plugins/counter-cache')
  , util = require('util');

/**
 * Many-to-many association with a join table.
 *
 * When the join table has additional attributes, these can be passed in the options object:
 *
 * ```js
 * UserProject = sequelize.define('user_project', {
 *   role: Sequelize.STRING
 * });
 * User.belongsToMany(Project, { through: UserProject });
 * Project.belongsToMany(User, { through: UserProject });
 * // through is required!
 *
 * user.addProject(project, { role: 'manager', transaction: t });
 * ```
 *
 * All methods allow you to pass either a persisted instance, its primary key, or a mixture:
 *
 * ```js
 * Project.create({ id: 11 }).then(function (project) {
 *   user.addProjects([project, 12]);
 * });
 * ```
 *
 * In the API reference below, replace `Assocation(s)` with the actual name of your association, e.g. for `User.belongsToMany(Project)` the getter will be `user.getProjects()`.
 *
 * @mixin BelongsToMany
 */
var BelongsToMany = function(source, target, options) {
  Association.call(this);

  options = options || {};

  if (options.through === undefined || options.through === true || options.through === null) {
    throw new Error('belongsToMany must be given a through option, either a string or a model');
  }

  if (!options.through.model) {
    options.through = {
      model: options.through
    };
  }

  this.associationType = 'BelongsToMany';
  this.source = source;
  this.target = target;
  this.targetAssociation = null;
  this.options = options;
  this.sequelize = source.modelManager.sequelize;
  this.through = _.assign({}, options.through);
  this.scope = options.scope;
  this.isMultiAssociation = true;
  this.isSelfAssociation = this.source === this.target;
  this.doubleLinked = false;
  this.as = this.options.as;

  if (!this.as && this.isSelfAssociation) {
    throw new Error('\'as\' must be defined for many-to-many self-associations');
  }

  if (this.as) {
    this.isAliased = true;

    if (Utils._.isPlainObject(this.as)) {
      this.options.name = this.as;
      this.as = this.as.plural;
    } else {
      this.options.name = {
        plural: this.as,
        singular: Utils.singularize(this.as)
      };
    }
  } else {
    this.as = this.target.options.name.plural;
    this.options.name = this.target.options.name;
  }

  this.combinedTableName = Utils.combineTableNames(
    this.source.tableName,
    this.isSelfAssociation ? (this.as || this.target.tableName) : this.target.tableName
  );

  /*
   * If self association, this is the target association - Unless we find a pairing association
   */
  if (this.isSelfAssociation) {
    this.targetAssociation = this;
  }

  /*
   * Default/generated foreign/other keys
   */
  if (_.isObject(this.options.foreignKey)) {
    this.foreignKeyAttribute = this.options.foreignKey;
    this.foreignKey = this.foreignKeyAttribute.name || this.foreignKeyAttribute.fieldName;
  } else {
    if (!this.options.foreignKey) {
      this.foreignKeyDefault = true;
    }

    this.foreignKeyAttribute = {};
    this.foreignKey = this.options.foreignKey || Utils.camelizeIf(
      [
        Utils.underscoredIf(this.source.options.name.singular, this.source.options.underscored),
        this.source.primaryKeyAttribute
      ].join('_'),
      !this.source.options.underscored
    );
  }

  if (_.isObject(this.options.otherKey)) {
    this.otherKeyAttribute = this.options.otherKey;
    this.otherKey = this.otherKeyAttribute.name || this.otherKeyAttribute.fieldName;
  } else {
    if (!this.options.otherKey) {
      this.otherKeyDefault = true;
    }

    this.otherKeyAttribute = {};
    this.otherKey = this.options.otherKey || Utils.camelizeIf(
      [
        Utils.underscoredIf(
          this.isSelfAssociation ?
            Utils.singularize(this.as) :
            this.target.options.name.singular,
          this.target.options.underscored
        ),
        this.target.primaryKeyAttribute
      ].join('_'),
      !this.target.options.underscored
    );
  }

  /*
   * Find paired association (if exists)
   */
  _.each(this.target.associations, function(association) {
    if (association.associationType !== 'BelongsToMany') return;
    if (association.target !== this.source) return;

    if (this.options.through.model === association.options.through.model) {
      this.paired = association;
      association.paired = this;
    }
  }, this);

  if (typeof this.through.model === 'string') {
    if (!this.sequelize.isDefined(this.through.model)) {
      this.through.model = this.sequelize.define(this.through.model, {}, _.extend(this.options, {
        tableName: this.through.model,
        indexes: {}, //we dont want indexes here (as referenced in #2416)
        paranoid: false  // A paranoid join table does not make sense
      }));
    } else {
      this.through.model = this.sequelize.model(this.through.model);
    }
  }

  if (this.paired) {
    if (this.otherKeyDefault) {
      this.otherKey = this.paired.foreignKey;
    }
    if (this.paired.otherKeyDefault) {
      // If paired otherKey was inferred we should make sure to clean it up before adding a new one that matches the foreignKey
      if (this.paired.otherKey !== this.foreignKey) {
        delete this.through.model.rawAttributes[this.paired.otherKey];
      }
      this.paired.otherKey = this.foreignKey;
      this.paired.foreignIdentifier = this.foreignKey;
      delete this.paired.foreignIdentifierField;
    }
  }

  if (this.through) {
    this.throughModel = this.through.model;
  }

  this.options.tableName = this.combinedName = (this.through.model === Object(this.through.model) ? this.through.model.tableName : this.through.model);

  this.associationAccessor = this.as;

  // Get singular and plural names, trying to uppercase the first letter, unless the model forbids it
  var plural = Utils.uppercaseFirst(this.options.name.plural)
    , singular = Utils.uppercaseFirst(this.options.name.singular);

  this.accessors = {
    /**
     * Get everything currently associated with this, using an optional where clause.
     *
     * @param {Object} [options]
     * @param {Object} [options.where] An optional where clause to limit the associated models
     * @param {String|Boolean} [options.scope] Apply a scope on the related model, or remove its default scope by passing false
     * @param {String} [options.schema] Apply a schema on the related model
     * @return {Promise<Array<Instance>>}
     * @method getAssociations
     */
    get: 'get' + plural,
    /**
     * Set the associated models by passing an array of instances or their primary keys. Everything that it not in the passed array will be un-associated.
     *
     * @param {Array<Instance|String|Number>} [newAssociations] An array of instances or primary key of instances to associate with this. Pass `null` or `undefined` to remove all associations.
     * @param {Object} [options] Options passed to `through.findAll`, `bulkCreate`, `update` and `destroy`. Can also hold additional attributes for the join table
     * @param {Object} [options.validate] Run validation for the join model
     * @return {Promise}
     * @method setAssociations
     */
    set: 'set' + plural,
    /**
     * Associate several instances with this.
     *
     * @param {Array<Instance|String|Number>} [newAssociations] An array of instances or primary key of instances to associate with this.
     * @param {Object} [options] Options passed to `through.findAll`, `bulkCreate` and `update`. Can also hold additional attributes for the join table.
     * @param {Object} [options.validate] Run validation for the join model.
     * @return {Promise}
     * @method addAssociations
     */
    addMultiple: 'add' + plural,
    /**
     * Associate one instance with this.
     *
     * @param {Instance|String|Number} [newAssociation] An instance or primary key of instance to associate with this.
     * @param {Object} [options] Options passed to `through.findAll`, `bulkCreate` and `update`. Can also hold additional attributes for the join table.
     * @param {Object} [options.validate] Run validation for the join model.
     * @return {Promise}
     * @method addAssociation
     */
    add: 'add' + singular,
     /**
     * Create a new instance of the associated model and associate it with this.
     *
     * @param {Object} [values]
     * @param {Object} [options] Options passed to create and add. Can also hold additional attributes for the join table
     * @return {Promise}
     * @method createAssociation
     */
    create: 'create' + singular,
    /**
     * Un-associate the instance.
     *
     * @param {Instance|String|Number} [oldAssociated] Can be an Instance or its primary key
     * @param {Object} [options] Options passed to `through.destroy`
     * @return {Promise}
     * @method removeAssociation
     */
    remove: 'remove' + singular,
    /**
     * Un-associate several instances.
     *
     * @param {Array<Instance|String|Number>} [oldAssociated] Can be an array of instances or their primary keys
     * @param {Object} [options] Options passed to `through.destroy`
     * @return {Promise}
     * @method removeAssociations
     */
    removeMultiple: 'remove' + plural,
    /**
     * Check if an instance is associated with this.
     *
     * @param {Instance|String|Number} [instance] Can be an Instance or its primary key
     * @param {Object} [options] Options passed to getAssociations
     * @return {Promise}
     * @method hasAssociation
     */
    hasSingle: 'has' + singular,
    /**
     * Check if all instances are associated with this.
     *
     * @param {Array<Instance|String|Number>} [instances] Can be an array of instances or their primary keys
     * @param {Object} [options] Options passed to getAssociations
     * @return {Promise}
     * @method hasAssociations
     */
    hasAll: 'has' + plural,
    /**
     * Count everything currently associated with this, using an optional where clause.
     *
     * @param {Object} [options]
     * @param {Object} [options.where] An optional where clause to limit the associated models
     * @param {String|Boolean} [options.scope] Apply a scope on the related model, or remove its default scope by passing false
     * @return {Promise<Int>}
     * @method countAssociations
     */
    count: 'count' + plural
  };

  if (this.options.counterCache) {
    new CounterCache(this, this.options.counterCache !== true ? this.options.counterCache : {});
  }
};

util.inherits(BelongsToMany, Association);

// the id is in the target table
// or in an extra table which connects two tables
BelongsToMany.prototype.injectAttributes = function() {
  var self = this;

  this.identifier = this.foreignKey;
  this.foreignIdentifier = this.otherKey;

  // remove any PKs previously defined by sequelize
  _.each(this.through.model.rawAttributes, function(attribute, attributeName) {
    if (attribute.primaryKey === true && attribute._autoGenerated === true) {
      delete self.through.model.rawAttributes[attributeName];
      self.primaryKeyDeleted = true;
    }
  });

  var sourceKey = this.source.rawAttributes[this.source.primaryKeyAttribute]
    , sourceKeyType = sourceKey.type
    , sourceKeyField = sourceKey.field || this.source.primaryKeyAttribute
    , targetKey = this.target.rawAttributes[this.target.primaryKeyAttribute]
    , targetKeyType = targetKey.type
    , targetKeyField = targetKey.field || this.target.primaryKeyAttribute
    , sourceAttribute = _.defaults({}, this.foreignKeyAttribute, { type: sourceKeyType })
    , targetAttribute = _.defaults({}, this.otherKeyAttribute, { type: targetKeyType });

  if (this.primaryKeyDeleted === true) {
    targetAttribute.primaryKey = sourceAttribute.primaryKey = true;
  } else if (this.through.unique !== false) {
    var uniqueKey = [this.through.model.tableName, this.foreignKey, this.otherKey, 'unique'].join('_');
    targetAttribute.unique = sourceAttribute.unique = uniqueKey;
  }

  if (!this.through.model.rawAttributes[this.foreignKey]) {
    this.through.model.rawAttributes[this.foreignKey] = {
      _autoGenerated: true
    };
  }

  if (!this.through.model.rawAttributes[this.otherKey]) {
    this.through.model.rawAttributes[this.otherKey] = {
      _autoGenerated: true
    };
  }

  if (this.options.constraints !== false) {
    sourceAttribute.references = {
      model: this.source.getTableName(),
      key:   sourceKeyField
    };
    // For the source attribute the passed option is the priority
    sourceAttribute.onDelete = this.options.onDelete || this.through.model.rawAttributes[this.foreignKey].onDelete;
    sourceAttribute.onUpdate = this.options.onUpdate || this.through.model.rawAttributes[this.foreignKey].onUpdate;

    if (!sourceAttribute.onDelete) sourceAttribute.onDelete = 'CASCADE';
    if (!sourceAttribute.onUpdate) sourceAttribute.onUpdate = 'CASCADE';

    targetAttribute.references = {
      model: this.target.getTableName(),
      key:   targetKeyField
    };
    // But the for target attribute the previously defined option is the priority (since it could've been set by another belongsToMany call)
    targetAttribute.onDelete = this.through.model.rawAttributes[this.otherKey].onDelete || this.options.onDelete;
    targetAttribute.onUpdate = this.through.model.rawAttributes[this.otherKey].onUpdate || this.options.onUpdate;

    if (!targetAttribute.onDelete) targetAttribute.onDelete = 'CASCADE';
    if (!targetAttribute.onUpdate) targetAttribute.onUpdate = 'CASCADE';
  }

  this.through.model.rawAttributes[this.foreignKey] = _.extend(this.through.model.rawAttributes[this.foreignKey], sourceAttribute);
  this.through.model.rawAttributes[this.otherKey] = _.extend(this.through.model.rawAttributes[this.otherKey], targetAttribute);

  this.identifierField = this.through.model.rawAttributes[this.foreignKey].field || this.foreignKey;
  this.foreignIdentifierField = this.through.model.rawAttributes[this.otherKey].field || this.otherKey;

  if (this.paired && !this.paired.foreignIdentifierField) {
    this.paired.foreignIdentifierField = this.through.model.rawAttributes[this.paired.otherKey].field || this.paired.otherKey;
  }

  this.through.model.refreshAttributes();

  this.toSource = new BelongsTo(this.through.model, this.source, {
    foreignKey: this.foreignKey
  });
  this.manyFromSource = new HasMany(this.source, this.through.model, {
    foreignKey: this.foreignKey
  });
  this.oneFromSource = new HasOne(this.source, this.through.model, {
    foreignKey: this.foreignKey,
    as: this.through.model.name
  });

  this.toTarget = new BelongsTo(this.through.model, this.target, {
    foreignKey: this.otherKey
  });
  this.manyFromTarget = new HasMany(this.target, this.through.model, {
    foreignKey: this.otherKey
  });
  this.oneFromTarget = new HasOne(this.target, this.through.model, {
    foreignKey: this.otherKey,
    as: this.through.model.name
  });

  if (this.paired && this.paired.otherKeyDefault) {
    this.paired.toTarget = new BelongsTo(this.paired.through.model, this.paired.target, {
      foreignKey: this.paired.otherKey
    });

    this.paired.oneFromTarget = new HasOne(this.paired.target, this.paired.through.model, {
      foreignKey: this.paired.otherKey,
      as: this.paired.through.model.name
    });
  }

  Helpers.checkNamingCollision(this);

  return this;
};

BelongsToMany.prototype.injectGetter = function(obj) {
  var association = this;

  obj[this.accessors.get] = function(options) {
    options = association.target.__optClone(options) || {};

    var instance = this
      , through = association.through
      , scopeWhere
      , throughWhere;

    if (association.scope) {
      scopeWhere = _.clone(association.scope);
    }

    options.where = {
      $and: [
        scopeWhere,
        options.where
      ]
    };

    if (Object(through.model) === through.model) {
      throughWhere = {};
      throughWhere[association.foreignKey] = instance.get(association.source.primaryKeyAttribute);

      if (through.scope) {
        _.assign(throughWhere, through.scope);
      }

      options.include = options.include || [];
      options.include.push({
        association: association.oneFromTarget,
        attributes: options.joinTableAttributes,
        required: true,
        where: throughWhere
      });
    }

    var model = association.target;
    if (options.hasOwnProperty('scope')) {
      if (!options.scope) {
        model = model.unscoped();
      } else {
        model = model.scope(options.scope);
      }
    }

    if (options.hasOwnProperty('schema')) {
      model = model.schema(options.schema, options.schemaDelimiter);
    }

    return model.findAll(options);
  };

  obj[this.accessors.count] = function(options) {
    var model = association.target
      , sequelize = model.sequelize;

    options = association.target.__optClone(options) || {};
    options.attributes = [
      [sequelize.fn('COUNT', sequelize.col([association.target.name, model.primaryKeyAttribute].join('.'))), 'count']
    ];
    options.joinTableAttributes = [];
    options.raw = true;
    options.plain = true;

    return obj[association.accessors.get].call(this, options).then(function (result) {
      return parseInt(result.count, 10);
    });
  };

  obj[this.accessors.hasSingle] = obj[this.accessors.hasAll] = function(instances, options) {
    var where = {};

    if (!Array.isArray(instances)) {
      instances = [instances];
    }

    options = options || {};
    options.scope = false;

    _.defaults(options, {
      raw: true
    });

    where.$or = instances.map(function (instance) {
      if (instance instanceof association.target.Instance) {
        return instance.where();
      } else {
        var $where = {};
        $where[association.target.primaryKeyAttribute] = instance;
        return $where;
      }
    });

    options.where = {
      $and: [
        where,
        options.where
      ]
    };

    return this[association.accessors.get](options).then(function(associatedObjects) {
      return associatedObjects.length === instances.length;
    });
  };

  return this;
};

BelongsToMany.prototype.injectSetter = function(obj) {
  var association = this;

  obj[this.accessors.set] = function(newAssociatedObjects, options) {
    options = options || {};
    var instance = this
      , sourceKey = association.source.primaryKeyAttribute
      , targetKey = association.target.primaryKeyAttribute
      , identifier = association.identifier
      , foreignIdentifier = association.foreignIdentifier
      , where = {};

    if (newAssociatedObjects === null) {
      newAssociatedObjects = [];
    } else {
      newAssociatedObjects = association.toInstanceArray(newAssociatedObjects);
    }

    where[identifier] = this.get(sourceKey);
    return association.through.model.findAll(_.defaults({
      where: where,
      raw: true,
    }, options)).then(function (currentRows) {
      var obsoleteAssociations = []
        , defaultAttributes = options
        , promises = []
        , unassociatedObjects;

      // Don't try to insert the transaction as an attribute in the through table
      defaultAttributes = _.omit(defaultAttributes, ['transaction', 'hooks', 'individualHooks', 'ignoreDuplicates', 'validate', 'fields', 'logging']);

      unassociatedObjects = newAssociatedObjects.filter(function(obj) {
        return !_.find(currentRows, function(currentRow) {
          return currentRow[foreignIdentifier] === obj.get(targetKey);
        });
      });

      currentRows.forEach(function(currentRow) {
        var newObj = _.find(newAssociatedObjects, function(obj) {
          return currentRow[foreignIdentifier] === obj.get(targetKey);
        });

        if (!newObj) {
          obsoleteAssociations.push(currentRow);
        } else {
          var throughAttributes = newObj[association.through.model.name];
          // Quick-fix for subtle bug when using existing objects that might have the through model attached (not as an attribute object)
          if (throughAttributes instanceof association.through.model.Instance) {
            throughAttributes = {};
          }

          var where = {}
            , attributes = _.defaults({}, throughAttributes, defaultAttributes);

          where[identifier] = instance.get(sourceKey);
          where[foreignIdentifier] = newObj.get(targetKey);

          if (Object.keys(attributes).length) {
            promises.push(association.through.model.update(attributes, _.extend(options, {
              where: where
            })));
          }
        }
      });

      if (obsoleteAssociations.length > 0) {
        var where = {};
        where[identifier] = instance.get(sourceKey);
        where[foreignIdentifier] = obsoleteAssociations.map(function(obsoleteAssociation) {
          return obsoleteAssociation[foreignIdentifier];
        });

        promises.push(association.through.model.destroy(_.defaults({
          where: where
        }, options)));
      }

      if (unassociatedObjects.length > 0) {
        var bulk = unassociatedObjects.map(function(unassociatedObject) {
          var attributes = {};

          attributes[identifier] = instance.get(sourceKey);
          attributes[foreignIdentifier] = unassociatedObject.get(targetKey);

          attributes = _.defaults(attributes, unassociatedObject[association.through.model.name], defaultAttributes);

          _.assign(attributes, association.through.scope);

          return attributes;
        }.bind(this));

        promises.push(association.through.model.bulkCreate(bulk, _.assign({ validate: true }, options)));
      }

      return Utils.Promise.all(promises);
    });
  };

  obj[this.accessors.addMultiple] = obj[this.accessors.add] = function(newInstances, additionalAttributes) {
    // If newInstances is null or undefined, no-op
    if (!newInstances) return Utils.Promise.resolve();

    additionalAttributes = additionalAttributes || {};

    var instance = this
      , defaultAttributes = _.omit(additionalAttributes, ['transaction', 'hooks', 'individualHooks', 'ignoreDuplicates', 'validate', 'fields', 'logging'])
      , sourceKey = association.source.primaryKeyAttribute
      , targetKey = association.target.primaryKeyAttribute
      , identifier = association.identifier
      , foreignIdentifier = association.foreignIdentifier
      , options = additionalAttributes;

    newInstances = association.toInstanceArray(newInstances);

    var where = {};
    where[identifier] = instance.get(sourceKey);
    where[foreignIdentifier] = newInstances.map(function (newInstance) { return newInstance.get(targetKey); });

    _.assign(where, association.through.scope);

    return association.through.model.findAll(_.defaults({
      where: where,
      raw: true,
    }, options)).then(function (currentRows) {
      var promises = [];

      var unassociatedObjects = [], changedAssociations = [];
      newInstances.forEach(function(obj) {
        var existingAssociation = _.find(currentRows, function(current) {
          return current[foreignIdentifier] === obj.get(targetKey);
        });

        if (!existingAssociation) {
          unassociatedObjects.push(obj);
        } else {
          var throughAttributes = obj[association.through.model.name]
            , attributes = _.defaults({}, throughAttributes, defaultAttributes);

          if (_.any(Object.keys(attributes), function (attribute) {
            return attributes[attribute] !== existingAssociation[attribute];
          })) {
            changedAssociations.push(obj);
          }
        }
      });

      if (unassociatedObjects.length > 0) {
        var bulk = unassociatedObjects.map(function(unassociatedObject) {
          var throughAttributes = unassociatedObject[association.through.model.name]
            , attributes = _.defaults({}, throughAttributes, defaultAttributes);

          attributes[identifier] = instance.get(sourceKey);
          attributes[foreignIdentifier] = unassociatedObject.get(targetKey);

          _.assign(attributes, association.through.scope);

          return attributes;
        }.bind(this));

        promises.push(association.through.model.bulkCreate(bulk, _.assign({ validate: true }, options)));
      }

      changedAssociations.forEach(function(assoc) {
        var throughAttributes = assoc[association.through.model.name]
          , attributes = _.defaults({}, throughAttributes, defaultAttributes)
          , where = {};
        // Quick-fix for subtle bug when using existing objects that might have the through model attached (not as an attribute object)
        if (throughAttributes instanceof association.through.model.Instance) {
          throughAttributes = {};
        }

        where[identifier] = instance.get(sourceKey);
        where[foreignIdentifier] = assoc.get(targetKey);

        promises.push(association.through.model.update(attributes, _.extend(options, {
          where: where
        })));
      });

      return Utils.Promise.all(promises);
    });
  };

  obj[this.accessors.removeMultiple] = obj[this.accessors.remove] = function(oldAssociatedObjects, options) {
    options = options || {};

    oldAssociatedObjects = association.toInstanceArray(oldAssociatedObjects);

    var where = {};
    where[association.identifier] = this.get(association.source.primaryKeyAttribute);
    where[association.foreignIdentifier] = oldAssociatedObjects.map(function (newInstance) { return newInstance.get(association.target.primaryKeyAttribute); });

    return association.through.model.destroy(_.defaults({
      where: where
    }, options));
  };

  return this;
};

BelongsToMany.prototype.injectCreator = function(obj) {
  var association = this;

  obj[this.accessors.create] = function(values, options) {
    var instance = this;
    options = options || {};
    values = values || {};

    if (Array.isArray(options)) {
      options = {
        fields: options
      };
    }

    if (association.scope) {
      _.assign(values, association.scope);
      if (options.fields) {
        options.fields = options.fields.concat(Object.keys(association.scope));
      }
    }

    // Create the related model instance
    return association.target.create(values, options).then(function(newAssociatedObject) {
      return instance[association.accessors.add](newAssociatedObject, _.omit(options, ['fields'])).return(newAssociatedObject);
    });
  };

  return this;
};

module.exports = BelongsToMany;


var tzTable = sequelize.define('tz_table', { foo: DataTypes.STRING });

return tzTable.sync({force: true}).then(function() {

return tzTable.create({foo: 'test'}).then(function(row) {

keep keep keep keep keep replace replace keep

'use strict';

/* jshint -W030 */
var chai = require('chai')
  , expect = chai.expect
  , Support = require(__dirname + '/../support')
  , current = Support.sequelize
  , sinon = require('sinon')
  , DataTypes = require(__dirname + '/../../../lib/data-types')
  , Promise = require('bluebird');

describe(Support.getTestDialectTeaser('Model'), function() {
  describe('method findOne', function () {
    before(function () {
      this.oldFindAll = current.Model.prototype.findAll;
    });
    after(function () {
      current.Model.prototype.findAll = this.oldFindAll;
    });

    beforeEach(function () {
      this.stub = current.Model.prototype.findAll = sinon.stub().returns(Promise.resolve());
    });

    describe('should not add limit when querying on a primary key', function () {
      it('with id primary key', function () {
        var Model = current.define('model');

        return Model.findOne({ where: { id: 42 }}).bind(this).then(function () {
          expect(this.stub.getCall(0).args[0]).to.be.an('object').not.to.have.property('limit');
        });
      });

      it('with custom primary key', function () {
        var Model = current.define('model', {
            uid: {
              type: DataTypes.INTEGER,
              primaryKey: true,
              autoIncrement: true
            }
          });

        return Model.findOne({ where: { uid: 42 }}).bind(this).then(function () {
          expect(this.stub.getCall(0).args[0]).to.be.an('object').not.to.have.property('limit');
        });
      });

      it('with blob primary key', function () {
        var Model = current.define('model', {
          id: {
            type: DataTypes.BLOB,
            primaryKey: true,
            autoIncrement: true
          }
        });

        return Model.findOne({ where: { id: new Buffer('foo') }}).bind(this).then(function () {
          expect(this.stub.getCall(0).args[0]).to.be.an('object').not.to.have.property('limit');
        });
      });
    });

    it('should add limit when using { $ gt on the primary key', function () {
      var Model = current.define('model');

      return Model.findOne({ where: { id: { $gt: 42 }}}).bind(this).then(function () {
        expect(this.stub.getCall(0).args[0]).to.be.an('object').to.have.property('limit');
      });
    });
  });
});


'use strict';

/* jshint -W030 */
var chai = require('chai')
  , expect = chai.expect
  , Support = require(__dirname + '/../support')
  , DataTypes = require(__dirname + '/../../../lib/data-types')
  , Sequelize = require(__dirname + '/../../../index')
  , Promise = Sequelize.Promise
  , _ = require('lodash');

describe(Support.getTestDialectTeaser('Self'), function() {
  it('supports freezeTableName', function() {
    var Group = this.sequelize.define('Group', {}, {
      tableName: 'user_group',
      timestamps: false,
      underscored: true,
      freezeTableName: true
    });

    Group.belongsTo(Group, { as: 'Parent', foreignKey: 'parent_id' });
    return Group.sync({force: true}).then(function() {
      return Group.findAll({
        include: [{
          model: Group,
          as: 'Parent'
        }]
      });
    });
  });

  it('can handle 1:m associations', function() {
    var Person = this.sequelize.define('Person', { name: DataTypes.STRING });

    Person.hasMany(Person, { as: 'Children', foreignKey: 'parent_id'});

    expect(Person.rawAttributes.parent_id).to.be.ok;

    return this.sequelize.sync({force: true}).then(function() {
      return Promise.all([
        Person.create({ name: 'Mary' }),
        Person.create({ name: 'John' }),
        Person.create({ name: 'Chris' })
      ]);
    }).spread(function(mary, john, chris) {
      return mary.setChildren([john, chris]);
    });
  });

  it('can handle n:m associations', function() {
    var self = this;

    var Person = this.sequelize.define('Person', { name: DataTypes.STRING });

    Person.belongsToMany(Person, { as: 'Parents', through: 'Family', foreignKey: 'ChildId', otherKey: 'PersonId' });
    Person.belongsToMany(Person, { as: 'Childs', through: 'Family', foreignKey: 'PersonId', otherKey: 'ChildId' });

    var foreignIdentifiers = _.map(_.values(Person.associations), 'foreignIdentifier');
    var rawAttributes = _.keys(this.sequelize.models.Family.rawAttributes);

    expect(foreignIdentifiers.length).to.equal(2);
    expect(rawAttributes.length).to.equal(4);

    expect(foreignIdentifiers).to.have.members(['PersonId', 'ChildId']);
    expect(rawAttributes).to.have.members(['createdAt', 'updatedAt', 'PersonId', 'ChildId']);

    return this.sequelize.sync({ force: true }).then(function() {
      return self.sequelize.Promise.all([
        Person.create({ name: 'Mary' }),
        Person.create({ name: 'John' }),
        Person.create({ name: 'Chris' })
      ]).spread(function(mary, john, chris) {
        return mary.setParents([john]).then(function() {
          return chris.addParent(john);
        }).then(function() {
          return john.getChilds();
        }).then(function(children) {
          expect(_.map(children, 'id')).to.have.members([mary.id, chris.id]);
        });
      });
    });
  });

  it('can handle n:m associations with pre-defined through table', function() {
    var Person = this.sequelize.define('Person', { name: DataTypes.STRING });
    var Family = this.sequelize.define('Family', {
      preexisting_child: {
        type: DataTypes.INTEGER,
        primaryKey: true
      },
      preexisting_parent: {
        type: DataTypes.INTEGER,
        primaryKey: true
      }
    }, { timestamps: false });

    Person.belongsToMany(Person, { as: 'Parents', through: Family, foreignKey: 'preexisting_child', otherKey: 'preexisting_parent' });
    Person.belongsToMany(Person, { as: 'Children', through: Family, foreignKey: 'preexisting_parent', otherKey: 'preexisting_child' });

    var foreignIdentifiers = _.map(_.values(Person.associations), 'foreignIdentifier');
    var rawAttributes = _.keys(Family.rawAttributes);

    expect(foreignIdentifiers.length).to.equal(2);
    expect(rawAttributes.length).to.equal(2);

    expect(foreignIdentifiers).to.have.members(['preexisting_parent', 'preexisting_child']);
    expect(rawAttributes).to.have.members(['preexisting_parent', 'preexisting_child']);

    var count = 0;
    return this.sequelize.sync({ force: true }).bind(this).then(function() {
      return Promise.all([
        Person.create({ name: 'Mary' }),
        Person.create({ name: 'John' }),
        Person.create({ name: 'Chris' })
      ]);
    }).spread(function(mary, john, chris) {
      this.mary = mary;
      this.chris = chris;
      this.john = john;
      return mary.setParents([john], {
        logging: function(sql) {
          if (sql.match(/INSERT/)) {
            count++;
            expect(sql).to.have.string('preexisting_child');
            expect(sql).to.have.string('preexisting_parent');
          }
        }
      });
    }).then(function() {
      return this.mary.addParent(this.chris, {
        logging: function(sql) {
          if (sql.match(/INSERT/)) {
              count++;
              expect(sql).to.have.string('preexisting_child');
              expect(sql).to.have.string('preexisting_parent');
          }
        }
      });
    }).then(function() {
      return this.john.getChildren({
        logging: function(sql) {
          count++;
          var whereClause = sql.split('FROM')[1]; // look only in the whereClause
          expect(whereClause).to.have.string('preexisting_child');
          expect(whereClause).to.have.string('preexisting_parent');
        }
      });
    }).then(function(children) {
      expect(count).to.be.equal(3);
      expect(_.map(children, 'id')).to.have.members([this.mary.id]);
    });
  });
});


'use strict';

/* jshint -W030 */
var chai = require('chai')
  , expect = chai.expect
  , Support = require(__dirname + '/../../support')
  , dialect = Support.getTestDialect()
  , config = require(__dirname + '/../../../config/config')
  , DataTypes = require(__dirname + '/../../../../lib/data-types')
  , _ = require('lodash');

if (dialect.match(/^postgres/)) {
  describe('[POSTGRES Specific] Sequelize', function() {
    it('should correctly parse the moment based timezone', function() {
      var options = _.extend(this.sequelize.options, { timezone: 'Asia/Kolkata', timestamps: true });
      var sequelize = Support.createSequelizeInstance(options);

      var tzTable = sequelize.define('tz_table', { foo: DataTypes.STRING });
      return tzTable.sync({force: true}).then(function() {
        return tzTable.create({foo: 'test'}).then(function(row) {
          expect(row).to.be.not.null;
        }).catch(function(e) {
          expect(e).to.be.null;
        });
      })
    });
  });
}


'use strict';

var Utils = require('./../utils')
  , Helpers = require('./helpers')
  , _ = require('lodash')
  , Association = require('./base')
  , util = require('util');

/**
 * One-to-one association
 *
 * In the API reference below, replace `Association` with the actual name of your association, e.g. for `User.hasOne(Project)` the getter will be `user.getProject()`.
 * This is almost the same as `belongsTo` with one exception. The foreign key will be defined on the target model.
 *
 * @mixin HasOne
 */
var HasOne = function(srcModel, targetModel, options) {
  Association.call(this);

  this.associationType = 'HasOne';
  this.source = srcModel;
  this.target = targetModel;
  this.options = options;
  this.isSingleAssociation = true;
  this.isSelfAssociation = (this.source === this.target);
  this.as = this.options.as;
  this.foreignKeyAttribute = {};

  if (this.as) {
    this.isAliased = true;
    this.options.name = {
      singular: this.as
    };
  } else {
    this.as = this.target.options.name.singular;
    this.options.name = this.target.options.name;
  }

  if (_.isObject(this.options.foreignKey)) {
    this.foreignKeyAttribute = this.options.foreignKey;
    this.foreignKey = this.foreignKeyAttribute.name || this.foreignKeyAttribute.fieldName;
  } else if (this.options.foreignKey) {
    this.foreignKey = this.options.foreignKey;
  }

  if (!this.foreignKey) {
    this.foreignKey = Utils.camelizeIf(
      [
        Utils.underscoredIf(Utils.singularize(this.source.name), this.target.options.underscored),
        this.source.primaryKeyAttribute
      ].join('_'),
      !this.source.options.underscored
    );
  }

  this.sourceIdentifier = this.source.primaryKeyAttribute;
  this.associationAccessor = this.as;
  this.options.useHooks = options.useHooks;

  if (this.target.rawAttributes[this.foreignKey]) {
    this.identifierField = this.target.rawAttributes[this.foreignKey].field || this.foreignKey;
  }

  // Get singular name, trying to uppercase the first letter, unless the model forbids it
  var singular = Utils.uppercaseFirst(this.options.name.singular);

  this.accessors = {
    /**
     * Get the associated instance.
     *
     * @param {Object} [options]
     * @param {String|Boolean} [options.scope] Apply a scope on the related model, or remove its default scope by passing false
     * @param {String} [options.schema] Apply a schema on the related model
     * @return {Promise<Instance>}
     * @method getAssociation
     */
    get: 'get' + singular,
    /**
     * Set the associated model.
     *
     * @param {Instance|String|Number} [newAssociation] An instance or the primary key of an instance to associate with this. Pass `null` or `undefined` to remove the association.
     * @param {Object} [options] Options passed to getAssociation and `target.save`
     * @return {Promise}
     * @method setAssociation
     */
    set: 'set' + singular,
    /**
     * Create a new instance of the associated model and associate it with this.
     *
     * @param {Object} [values]
     * @param {Object} [options] Options passed to `target.create` and setAssociation.
     * @return {Promise}
     * @method createAssociation
     */
    create: 'create' + singular
  };
};

util.inherits(HasOne, Association);

// the id is in the target table
HasOne.prototype.injectAttributes = function() {
  var newAttributes = {}
    , keyType = this.source.rawAttributes[this.source.primaryKeyAttribute].type;

  newAttributes[this.foreignKey] = _.defaults({}, this.foreignKeyAttribute, {
    type: this.options.keyType || keyType,
    allowNull : true
  });
  Utils.mergeDefaults(this.target.rawAttributes, newAttributes);

  this.identifierField = this.target.rawAttributes[this.foreignKey].field || this.foreignKey;

  if (this.options.constraints !== false) {
    var target = this.target.rawAttributes[this.foreignKey] || newAttributes[this.foreignKey];
    this.options.onDelete = this.options.onDelete || (target.allowNull ? 'SET NULL' : 'CASCADE');
    this.options.onUpdate = this.options.onUpdate || 'CASCADE';
  }

  Helpers.addForeignKeyConstraints(this.target.rawAttributes[this.foreignKey], this.source, this.target, this.options);

  // Sync attributes and setters/getters to Model prototype
  this.target.refreshAttributes();

  Helpers.checkNamingCollision(this);

  return this;
};

HasOne.prototype.injectGetter = function(instancePrototype) {
  var association = this;

  instancePrototype[this.accessors.get] = function(options) {
    var where = {};
    where[association.foreignKey] = this.get(association.sourceIdentifier);

    options = association.target.__optClone(options) || {};

    options.where = {
      $and: [
        options.where,
        where
      ]
    };

    if (options.limit === undefined) options.limit = null;

    var model = association.target;
    if (options.hasOwnProperty('scope')) {
      if (!options.scope) {
        model = model.unscoped();
      } else {
        model = model.scope(options.scope);
      }
    }

    if (options.hasOwnProperty('schema')) {
      model = model.schema(options.schema, options.schemaDelimiter);
    }

    return model.find(options);
  };

  return this;
};

HasOne.prototype.injectSetter = function(instancePrototype) {
  var association = this;

  instancePrototype[this.accessors.set] = function(associatedInstance, options) {
    var instance = this;

    options = options || {};
    options.scope = false;
    return instance[association.accessors.get](options).then(function(oldInstance) {
      if (oldInstance) {
        oldInstance[association.foreignKey] = null;
        return oldInstance.save(_.extend({}, options, {
          fields: [association.foreignKey],
          allowNull: [association.foreignKey],
          association: true
        }));
      }
    }).then(function() {
      if (associatedInstance) {
        if (!(associatedInstance instanceof association.target.Instance)) {
          var tmpInstance = {};
          tmpInstance[association.target.primaryKeyAttribute] = associatedInstance;
          associatedInstance = association.target.build(tmpInstance, {
            isNewRecord: false
          });
        }
        associatedInstance.set(association.foreignKey, instance.get(association.sourceIdentifier));
        return associatedInstance.save(options);
      }
      return null;
    });
  };

  return this;
};

HasOne.prototype.injectCreator = function(instancePrototype) {
  var association = this;

  instancePrototype[this.accessors.create] = function(values, options) {
    var instance = this;
    values = values || {};
    options = options || {};

    values[association.foreignKey] = instance.get(association.sourceIdentifier);
    if (options.fields) options.fields.push(association.foreignKey);
    return association.target.create(values, options);
  };

  return this;
};

module.exports = HasOne;


'use strict';

/* jshint -W030 */
var chai = require('chai')
  , Sequelize = require('../../../index')
  , expect = chai.expect
  , Support = require(__dirname + '/../support')
  , DataTypes = require(__dirname + '/../../../lib/data-types')
  , Promise = Sequelize.Promise
  , dialect = Support.getTestDialect();

var sortById = function(a, b) {
  return a.id < b.id ? -1 : 1;
};

describe(Support.getTestDialectTeaser('Includes with schemas'), function() {
  describe('findAll', function() {
    this.timeout(30000);
    beforeEach(function() {
      var self = this;
      this.fixtureA = function() {
        return self.sequelize.dropAllSchemas().then(function() {
          return self.sequelize.createSchema('account').then(function() {
            var AccUser = self.sequelize.define('AccUser', {}, {schema: 'account'})
              , Company = self.sequelize.define('Company', {
                  name: DataTypes.STRING
                }, {schema: 'account'})
              , Product = self.sequelize.define('Product', {
                  title: DataTypes.STRING
                }, {schema: 'account'})
              , Tag = self.sequelize.define('Tag', {
                  name: DataTypes.STRING
                }, {schema: 'account'})
              , Price = self.sequelize.define('Price', {
                  value: DataTypes.FLOAT
                }, {schema: 'account'})
              , Customer = self.sequelize.define('Customer', {
                  name: DataTypes.STRING
              }, {schema: 'account'})
              , Group = self.sequelize.define('Group', {
                  name: DataTypes.STRING
                }, {schema: 'account'})
              , GroupMember = self.sequelize.define('GroupMember', {

                }, {schema: 'account'})
              , Rank = self.sequelize.define('Rank', {
                  name: DataTypes.STRING,
                  canInvite: {
                    type: DataTypes.INTEGER,
                    defaultValue: 0
                  },
                  canRemove: {
                    type: DataTypes.INTEGER,
                    defaultValue: 0
                  },
                  canPost: {
                    type: DataTypes.INTEGER,
                    defaultValue: 0
                  }
                }, {schema: 'account'});

            self.models = {
              AccUser: AccUser,
              Company: Company,
              Product: Product,
              Tag: Tag,
              Price: Price,
              Customer: Customer,
              Group: Group,
              GroupMember: GroupMember,
              Rank: Rank
            };

            AccUser.hasMany(Product);
            Product.belongsTo(AccUser);

            Product.belongsToMany(Tag, {through: 'product_tag'});
            Tag.belongsToMany(Product, {through: 'product_tag'});
            Product.belongsTo(Tag, {as: 'Category'});
            Product.belongsTo(Company);

            Product.hasMany(Price);
            Price.belongsTo(Product);

            AccUser.hasMany(GroupMember, {as: 'Memberships'});
            GroupMember.belongsTo(AccUser);
            GroupMember.belongsTo(Rank);
            GroupMember.belongsTo(Group);
            Group.hasMany(GroupMember, {as: 'Memberships'});

            return self.sequelize.sync({force: true}).then(function() {
              return Promise.all([
                Group.bulkCreate([
                  {name: 'Developers'},
                  {name: 'Designers'},
                  {name: 'Managers'}
                ]),
                Company.bulkCreate([
                  {name: 'Sequelize'},
                  {name: 'Coca Cola'},
                  {name: 'Bonanza'},
                  {name: 'NYSE'},
                  {name: 'Coshopr'}
                ]),
                Rank.bulkCreate([
                  {name: 'Admin', canInvite: 1, canRemove: 1, canPost: 1},
                  {name: 'Trustee', canInvite: 1, canRemove: 0, canPost: 1},
                  {name: 'Member', canInvite: 1, canRemove: 0, canPost: 0}
                ]),
                Tag.bulkCreate([
                  {name: 'A'},
                  {name: 'B'},
                  {name: 'C'},
                  {name: 'D'},
                  {name: 'E'}
                ])
              ]).then(function() {
                return Promise.all([
                  Group.findAll(),
                  Company.findAll(),
                  Rank.findAll(),
                  Tag.findAll()
                ]);
              }).spread(function (groups, companies, ranks, tags) {
                return Promise.each([0, 1, 2, 3, 4], function (i) {
                  return Promise.all([
                    AccUser.create(),
                    Product.bulkCreate([
                      {title: 'Chair'},
                      {title: 'Desk'},
                      {title: 'Bed'},
                      {title: 'Pen'},
                      {title: 'Monitor'}
                    ]).then(function() {
                      return Product.findAll();
                    })
                  ]).spread(function (user, products) {
                    var groupMembers = [
                      {AccUserId: user.id, GroupId: groups[0].id, RankId: ranks[0].id},
                      {AccUserId: user.id, GroupId: groups[1].id, RankId: ranks[2].id}
                    ];
                    if (i < 3) {
                      groupMembers.push({AccUserId: user.id, GroupId: groups[2].id, RankId: ranks[1].id});
                    }

                    return Promise.join(
                      GroupMember.bulkCreate(groupMembers),
                      user.setProducts([
                        products[(i * 5) + 0],
                        products[(i * 5) + 1],
                        products[(i * 5) + 3]
                      ]),
                      Promise.join(
                        products[(i * 5) + 0].setTags([
                          tags[0],
                          tags[2]
                        ]),
                        products[(i * 5) + 1].setTags([
                          tags[1]
                        ]),
                        products[(i * 5) + 0].setCategory(tags[1]),
                        products[(i * 5) + 2].setTags([
                          tags[0]
                        ]),
                        products[(i * 5) + 3].setTags([
                          tags[0]
                        ])
                      ),
                      Promise.join(
                        products[(i * 5) + 0].setCompany(companies[4]),
                        products[(i * 5) + 1].setCompany(companies[3]),
                        products[(i * 5) + 2].setCompany(companies[2]),
                        products[(i * 5) + 3].setCompany(companies[1]),
                        products[(i * 5) + 4].setCompany(companies[0])
                      ),
                      Price.bulkCreate([
                        {ProductId: products[(i * 5) + 0].id, value: 5},
                        {ProductId: products[(i * 5) + 0].id, value: 10},
                        {ProductId: products[(i * 5) + 1].id, value: 5},
                        {ProductId: products[(i * 5) + 1].id, value: 10},
                        {ProductId: products[(i * 5) + 1].id, value: 15},
                        {ProductId: products[(i * 5) + 1].id, value: 20},
                        {ProductId: products[(i * 5) + 2].id, value: 20},
                        {ProductId: products[(i * 5) + 3].id, value: 20}
                      ])
                    );
                  });
                });
              });
            });
          });
        });
      };
    });

    it('should support an include with multiple different association types', function() {
      var self = this;

      return self.sequelize.dropAllSchemas().then(function() {
        return self.sequelize.createSchema('account').then(function() {
          var AccUser = self.sequelize.define('AccUser', {}, {schema: 'account'})
            , Product = self.sequelize.define('Product', {
                title: DataTypes.STRING
              }, {schema: 'account'})
            , Tag = self.sequelize.define('Tag', {
                name: DataTypes.STRING
              }, {schema: 'account'})
            , Price = self.sequelize.define('Price', {
                value: DataTypes.FLOAT
              }, {schema: 'account'})
            , Group = self.sequelize.define('Group', {
                name: DataTypes.STRING
              }, {schema: 'account'})
            , GroupMember = self.sequelize.define('GroupMember', {

              }, {schema: 'account'})
            , Rank = self.sequelize.define('Rank', {
                name: DataTypes.STRING,
                canInvite: {
                  type: DataTypes.INTEGER,
                  defaultValue: 0
                },
                canRemove: {
                  type: DataTypes.INTEGER,
                  defaultValue: 0
                }
              }, {schema: 'account'});

          AccUser.hasMany(Product);
          Product.belongsTo(AccUser);

          Product.belongsToMany(Tag, {through: 'product_tag'});
          Tag.belongsToMany(Product, {through: 'product_tag'});
          Product.belongsTo(Tag, {as: 'Category'});

          Product.hasMany(Price);
          Price.belongsTo(Product);

          AccUser.hasMany(GroupMember, {as: 'Memberships'});
          GroupMember.belongsTo(AccUser);
          GroupMember.belongsTo(Rank);
          GroupMember.belongsTo(Group);
          Group.hasMany(GroupMember, {as: 'Memberships'});

          return self.sequelize.sync({force: true}).then(function() {
            return Promise.all([
              Group.bulkCreate([
                {name: 'Developers'},
                {name: 'Designers'}
              ]).then(function() {
                return Group.findAll();
              }),
              Rank.bulkCreate([
                {name: 'Admin', canInvite: 1, canRemove: 1},
                {name: 'Member', canInvite: 1, canRemove: 0}
              ]).then(function() {
                return Rank.findAll();
              }),
              Tag.bulkCreate([
                {name: 'A'},
                {name: 'B'},
                {name: 'C'}
              ]).then(function() {
                return Tag.findAll();
              })
            ]).spread(function(groups, ranks, tags) {
              return Promise.each([0, 1, 2, 3, 4], function (i) {
                return Promise.all([
                  AccUser.create(),
                  Product.bulkCreate([
                    {title: 'Chair'},
                    {title: 'Desk'}
                  ]).then(function() {
                    return Product.findAll();
                  })
                ]).spread(function(user, products) {
                  return Promise.all([
                    GroupMember.bulkCreate([
                      {AccUserId: user.id, GroupId: groups[0].id, RankId: ranks[0].id},
                      {AccUserId: user.id, GroupId: groups[1].id, RankId: ranks[1].id}
                    ]),
                    user.setProducts([
                      products[(i * 2) + 0],
                      products[(i * 2) + 1]
                    ]),
                    products[(i * 2) + 0].setTags([
                      tags[0],
                      tags[2]
                    ]),
                    products[(i * 2) + 1].setTags([
                      tags[1]
                    ]),
                    products[(i * 2) + 0].setCategory(tags[1]),
                    Price.bulkCreate([
                      {ProductId: products[(i * 2) + 0].id, value: 5},
                      {ProductId: products[(i * 2) + 0].id, value: 10},
                      {ProductId: products[(i * 2) + 1].id, value: 5},
                      {ProductId: products[(i * 2) + 1].id, value: 10},
                      {ProductId: products[(i * 2) + 1].id, value: 15},
                      {ProductId: products[(i * 2) + 1].id, value: 20}
                    ])
                  ]);
                });
              });
            }).then(function() {
              return AccUser.findAll({
                include: [
                  {model: GroupMember, as: 'Memberships', include: [
                    Group,
                    Rank
                  ]},
                  {model: Product, include: [
                    Tag,
                    {model: Tag, as: 'Category'},
                    Price
                  ]}
                ],
                order: [
                  [AccUser.rawAttributes.id, 'ASC']
                ]
              }).then(function(users) {
                users.forEach(function(user) {
                  expect(user.Memberships).to.be.ok;
                  user.Memberships.sort(sortById);

                  expect(user.Memberships.length).to.equal(2);
                  expect(user.Memberships[0].Group.name).to.equal('Developers');
                  expect(user.Memberships[0].Rank.canRemove).to.equal(1);
                  expect(user.Memberships[1].Group.name).to.equal('Designers');
                  expect(user.Memberships[1].Rank.canRemove).to.equal(0);

                  user.Products.sort(sortById);
                  expect(user.Products.length).to.equal(2);
                  expect(user.Products[0].Tags.length).to.equal(2);
                  expect(user.Products[1].Tags.length).to.equal(1);
                  expect(user.Products[0].Category).to.be.ok;
                  expect(user.Products[1].Category).not.to.be.ok;

                  expect(user.Products[0].Prices.length).to.equal(2);
                  expect(user.Products[1].Prices.length).to.equal(4);
                });
              });
            });
          });
        });
      });
    });

    it('should support many levels of belongsTo', function() {
      var A = this.sequelize.define('a', {}, {schema: 'account'})
        , B = this.sequelize.define('b', {}, {schema: 'account'})
        , C = this.sequelize.define('c', {}, {schema: 'account'})
        , D = this.sequelize.define('d', {}, {schema: 'account'})
        , E = this.sequelize.define('e', {}, {schema: 'account'})
        , F = this.sequelize.define('f', {}, {schema: 'account'})
        , G = this.sequelize.define('g', {}, {schema: 'account'})
        , H = this.sequelize.define('h', {}, {schema: 'account'});

      A.belongsTo(B);
      B.belongsTo(C);
      C.belongsTo(D);
      D.belongsTo(E);
      E.belongsTo(F);
      F.belongsTo(G);
      G.belongsTo(H);

      var b, singles = [
        B,
        C,
        D,
        E,
        F,
        G,
        H
      ];

      return this.sequelize.sync().then(function() {
        return A.bulkCreate([
          {}, {}, {}, {}, {}, {}, {}, {}
        ]).then(function() {
          var previousInstance;
          return Promise.each(singles, function(model) {
            return model.create({}).then(function(instance) {
              if (previousInstance) {
                return previousInstance['set'+ Sequelize.Utils.uppercaseFirst(model.name)](instance).then(function() {
                  previousInstance = instance;
                });
              }
              previousInstance = b = instance;
              return void(0);
            });
          });
        }).then(function() {
          return A.findAll();
        }).then(function(as) {
          var promises = [];
          as.forEach(function(a) {
            promises.push(a.setB(b));
          });
          return Promise.all(promises);
        }).then(function() {
          return A.findAll({
            include: [
              {model: B, include: [
                {model: C, include: [
                  {model: D, include: [
                    {model: E, include: [
                      {model: F, include: [
                        {model: G, include: [
                          {model: H}
                        ]}
                      ]}
                    ]}
                  ]}
                ]}
              ]}
            ]
          }).then(function(as) {
            expect(as.length).to.be.ok;
            as.forEach(function(a) {
              expect(a.b.c.d.e.f.g.h).to.be.ok;
            });
          });
        });
      });
    });

    it('should support ordering with only belongsTo includes', function() {
      var User = this.sequelize.define('SpecialUser', {}, {schema: 'account'})
        , Item = this.sequelize.define('Item', {'test': DataTypes.STRING}, {schema: 'account'})
        , Order = this.sequelize.define('Order', {'position': DataTypes.INTEGER}, {schema: 'account'});

      User.belongsTo(Item, {'as': 'itemA', foreignKey: 'itemA_id'});
      User.belongsTo(Item, {'as': 'itemB', foreignKey: 'itemB_id'});
      User.belongsTo(Order);

      return this.sequelize.sync().then(function() {
        return Promise.all([
          User.bulkCreate([{}, {}, {}]),
          Item.bulkCreate([
            {'test': 'abc'},
            {'test': 'def'},
            {'test': 'ghi'},
            {'test': 'jkl'}
          ]),
          Order.bulkCreate([
            {'position': 2},
            {'position': 3},
            {'position': 1}
          ])
        ]).then(function() {
          return Promise.all([
            User.findAll(),
            Item.findAll({order: ['id']}),
            Order.findAll({order: ['id']})
          ]);
        }).spread(function(users, items, orders) {
          return Promise.all([
            users[0].setItemA(items[0]),
            users[0].setItemB(items[1]),
            users[0].setOrder(orders[2]),
            users[1].setItemA(items[2]),
            users[1].setItemB(items[3]),
            users[1].setOrder(orders[1]),
            users[2].setItemA(items[0]),
            users[2].setItemB(items[3]),
            users[2].setOrder(orders[0])
          ]);
        }).spread(function() {
          return User.findAll({
            'include': [
              {'model': Item, 'as': 'itemA', where: {test: 'abc'}},
              {'model': Item, 'as': 'itemB'},
              Order],
            'order': [
              [Order, 'position']
            ]
          }).then(function(as) {
            expect(as.length).to.eql(2);
            expect(as[0].itemA.test).to.eql('abc');
            expect(as[1].itemA.test).to.eql('abc');
            expect(as[0].Order.position).to.eql(1);
            expect(as[1].Order.position).to.eql(2);
          });
        });
      });
    });

    it('should include attributes from through models', function() {
      var Product = this.sequelize.define('Product', {
            title: DataTypes.STRING
          }, {schema: 'account'})
        , Tag = this.sequelize.define('Tag', {
            name: DataTypes.STRING
          }, {schema: 'account'})
        , ProductTag = this.sequelize.define('ProductTag', {
            priority: DataTypes.INTEGER
        }, {schema: 'account'});

      Product.belongsToMany(Tag, {through: ProductTag});
      Tag.belongsToMany(Product, {through: ProductTag});

      return this.sequelize.sync({force: true}).then(function() {
        return Promise.all([
          Product.bulkCreate([
            {title: 'Chair'},
            {title: 'Desk'},
            {title: 'Dress'}
          ]),
          Tag.bulkCreate([
            {name: 'A'},
            {name: 'B'},
            {name: 'C'}
          ])
        ]).spread(function() {
          return Promise.all([
            Product.findAll(),
            Tag.findAll()
          ]);
        }).spread(function(products, tags) {
          return Promise.all([
            products[0].addTag(tags[0], {priority: 1}),
            products[0].addTag(tags[1], {priority: 2}),
            products[1].addTag(tags[1], {priority: 1}),
            products[2].addTag(tags[0], {priority: 3}),
            products[2].addTag(tags[1], {priority: 1}),
            products[2].addTag(tags[2], {priority: 2})
          ]);
        }).spread(function() {
          return Product.findAll({
            include: [
              {model: Tag}
            ],
            order: [
              ['id', 'ASC'],
              [Tag, 'id', 'ASC']
            ]
          }).then(function(products) {
            expect(products[0].Tags[0].ProductTag.priority).to.equal(1);
            expect(products[0].Tags[1].ProductTag.priority).to.equal(2);
            expect(products[1].Tags[0].ProductTag.priority).to.equal(1);
            expect(products[2].Tags[0].ProductTag.priority).to.equal(3);
            expect(products[2].Tags[1].ProductTag.priority).to.equal(1);
            expect(products[2].Tags[2].ProductTag.priority).to.equal(2);
          });
        });
      });
    });

    it('should support a required belongsTo include', function() {
      var User = this.sequelize.define('User', {}, {schema: 'account'})
        , Group = this.sequelize.define('Group', {}, {schema: 'account'});

      User.belongsTo(Group);

      return this.sequelize.sync({force: true}).then(function() {
        return Promise.all([
          Group.bulkCreate([{}, {}]),
          User.bulkCreate([{}, {}, {}])
        ]).then(function() {
          return Promise.all([
            Group.findAll(),
            User.findAll()
          ]);
        }).spread(function(groups, users) {
          return users[2].setGroup(groups[1]);
        }).then(function() {
          return User.findAll({
            include: [
              {model: Group, required: true}
            ]
          }).then(function(users) {
            expect(users.length).to.equal(1);
            expect(users[0].Group).to.be.ok;
          });
        });
      });
    });

    it('should be possible to extend the on clause with a where option on a belongsTo include', function() {
      var User = this.sequelize.define('User', {}, {schema: 'account'})
        , Group = this.sequelize.define('Group', {
            name: DataTypes.STRING
          }, {schema: 'account'});

      User.belongsTo(Group);

      return this.sequelize.sync({force: true}).then(function() {
        return Promise.all([
          Group.bulkCreate([
            {name: 'A'},
            {name: 'B'}
          ]),
          User.bulkCreate([{}, {}])
        ]).then(function() {
          return Promise.all([
            Group.findAll(),
            User.findAll()
          ]);
        }).spread(function(groups, users) {
          return Promise.all([
            users[0].setGroup(groups[1]),
            users[1].setGroup(groups[0])
          ]);
        }).then(function() {
          return User.findAll({
            include: [
              {model: Group, where: {name: 'A'}}
            ]
          }).then(function(users) {
            expect(users.length).to.equal(1);
            expect(users[0].Group).to.be.ok;
            expect(users[0].Group.name).to.equal('A');
          });
        });
      });
    });

    it('should be possible to extend the on clause with a where option on a belongsTo include', function() {
      var User = this.sequelize.define('User', {}, {schema: 'account'})
        , Group = this.sequelize.define('Group', {
            name: DataTypes.STRING
          }, {schema: 'account'});

      User.belongsTo(Group);

      return this.sequelize.sync({force: true}).then(function() {
        return Promise.all([
          Group.bulkCreate([
            {name: 'A'},
            {name: 'B'}
          ]),
          User.bulkCreate([{}, {}])
        ]).then(function() {
          return Promise.all([
            Group.findAll(),
            User.findAll()
          ]);
        }).spread(function(groups, users) {
          return Promise.all([
            users[0].setGroup(groups[1]),
            users[1].setGroup(groups[0])
          ]);
        }).then(function() {
          return User.findAll({
            include: [
              {model: Group, required: true}
            ]
          }).then(function(users) {
            users.forEach(function(user) {
              expect(user.Group).to.be.ok;
            });
          });
        });
      });
    });

    it('should be possible to define a belongsTo include as required with child hasMany with limit', function() {
      var User = this.sequelize.define('User', {}, {schema: 'account'}) , Group = this.sequelize.define('Group', {
            name: DataTypes.STRING
          }, {schema: 'account'})
        , Category = this.sequelize.define('Category', {
            category: DataTypes.STRING
          }, {schema: 'account'});

      User.belongsTo(Group);
      Group.hasMany(Category);

      return this.sequelize.sync({force: true}).then(function() {
        return Promise.all([
          Group.bulkCreate([
            {name: 'A'},
            {name: 'B'}
          ]),
          User.bulkCreate([{}, {}]),
          Category.bulkCreate([{}, {}])
        ]).then(function() {
          return Promise.all([
            Group.findAll(),
            User.findAll(),
            Category.findAll()
          ]);
        }).spread(function(groups, users, categories) {
          var promises = [
            users[0].setGroup(groups[1]),
            users[1].setGroup(groups[0])
          ];
          groups.forEach(function(group) {
            promises.push(group.setCategories(categories));
          });
          return Promise.all(promises);
        }).then(function() {
          return User.findAll({
            include: [
              {model: Group, required: true, include: [
                {model: Category}
              ]}
            ],
            limit: 1
          }).then(function(users) {
            expect(users.length).to.equal(1);
            users.forEach(function(user) {
              expect(user.Group).to.be.ok;
              expect(user.Group.Categories).to.be.ok;
            });
          });
        });
      });
    });

    it('should be possible to define a belongsTo include as required with child hasMany with limit and aliases', function() {
      var User = this.sequelize.define('User', {}, {schema: 'account'})
        , Group = this.sequelize.define('Group', {
            name: DataTypes.STRING
          }, {schema: 'account'})
        , Category = this.sequelize.define('Category', {
            category: DataTypes.STRING
          }, {schema: 'account'});

      User.belongsTo(Group, {as: 'Team'});
      Group.hasMany(Category, {as: 'Tags'});

      return this.sequelize.sync({force: true}).then(function() {
        return Promise.all([
          Group.bulkCreate([
            {name: 'A'},
            {name: 'B'}
          ]),
          User.bulkCreate([{}, {}]),
          Category.bulkCreate([{}, {}])
        ]).then(function() {
          return Promise.all([
            Group.findAll(),
            User.findAll(),
            Category.findAll()
          ]);
        }).spread(function(groups, users, categories) {
          var promises = [
            users[0].setTeam(groups[1]),
            users[1].setTeam(groups[0])
          ];
          groups.forEach(function(group) {
            promises.push(group.setTags(categories));
          });
          return Promise.all(promises);
        }).then(function() {
          return User.findAll({
            include: [
              {model: Group, required: true, as: 'Team', include: [
                {model: Category, as: 'Tags'}
              ]}
            ],
            limit: 1
          }).then(function(users) {
            expect(users.length).to.equal(1);
            users.forEach(function(user) {
              expect(user.Team).to.be.ok;
              expect(user.Team.Tags).to.be.ok;
            });
          });
        });
      });
    });

    it('should be possible to define a belongsTo include as required with child hasMany which is not required with limit', function() {
      var User = this.sequelize.define('User', {}, {schema: 'account'})
        , Group = this.sequelize.define('Group', {
            name: DataTypes.STRING
          }, {schema: 'account'})
        , Category = this.sequelize.define('Category', {
            category: DataTypes.STRING
          }, {schema: 'account'});

      User.belongsTo(Group);
      Group.hasMany(Category);

      return this.sequelize.sync({force: true}).then(function() {
        return Promise.all([
          Group.bulkCreate([
            {name: 'A'},
            {name: 'B'}
          ]),
          User.bulkCreate([{}, {}]),
          Category.bulkCreate([{}, {}])
        ]).then(function() {
          return Promise.all([
            Group.findAll(),
            User.findAll(),
            Category.findAll()
          ]);
        }).spread(function (groups, users, categories) {
          var promises = [
            users[0].setGroup(groups[1]),
            users[1].setGroup(groups[0])
          ];
          groups.forEach(function(group) {
            promises.push(group.setCategories(categories));
          });
          return Promise.all(promises);
        }).then(function() {
          return User.findAll({
            include: [
              {model: Group, required: true, include: [
                {model: Category, required: false}
              ]}
            ],
            limit: 1
          }).then(function(users) {
            expect(users.length).to.equal(1);
            users.forEach(function(user) {
              expect(user.Group).to.be.ok;
              expect(user.Group.Categories).to.be.ok;
            });
          });
        });
      });
    });

    it('should be possible to extend the on clause with a where option on a hasOne include', function() {
      var User = this.sequelize.define('User', {}, {schema: 'account'})
        , Project = this.sequelize.define('Project', {
            title: DataTypes.STRING
          }, {schema: 'account'});

      User.hasOne(Project, {as: 'LeaderOf'});

      return this.sequelize.sync({force: true}).then(function() {
        return Promise.all([
          Project.bulkCreate([
            {title: 'Alpha'},
            {title: 'Beta'}
          ]),
          User.bulkCreate([{}, {}])
        ]).then(function() {
          return Promise.all([
            Project.findAll(),
            User.findAll()
          ]);
        }).spread(function(projects, users) {
          return Promise.all([
            users[1].setLeaderOf(projects[1]),
            users[0].setLeaderOf(projects[0])
          ]);
        }).then(function() {
          return User.findAll({
            include: [
              {model: Project, as: 'LeaderOf', where: {title: 'Beta'}}
            ]
          }).then(function(users) {
            expect(users.length).to.equal(1);
            expect(users[0].LeaderOf).to.be.ok;
            expect(users[0].LeaderOf.title).to.equal('Beta');
          });
        });
      });
    });

    it('should be possible to extend the on clause with a where option on a hasMany include with a through model', function() {
      var Product = this.sequelize.define('Product', {
            title: DataTypes.STRING
          }, {schema: 'account'})
        , Tag = this.sequelize.define('Tag', {
            name: DataTypes.STRING
          }, {schema: 'account'})
        , ProductTag = this.sequelize.define('ProductTag', {
            priority: DataTypes.INTEGER
        }, {schema: 'account'});

      Product.belongsToMany(Tag, {through: ProductTag});
      Tag.belongsToMany(Product, {through: ProductTag});

      return this.sequelize.sync({force: true}).then(function() {
        return Promise.all([
          Product.bulkCreate([
            {title: 'Chair'},
            {title: 'Desk'},
            {title: 'Dress'}
          ]),
          Tag.bulkCreate([
            {name: 'A'},
            {name: 'B'},
            {name: 'C'}
          ])
        ]).then(function() {
          return Promise.all([
            Product.findAll(),
            Tag.findAll()
          ]);
        }).spread(function(products, tags) {
          return Promise.all([
            products[0].addTag(tags[0], {priority: 1}),
            products[0].addTag(tags[1], {priority: 2}),
            products[1].addTag(tags[1], {priority: 1}),
            products[2].addTag(tags[0], {priority: 3}),
            products[2].addTag(tags[1], {priority: 1}),
            products[2].addTag(tags[2], {priority: 2})
          ]);
        }).then(function() {
          return Product.findAll({
            include: [
              {model: Tag, where: {name: 'C'}}
            ]
          }).then(function(products) {
            expect(products.length).to.equal(1);
            expect(products[0].Tags.length).to.equal(1);
          });
        });
      });
    });

    it('should be possible to extend the on clause with a where option on nested includes', function() {
      var User = this.sequelize.define('User', {
            name: DataTypes.STRING
          }, {schema: 'account'})
        , Product = this.sequelize.define('Product', {
            title: DataTypes.STRING
          }, {schema: 'account'})
        , Tag = this.sequelize.define('Tag', {
            name: DataTypes.STRING
          }, {schema: 'account'})
        , Price = this.sequelize.define('Price', {
            value: DataTypes.FLOAT
          }, {schema: 'account'})
        , Group = this.sequelize.define('Group', {
            name: DataTypes.STRING
          }, {schema: 'account'})
        , GroupMember = this.sequelize.define('GroupMember', {

          }, {schema: 'account'})
        , Rank = this.sequelize.define('Rank', {
            name: DataTypes.STRING,
            canInvite: {
              type: DataTypes.INTEGER,
              defaultValue: 0
            },
            canRemove: {
              type: DataTypes.INTEGER,
              defaultValue: 0
            }
          }, {schema: 'account'});

      User.hasMany(Product);
      Product.belongsTo(User);

      Product.belongsToMany(Tag, {through: 'product_tag'});
      Tag.belongsToMany(Product, {through: 'product_tag'});
      Product.belongsTo(Tag, {as: 'Category'});

      Product.hasMany(Price);
      Price.belongsTo(Product);

      User.hasMany(GroupMember, {as: 'Memberships'});
      GroupMember.belongsTo(User);
      GroupMember.belongsTo(Rank);
      GroupMember.belongsTo(Group);
      Group.hasMany(GroupMember, {as: 'Memberships'});

      return this.sequelize.sync({force: true}).then(function() {
        return Promise.all([
          Group.bulkCreate([
            {name: 'Developers'},
            {name: 'Designers'}
          ]),
          Rank.bulkCreate([
            {name: 'Admin', canInvite: 1, canRemove: 1},
            {name: 'Member', canInvite: 1, canRemove: 0}
          ]),
          Tag.bulkCreate([
            {name: 'A'},
            {name: 'B'},
            {name: 'C'}
          ])
        ]).then(function() {
          return Promise.all([
            Group.findAll(),
            Rank.findAll(),
            Tag.findAll()
          ]);
        }).spread(function(groups, ranks, tags) {
          return Promise.resolve([0, 1, 2, 3, 4]).each(function (i) {
            return Promise.all([
              User.create({name: 'FooBarzz'}),
              Product.bulkCreate([
                {title: 'Chair'},
                {title: 'Desk'}
              ]).then(function() {
                return Product.findAll();
              })
            ]).spread(function(user, products) {
              return Promise.all([
                GroupMember.bulkCreate([
                  {UserId: user.id, GroupId: groups[0].id, RankId: ranks[0].id},
                  {UserId: user.id, GroupId: groups[1].id, RankId: ranks[1].id}
                ]),
                user.setProducts([
                  products[(i * 2) + 0],
                  products[(i * 2) + 1]
                ]),
                products[(i * 2) + 0].setTags([
                  tags[0],
                  tags[2]
                ]),
                products[(i * 2) + 1].setTags([
                  tags[1]
                ]),
                products[(i * 2) + 0].setCategory(tags[1]),
                Price.bulkCreate([
                  {ProductId: products[(i * 2) + 0].id, value: 5},
                  {ProductId: products[(i * 2) + 0].id, value: 10},
                  {ProductId: products[(i * 2) + 1].id, value: 5},
                  {ProductId: products[(i * 2) + 1].id, value: 10},
                  {ProductId: products[(i * 2) + 1].id, value: 15},
                  {ProductId: products[(i * 2) + 1].id, value: 20}
                ])
              ]);
            });
          });
        }).then(function(){
          return User.findAll({
            include: [
              {model: GroupMember, as: 'Memberships', include: [
                Group,
                {model: Rank, where: {name: 'Admin'}}
              ]},
              {model: Product, include: [
                Tag,
                {model: Tag, as: 'Category'},
                {model: Price, where: {
                  value: {
                    gt: 15
                  }
                }}
              ]}
            ],
            order: [
              ['id', 'ASC']
            ]
          }).then(function(users) {
            users.forEach(function(user) {
              expect(user.Memberships.length).to.equal(1);
              expect(user.Memberships[0].Rank.name).to.equal('Admin');
              expect(user.Products.length).to.equal(1);
              expect(user.Products[0].Prices.length).to.equal(1);
            });
          });
        });
      });
    });

    it('should be possible to use limit and a where with a belongsTo include', function() {
      var User = this.sequelize.define('User', {}, {schema: 'account'})
        , Group = this.sequelize.define('Group', {
            name: DataTypes.STRING
          }, {schema: 'account'});

      User.belongsTo(Group);

      return this.sequelize.sync({force: true}).then(function() {
        return Promise.props({
          groups: Group.bulkCreate([
            {name: 'A'},
            {name: 'B'}
          ]).then(function() {
            return Group.findAll();
          }),
          users: User.bulkCreate([{}, {}, {}, {}]).then(function() {
            return User.findAll();
          })
        }).then(function (results) {
          return Promise.join(
            results.users[1].setGroup(results.groups[0]),
            results.users[2].setGroup(results.groups[0]),
            results.users[3].setGroup(results.groups[1]),
            results.users[0].setGroup(results.groups[0])
          );
        }).then(function () {
          return User.findAll({
            include: [
              {model: Group, where: {name: 'A'}}
            ],
            limit: 2
          }).then(function(users) {
            expect(users.length).to.equal(2);

            users.forEach(function(user) {
              expect(user.Group.name).to.equal('A');
            });
          });
        });
      });
    });

    it('should be possible use limit, attributes and a where on a belongsTo with additional hasMany includes', function() {
      var self = this;
      return this.fixtureA().then(function () {
        return self.models.Product.findAll({
          attributes: ['title'],
          include: [
            {model: self.models.Company, where: {name: 'NYSE'}},
            {model: self.models.Tag},
            {model: self.models.Price}
          ],
          limit: 3,
          order: [
            ['id', 'ASC']
          ]
        }).then(function(products) {
          expect(products.length).to.equal(3);

          products.forEach(function(product) {
            expect(product.Company.name).to.equal('NYSE');
            expect(product.Tags.length).to.be.ok;
            expect(product.Prices.length).to.be.ok;
          });
        });
      });
    });

    it('should be possible to use limit and a where on a hasMany with additional includes', function() {
      var self = this;
      return this.fixtureA().then(function () {
        return self.models.Product.findAll({
          include: [
            {model: self.models.Company},
            {model: self.models.Tag},
            {model: self.models.Price, where: {
              value: {gt: 5}
            }}
          ],
          limit: 6,
          order: [
            ['id', 'ASC']
          ]
        }).then(function(products) {
          expect(products.length).to.equal(6);

          products.forEach(function(product) {
            expect(product.Tags.length).to.be.ok;
            expect(product.Prices.length).to.be.ok;

            product.Prices.forEach(function(price) {
              expect(price.value).to.be.above(5);
            });
          });
        });
      });
    });

    it('should be possible to use limit and a where on a hasMany with a through model with additional includes', function() {
      var self = this;
      return this.fixtureA().then(function () {
        return self.models.Product.findAll({
          include: [
            {model: self.models.Company},
            {model: self.models.Tag, where: {name: ['A', 'B', 'C']}},
            {model: self.models.Price}
          ],
          limit: 10,
          order: [
            ['id', 'ASC']
          ]
        }).then(function(products) {
          expect(products.length).to.equal(10);

          products.forEach(function(product) {
            expect(product.Tags.length).to.be.ok;
            expect(product.Prices.length).to.be.ok;

            product.Tags.forEach(function(tag) {
              expect(['A', 'B', 'C']).to.include(tag.name);
            });
          });
        });
      });
    });

    it('should support including date fields, with the correct timeszone', function() {
      var User = this.sequelize.define('user', {
            dateField: Sequelize.DATE
          }, {timestamps: false, schema: 'account'})
        , Group = this.sequelize.define('group', {
            dateField: Sequelize.DATE
          }, {timestamps: false, schema: 'account'});

      User.belongsToMany(Group, {through: 'group_user'});
      Group.belongsToMany(User, {through: 'group_user'});

      return this.sequelize.sync().then(function() {
        return User.create({ dateField: Date.UTC(2014, 1, 20) }).then(function(user) {
          return Group.create({ dateField: Date.UTC(2014, 1, 20) }).then(function(group) {
            return user.addGroup(group).then(function() {
              return User.findAll({
                where: {
                  id: user.id
                },
                include: [Group]
              }).then(function(users) {
                if (dialect === 'sqlite') {
                  expect(new Date(users[0].dateField).getTime()).to.equal(Date.UTC(2014, 1, 20));
                  expect(new Date(users[0].groups[0].dateField).getTime()).to.equal(Date.UTC(2014, 1, 20));
                } else {
                  expect(users[0].dateField.getTime()).to.equal(Date.UTC(2014, 1, 20));
                  expect(users[0].groups[0].dateField.getTime()).to.equal(Date.UTC(2014, 1, 20));
                }
              });
            });
          });
        });
      });
    });

  });

  describe('findOne', function() {
    ([ true, false ]).forEach(function (useNewReferencesStyle) {
    it('should work with schemas', function() {
      var self = this;
      var UserModel = this.sequelize.define('User', {
        Id: {
          type: DataTypes.INTEGER,
          primaryKey: true
        },
        Name: DataTypes.STRING,
        UserType: DataTypes.INTEGER,
        Email: DataTypes.STRING,
        PasswordHash: DataTypes.STRING,
        Enabled: {
          type: DataTypes.BOOLEAN
        },
        CreatedDatetime: DataTypes.DATE,
        UpdatedDatetime: DataTypes.DATE
      }, {
        schema: 'hero',
        tableName: 'User',
        timestamps: false
      });

      var UserIdColumn = useNewReferencesStyle
        ? { type: Sequelize.INTEGER, references: UserModel, referencesKey: 'Id' }
        : { type: Sequelize.INTEGER, references: { model: UserModel, key: 'Id' } }
        ;

      var ResumeModel = this.sequelize.define('Resume', {
        Id: {
          type: Sequelize.INTEGER,
          primaryKey: true
        },
        UserId: UserIdColumn,
        Name: Sequelize.STRING,
        Contact: Sequelize.STRING,
        School: Sequelize.STRING,
        WorkingAge: Sequelize.STRING,
        Description: Sequelize.STRING,
        PostType: Sequelize.INTEGER,
        RefreshDatetime: Sequelize.DATE,
        CreatedDatetime: Sequelize.DATE
      }, {
        schema: 'hero',
        tableName: 'resume',
        timestamps: false
      });

      UserModel.hasOne(ResumeModel, {
        foreignKey: 'UserId',
        as: 'Resume'
      });

      ResumeModel.belongsTo(UserModel, {
        foreignKey: 'UserId'
      });

      return self.sequelize.dropAllSchemas().then(function() {
        return self.sequelize.createSchema('hero');
      }).then(function() {
        return self.sequelize.sync({force: true}).then(function() {
          return UserModel.find({
            where: {
              Id: 1
            },
            include: [{
              model: ResumeModel,
              as: 'Resume'
            }]
          });
        });
      });
    });
    });
  });
});


'use strict';

/* jshint -W030 */
var chai = require('chai')
  , expect = chai.expect
  , Support = require(__dirname + '/../../support')
  , dialect = Support.getTestDialect()
  , config = require(__dirname + '/../../../config/config')
  , DataTypes = require(__dirname + '/../../../../lib/data-types');

if (dialect.match(/^postgres/)) {
  describe('[POSTGRES Specific] associations', function() {
    describe('many-to-many', function() {
      describe('where tables have the same prefix', function() {
        it('should create a table wp_table1wp_table2s', function() {
          var Table2 = this.sequelize.define('wp_table2', {foo: DataTypes.STRING})
            , Table1 = this.sequelize.define('wp_table1', {foo: DataTypes.STRING});

          Table1.belongsToMany(Table2, { through: 'wp_table1swp_table2s' });
          Table2.belongsToMany(Table1, { through: 'wp_table1swp_table2s' });

          expect(this.sequelize.modelManager.getModel('wp_table1swp_table2s')).to.exist;
        });
      });

      describe('when join table name is specified', function() {
        beforeEach(function() {
          var Table2 = this.sequelize.define('ms_table1', {foo: DataTypes.STRING})
            , Table1 = this.sequelize.define('ms_table2', {foo: DataTypes.STRING});

          Table1.belongsToMany(Table2, {through: 'table1_to_table2'});
          Table2.belongsToMany(Table1, {through: 'table1_to_table2'});
        });

        it('should not use a combined name', function() {
          expect(this.sequelize.modelManager.getModel('ms_table1sms_table2s')).not.to.exist;
        });

        it('should use the specified name', function() {
          expect(this.sequelize.modelManager.getModel('table1_to_table2')).to.exist;
        });
      });
    });

    describe('HasMany', function() {
      describe('addDAO / getModel', function() {
        beforeEach(function() {
          var self = this;

          //prevent periods from occurring in the table name since they are used to delimit (table.column)
          this.User = this.sequelize.define('User' + config.rand(), { name: DataTypes.STRING });
          this.Task = this.sequelize.define('Task' + config.rand(), { name: DataTypes.STRING });
          this.users = null;
          this.tasks = null;

          this.User.belongsToMany(this.Task, {as: 'Tasks', through: 'usertasks'});
          this.Task.belongsToMany(this.User, {as: 'Users', through: 'usertasks'});

          var users = []
            , tasks = [];

          for (var i = 0; i < 5; ++i) {
            users[users.length] = {name: 'User' + Math.random()};
          }

          for (var x = 0; x < 5; ++x) {
            tasks[tasks.length] = {name: 'Task' + Math.random()};
          }

          return this.sequelize.sync({ force: true }).then(function() {
            return self.User.bulkCreate(users).then(function() {
              return self.Task.bulkCreate(tasks).then(function() {
                return self.User.findAll().then(function(_users) {
                  return self.Task.findAll().then(function(_tasks) {
                    self.user = _users[0];
                    self.task = _tasks[0];
                  });
                });
              });
            });
          });
        });

        it('should correctly add an association to the dao', function() {
          var self = this;

          return self.user.getTasks().then(function(_tasks) {
            expect(_tasks).to.have.length(0);
            return self.user.addTask(self.task).then(function() {
              return self.user.getTasks().then(function(_tasks) {
                expect(_tasks).to.have.length(1);
              });
            });
          });
        });
      });

      describe('removeDAO', function() {
        it('should correctly remove associated objects', function() {
          var self = this
            , users = []
            , tasks = [];

          //prevent periods from occurring in the table name since they are used to delimit (table.column)
          this.User = this.sequelize.define('User' + config.rand(), { name: DataTypes.STRING });
          this.Task = this.sequelize.define('Task' + config.rand(), { name: DataTypes.STRING });
          this.users = null;
          this.tasks = null;

          this.User.belongsToMany(this.Task, {as: 'Tasks', through: 'usertasks'});
          this.Task.belongsToMany(this.User, {as: 'Users', through: 'usertasks'});

          for (var i = 0; i < 5; ++i) {
            users[users.length] = {id: i + 1, name: 'User' + Math.random()};
          }

          for (var x = 0; x < 5; ++x) {
            tasks[tasks.length] = {id: x + 1, name: 'Task' + Math.random()};
          }

          return this.sequelize.sync({ force: true }).then(function() {
            return self.User.bulkCreate(users).then(function() {
              return self.Task.bulkCreate(tasks).then(function() {
                return self.User.findAll().then(function(_users) {
                  return self.Task.findAll().then(function(_tasks) {
                    self.user = _users[0];
                    self.task = _tasks[0];
                    self.users = _users;
                    self.tasks = _tasks;

                    return self.user.getTasks().then(function(__tasks) {
                      expect(__tasks).to.have.length(0);
                      return self.user.setTasks(self.tasks).then(function() {
                        return self.user.getTasks().then(function(_tasks) {
                          expect(_tasks).to.have.length(self.tasks.length);
                          return self.user.removeTask(self.tasks[0]).then(function() {
                            return self.user.getTasks().then(function(_tasks) {
                              expect(_tasks).to.have.length(self.tasks.length - 1);
                              return self.user.removeTasks([self.tasks[1], self.tasks[2]]).then(function() {
                                return self.user.getTasks().then(function(_tasks) {
                                  expect(_tasks).to.have.length(self.tasks.length - 3);
                                });
                              });
                            });
                          });
                        });
                      });
                    });
                  });
                });
              });
            });
          });
        });
      });
    });
  });
}