'use strict';

var Utils = require('./utils')
  , Mixin = require('./associations/mixin')
  , InstanceValidator = require('./instance-validator')
  , DataTypes = require('./data-types')
  , Promise = require("./promise")
  , _ = require('lodash')
  , defaultsOptions = { raw: true };

module.exports = (function() {
  /**
   * This class represents an single instance, a database row. You might see it referred to as both Instance and instance. You should not
   * instantiate the Instance class directly, instead you access it using the finder and creation methods on the model.
   *
   * Instance instances operate with the concept of a `dataValues` property, which stores the actual values represented by the instance.
   * By default, the values from dataValues can also be accessed directly from the Instance, that is:
   * ```js
   * instance.field
   * // is the same as
   * instance.get('field')
   * // is the same as
   * instance.getDataValue('field')
   * ```
   * However, if getters and/or setters are defined for `field` they will be invoked, instead of returning the value from `dataValues`.
   * Accessing properties directly or using `get` is preferred for regular use, `getDataValue` should only be used for custom getters.
   *
   * @see {Sequelize#define} for more information about getters and setters
   * @class Instance
   */
  var Instance = function(values, options) {
    this.dataValues = {};
    this._previousDataValues = {};
    this.__options = this.Model.options;
    this.options = options || {};
    this.hasPrimaryKeys = this.Model.options.hasPrimaryKeys;
    this.__eagerlyLoadedAssociations = [];
    /**
     * Returns true if this instance has not yet been persisted to the database
     * @property isNewRecord
     * @return {Boolean}
     */
    this.isNewRecord = options.isNewRecord;

    /**
     * Returns the Model the instance was created from.
     * @see {Model}
     * @property Model
     * @return Model
     */

    initValues.call(this, values, options);
  };

  /**
   * A reference to the sequelize instance
   * @see {Sequelize}
   * @property sequelize
   * @return {Sequelize}
   */
  Object.defineProperty(Instance.prototype, 'sequelize', {
    get: function() { return this.Model.modelManager.sequelize; }
  });

  Object.defineProperty(Instance.prototype, 'QueryInterface', {
    get: function() { return this.sequelize.getQueryInterface(); }
  });

  /**
   * If timestamps and paranoid are enabled, returns whether the deletedAt timestamp of this instance is set. Otherwise, always returns false.
   * @property isDeleted
   * @return {Boolean}
   */
  Object.defineProperty(Instance.prototype, 'isDeleted', {
    get: function() {
      return this.Model._timestampAttributes.deletedAt && this.dataValues[this.Model._timestampAttributes.deletedAt] !== null;
    }
  });

  /**
   * Get the values of this Instance. Proxies to `this.get`
   * @see {Instance#get}
   * @property values
   * @return {Object}
   */
  Object.defineProperty(Instance.prototype, 'values', {
    get: function() {
      return this.get();
    }
  });

  /**
   * A getter for `this.changed()`. Returns true if any keys have changed.
   *
   * @see {Instance#changed}
   * @property isDirty
   * @return {Boolean}
   */
  Object.defineProperty(Instance.prototype, 'isDirty', {
    get: function() {
      return !!this.changed();
    }
  });

  /**
   * Get the values of the primary keys of this instance.
   *
   * @property primaryKeyValues
   * @return {Object}
   */
  Object.defineProperty(Instance.prototype, 'primaryKeyValues', {
    get: function() {
      var result = {}
        , self = this;

      Utils._.each(this.Model.primaryKeys, function(_, attr) {
        result[attr] = self.dataValues[attr];
      });

      return result;
    }
  });

  Object.defineProperty(Instance.prototype, 'identifiers', {
    get: function() {
      var primaryKeys = Object.keys(this.Model.primaryKeys)
        , result = {}
        , self = this;
      primaryKeys.forEach(function(identifier) {
        result[identifier] = self.dataValues[identifier];
      });

      return result;
    }
  });

  Instance.prototype.toString = function () {
    return '[object SequelizeInstance]';
  };

  /**
   * Get the value of the underlying data value
   *
   * @param {String} key
   * @return {any}
   */
  Instance.prototype.getDataValue = function(key) {
    return this.dataValues[key];
  };

  /**
   * Update the underlying data value
   *
   * @param {String} key
   * @param {any} value
   */
  Instance.prototype.setDataValue = function(key, value) {
    this.dataValues[key] = value;
  };

  /**
   * If no key is given, returns all values of the instance, also invoking virtual getters.
   *
   * If key is given and a field or virtual getter is present for the key it will call that getter - else it will return the value for key.
   *
   * @param {String} [key]
   * @param {Object} [options]
   * @param {Boolean} [options.plain=false] If set to true, included instances will be returned as plain objects
   * @return {Object|any}
   */
  Instance.prototype.get = function(key, options) {
    if (options === undefined && typeof key === "object") {
      options = key;
      key = undefined;
    }

    if (key) {
      if (this._customGetters[key]) {
        return this._customGetters[key].call(this, key);
      }
      if (options && options.plain && this.options.include && this.options.includeNames.indexOf(key) !== -1) {
        if (Array.isArray(this.dataValues[key])) {
          return this.dataValues[key].map(function (instance) {
            return instance.get({plain: options.plain});
          });
        } else if (this.dataValues[key] instanceof Instance) {
          return this.dataValues[key].get({plain: options.plain});
        } else {
          return this.dataValues[key];
        }
      }
      return this.dataValues[key];
    }

    if (this._hasCustomGetters || (options && options.plain && this.options.include) || (options && options.clone)) {
      var values = {}
        , _key;

      if (this._hasCustomGetters) {
        for (_key in this._customGetters) {
          if (this._customGetters.hasOwnProperty(_key)) {
            values[_key] = this.get(_key);
          }
        }
      }

      for (_key in this.dataValues) {
        if (!values.hasOwnProperty(_key) && this.dataValues.hasOwnProperty(_key)) {
          if (options && options.plain && this.options.include && this.options.includeNames.indexOf(_key) !== -1) {
            if (Array.isArray(this.dataValues[_key])) {
              values[_key] = this.dataValues[_key].map(function (instance) {
                return instance.get({plain: options.plain});
              });
            } else if (this.dataValues[_key] instanceof Instance) {
              values[_key] = this.dataValues[_key].get({plain: options.plain});
            } else {
              values[_key] = this.dataValues[_key];
            }
          } else {
            values[_key] = this.dataValues[_key];
          }
        }
      }
      return values;
    }
    return this.dataValues;
  };

  /**
   * Set is used to update values on the instance (the sequelize representation of the instance that is, remember that nothing will be persisted before you actually call `save`).
   * In its most basic form `set` will update a value stored in the underlying `dataValues` object. However, if a custom setter function is defined for the key, that function
   * will be called instead. To bypass the setter, you can pass `raw: true` in the options object.
   *
   * If set is called with an object, it will loop over the object, and call set recursively for each key, value pair. If you set raw to true, the underlying dataValues will either be
   * set directly to the object passed, or used to extend dataValues, if dataValues already contain values.
   *
   * When set is called, the previous value of the field is stored, so that you can later see which fields changed (see `changed`).
   *
   * Set can also be used to build instances for associations, if you have values for those. TODO - mick should probably write something here about how includes in set works - perhaps also even some tests?
   *
   * @see {Model#find} for more information about includes
   * @param {String|Object} key
   * @param {any} value
   * @param {Object} [options]
   * @param {Boolean} [options.raw=false] If set to true, field and virtual setters will be ignored
   * @param {Boolean} [options.reset=false] Clear all previously set data values
   * @param {Object}  [options.include]
   * @alias setAttributes
   */
  Instance.prototype.set = function(key, value, options) {
    var values
      , originalValue
      , keys
      , i
      , length;

    if (typeof key === 'object') {
      values = key;
      options = value;
      options || (options = {});

      if (options.reset) {
        this.dataValues = {};
      }

      // If raw, and we're not dealing with includes or special attributes, just set it straight on the dataValues object
      if (options.raw && !(this.options && this.options.include) && !(options && options.attributes) && !this.Model._hasBooleanAttributes && !this.Model._hasDateAttributes) {
        if (Object.keys(this.dataValues).length) {
          this.dataValues = _.extend(this.dataValues, values);
        } else {
          this.dataValues = values;
        }
        // If raw, .changed() shouldn't be true
        this._previousDataValues = _.clone(this.dataValues);
      } else {
        // Loop and call set

        if (options.attributes) {
          keys = options.attributes;
          if (this.Model._hasVirtualAttributes) {
            keys = keys.concat(this.Model._virtualAttributes);
          }

          if (this.options.includeNames) {
            keys = keys.concat(this.options.includeNames);
          }

          for (i = 0, length = keys.length; i < length; i++) {
            if (values[keys[i]] !== undefined) {
              this.set(keys[i], values[keys[i]], options);
            }
          }
        } else {
          for (key in values) {
            this.set(key, values[key], options);
          }
        }

        if (options.raw) {
          // If raw, .changed() shouldn't be true
          this._previousDataValues = _.clone(this.dataValues);
        }
      }
    } else {
      options || (options = {});
      if (!options.raw) {
        originalValue = this.dataValues[key];
      }

      // If not raw, and there's a customer setter
      if (!options.raw && this._customSetters[key]) {
        this._customSetters[key].call(this, value, key);
      } else {
        // Check if we have included models, and if this key matches the include model names/aliases

        if (this.options && this.options.include && this.options.includeNames.indexOf(key) !== -1 && value) {
          // Pass it on to the include handler
          this._setInclude(key, value, options);
          return;
        } else {
          // Bunch of stuff we won't do when its raw
          if (!options.raw) {
            // If attribute is not in model definition, return
            if (!this._isAttribute(key)) {
              return;
            }

            // If attempting to set primary key and primary key is already defined, return
            if (this.Model._hasPrimaryKeys && originalValue && this.Model._isPrimaryKey(key)) {
              return;
            }

            // If attempting to set read only attributes, return
            if (!this.isNewRecord && this.Model._hasReadOnlyAttributes && this.Model._isReadOnlyAttribute(key)) {
              return;
            }

            // Convert date fields to real date objects
            if (this.Model._hasDateAttributes && this.Model._isDateAttribute(key) && value !== null && value !== undefined && !(value instanceof Date) && !value._isSequelizeMethod) {
              value = new Date(value);
            }
          }

          // Convert boolean-ish values to booleans
          if (this.Model._hasBooleanAttributes && this.Model._isBooleanAttribute(key) && value !== null && value !== undefined && !value._isSequelizeMethod) {
            if (Buffer.isBuffer(value) && value.length === 1) {
              // Bit fields are returned as buffers
              value = value[0];
            }

            if (_.isString(value)) {
              // Only take action on valid boolean strings.
              value = (value === "true") ? true : (value === "false") ? false : value;

            } else if (_.isNumber(value)) {
              // Only take action on valid boolean integers.
              value = (value === 1) ? true : (value === 0) ? false : value;
            }
          }

          if (!options.raw && originalValue !== value) {
            this._previousDataValues[key] = originalValue;
          }
          this.dataValues[key] = value;
        }
      }
    }

    return this;
  };

  Instance.prototype.setAttributes = function(updates) {
    return this.set(updates);
  };

  /**
   * If changed is called with a string it will return a boolean indicating whether the value of that key in `dataValues` is different from the value in `_previousDataValues`.
   *
   * If changed is called without an argument, it will return an array of keys that have changed.
   *
   * @param {String} [key]
   * @return {Boolean|Array}
   */
  Instance.prototype.changed = function(key) {
    if (key) {
      if (this.Model._isDateAttribute(key) && this._previousDataValues[key] && this.dataValues[key]) {
        return this._previousDataValues[key].valueOf() !== this.dataValues[key].valueOf();
      }
      return this._previousDataValues[key] !== this.dataValues[key];
    }
    var changed = Object.keys(this.dataValues).filter(function(key) {
      return this.changed(key);
    }.bind(this));

    return changed.length ? changed : false;
  };

  /**
   * Returns the previous value for key from `_previousDataValues`.
   * @param {String} key
   * @return {any}
   */
  Instance.prototype.previous = function(key) {
    return this._previousDataValues[key];
  };

  Instance.prototype._setInclude = function(key, value, options) {
    if (!Array.isArray(value)) value = [value];
    if (value[0] instanceof Instance) {
      value = value.map(function(instance) {
        return instance.dataValues;
      });
    }

    var include = this.options.includeMap[key]
      , association = include.association
      , self = this
      , accessor = key
      , childOptions
      , primaryKeyAttribute  = include.model.primaryKeyAttribute
      , isEmpty;

    if (!isEmpty) {
      childOptions = {
        isNewRecord: false,
        isDirty: false,
        include: include.include,
        includeNames: include.includeNames,
        includeMap: include.includeMap,
        includeValidated: true,
        raw: options.raw,
        attributes: include.originalAttributes
      };
    }
    if (include.originalAttributes === undefined || include.originalAttributes.length) {
      if (association.isSingleAssociation) {
        if (Array.isArray(value)) {
          value = value[0];
        }

        isEmpty = value && value[primaryKeyAttribute] === null;
        self[accessor] = self.dataValues[accessor] = isEmpty ? null : include.model.build(value, childOptions);
      } else {
        isEmpty = value[0] && value[0][primaryKeyAttribute] === null;
        self[accessor] = self.dataValues[accessor] = isEmpty ? [] : include.model.bulkBuild(value, childOptions);
      }
    }
  };

  /**
   * Validate this instance, and if the validation passes, persist it to the database.
   *
   * On success, the callback will be called with this instance. On validation error, the callback will be called with an instance of `Sequelize.ValidationError`.
   * This error will have a property for each of the fields for which validation failed, with the error message for that field.
   *
   * @param {Object} [options]
   * @param {Object} [options.fields] An optional array of strings, representing database columns. If fields is provided, only those columns will be validated and saved.
   * @param {Boolean} [options.silent=false] If true, the updatedAt timestamp will not be updated.
   * @param {Boolean} [options.validate=true] If false, validations won't be run.
   * @param {Transaction} [options.transaction]
   *
   * @return {Promise<this|Errors.ValidationError>}
   */
  Instance.prototype.save = function(options, deprecated) {
    if (options instanceof Array) {
      options = { fields: options };
    }

    options = Utils._.extend({
      hooks: true,
      validate: true
    }, options, deprecated);

    if (!options.fields) {
      if (this.isNewRecord) {
        options.fields = Object.keys(this.Model.attributes);
      } else {
        options.fields = _.intersection(this.changed(), Object.keys(this.Model.attributes));
      }

      options.defaultFields = options.fields;
    }

    if (options.returning === undefined) {
      if (options.association) {
        options.returning = false;
      } else if (this.isNewRecord) {
        options.returning = true;
      }
    }

    var self = this
      , updatedAtAttr = this.Model._timestampAttributes.updatedAt
      , createdAtAttr = this.Model._timestampAttributes.createdAt
      , hook = self.isNewRecord ? 'Create' : 'Update';

    if (updatedAtAttr && options.fields.indexOf(updatedAtAttr) === -1) {
      options.fields.push(updatedAtAttr);
    }

    if (options.silent === true && !(this.isNewRecord && this.get(updatedAtAttr, {raw: true}))) {
      // UpdateAtAttr might have been added as a result of Object.keys(Model.attributes). In that case we have to remove it again
      Utils._.remove(options.fields, function(val) {
        return val === updatedAtAttr;
      });
      updatedAtAttr = false;
    }

    if (this.isNewRecord === true && createdAtAttr && options.fields.indexOf(createdAtAttr) === -1) {
      options.fields.push(createdAtAttr);
    }

    return Promise.bind(this).then(function() {
      // Validate
      if (options.validate) {
        return Promise.bind(this).then(function () {
          // hookValidate rejects with errors, validate returns with errors
          if (options.hooks) return this.hookValidate(options);

          return this.validate(options).then(function (err) {
            if (err) throw err;
          });
        });
      }
    }).then(function() {
      return Promise.bind(this).then(function() {
        // Run before hook
        if (options.hooks) {
          var beforeHookValues = _.pick(this.dataValues, options.fields)
            , afterHookValues
            , hookChanged
            , ignoreChanged = _.difference(this.changed(), options.fields); // In case of update where it's only supposed to update the passed values and the hook values

          if (updatedAtAttr && options.fields.indexOf(updatedAtAttr) !== -1) {
            ignoreChanged = _.without(ignoreChanged, updatedAtAttr);
          }

          return this.Model.runHooks('before' + hook, this, options).bind(this).then(function() {
            if (options.defaultFields && !this.isNewRecord) {
              afterHookValues = _.pick(this.dataValues, _.difference(this.changed(), ignoreChanged));

              hookChanged = [];
              Object.keys(afterHookValues).forEach(function (key) {
                if (afterHookValues[key] !== beforeHookValues[key]) {
                  hookChanged.push(key);
                }
              });

              options.fields = _.unique(options.fields.concat(hookChanged));
            }

            if (hookChanged) {
              if (options.validate) {
                // Validate again

                options.skip = _.difference(Object.keys(this.Model.rawAttributes), hookChanged);
                return Promise.bind(this).then(function () {
                  // hookValidate rejects with errors, validate returns with errors
                  if (options.hooks) return this.hookValidate(options);

                  return this.validate(options).then(function (err) {
                    if (err) throw err;
                  });
                }).then(function() {
                  delete options.skip;
                });
              }
            }
          });
        }
      }).then(function() {
        if (!options.fields.length) return this;

        var values = Utils.mapValueFieldNames(this.dataValues, options.fields, this.Model)
          , query = null
          , args = [];

        if (updatedAtAttr && !options.silent) {
          self.dataValues[updatedAtAttr] = values[self.Model.rawAttributes[updatedAtAttr].field || updatedAtAttr] = self.Model.__getTimestamp(updatedAtAttr);
        }

        if (self.isNewRecord && createdAtAttr && !values[createdAtAttr]) {
          self.dataValues[createdAtAttr] = values[self.Model.rawAttributes[createdAtAttr].field || createdAtAttr] = self.Model.__getTimestamp(createdAtAttr);
        }

        if (self.isNewRecord) {
          query = 'insert';
          args = [self, self.Model.getTableName(options), values, options];
        } else {
          var identifier = self.primaryKeyValues;

          if (identifier) {
            identifier = Utils.mapValueFieldNames(identifier, Object.keys(identifier), this.Model);

          } else if (identifier === null && self.__options.whereCollection !== null) {
            identifier = self.__options.whereCollection;
          }

          query = 'update';
          args = [self, self.Model.getTableName(options), values, identifier, options];
        }

        return self.QueryInterface[query].apply(self.QueryInterface, args)
          .then(function(result) {
            // Transfer database generated values (defaults, autoincrement, etc)
            Object.keys(self.Model.rawAttributes).forEach(function (attr) {
              if (self.Model.rawAttributes[attr].field &&
                  values[self.Model.rawAttributes[attr].field] !== undefined &&
                  self.Model.rawAttributes[attr].field !== attr
              ) {
                values[attr] = values[self.Model.rawAttributes[attr].field];
                delete values[self.Model.rawAttributes[attr].field];
              }
            });
            values = _.extend(values, result.dataValues);

            result.dataValues = _.extend(result.dataValues, values);
            return result;
          })
          .tap(function(result) {
            // Run after hook
            if (options.hooks) {
              return self.Model.runHooks('after' + hook, result, options);
            }
          })
          .then(function(result) {
            options.fields.forEach(function (field) {
              result._previousDataValues[field] = result.dataValues[field];
            });
            self.isNewRecord = false;
            return result;
          });
      });
    });
  };

 /*
  * Refresh the current instance in-place, i.e. update the object with current data from the DB and return the same object.
  * This is different from doing a `find(Instance.id)`, because that would create and return a new instance. With this method,
  * all references to the Instance are updated with the new data and no new objects are created.
  *
  * @see {Model#find}
  * @param {Object} [options] Options that are passed on to `Model.find`
  * @return {Promise<this>}
  */
  Instance.prototype.reload = function(options) {
    var self = this
      , where = [
        this.QueryInterface.quoteTable(this.Model.name) + '.' + this.QueryInterface.quoteIdentifier(this.Model.primaryKeyField) + '=?',
        this.get(this.Model.primaryKeyAttribute, {raw: true})
      ];

    return this.Model.find({
      where: where,
      limit: 1,
      include: this.options.include || null
    }, options).then(function(reload) {
      self.set(reload.dataValues, {raw: true, reset: true});
    }).return(self);
  };

  /*
   * Validate the attribute of this instance according to validation rules set in the model definition.
   *
   * Emits null if and only if validation successful; otherwise an Error instance containing { field name : [error msgs] } entries.
   *
   * @param {Object} [options] Options that are passed to the validator
   * @param {Array} [options.skip] An array of strings. All properties that are in this array will not be validated
   * @see {InstanceValidator}
   *
   * @return {Promise<undefined|Errors.ValidationError}
   */
  Instance.prototype.validate = function(options) {
    return new InstanceValidator(this, options).validate();
  };

  Instance.prototype.hookValidate = function(options) {
    return new InstanceValidator(this, options).hookValidate();
  };

  /**
   * This is the same as calling `set` and then calling `save`.
   *
   * @see {Instance#set}
   * @see {Instance#save}
   * @param {Object} updates See `set`
   * @param {Object} options See `save`
   *
   * @return {Promise<this>}
   * @alias updateAttributes
   */
  Instance.prototype.update = function(values, options) {
    options = options || {};
    if (Array.isArray(options)) options = {fields: options};

    this.set(values, {attributes: options.fields});

    if (!options.fields) {
      options.fields = _.intersection(Object.keys(values), this.changed());
      options.defaultFields = options.fields;
    }

    return this.save(options);
  };
  Instance.prototype.updateAttributes = Instance.prototype.update;

  /**
   * Destroy the row corresponding to this instance. Depending on your setting for paranoid, the row will either be completely deleted, or have its deletedAt timestamp set to the current time.
   *
   * @param {Object}      [options={}]
   * @param {Boolean}     [options.force=false] If set to true, paranoid models will actually be deleted
   * @param {Transaction} [options.transaction]
   *
   * @return {Promise<undefined>}
   */
  Instance.prototype.destroy = function(options) {
    options = Utils._.extend({
      hooks: true,
      force: false
    }, options || {});

    var self = this;

    // This semi awkward syntax where we can't return the chain directly but have to return the last .then() call is to allow sql proxying
    return Promise.try(function() {
      // Run before hook
      if (options.hooks) {
        return self.Model.runHooks('beforeDestroy', self, options);
      }
    }).then(function() {
      var where;

      if (self.Model._timestampAttributes.deletedAt && options.force === false) {
        self.dataValues[self.Model._timestampAttributes.deletedAt] = new Date();
        return self.save(_.extend(_.clone(options), {hooks : false}));
      } else {
        where = {};
        where[self.Model.rawAttributes[self.Model.primaryKeyAttribute].field] = self.get(self.Model.primaryKeyAttribute, {raw: true});
        return self.QueryInterface.delete(self, self.Model.getTableName(options), where, _.defaults(options, {limit: null}));
      }
    }).tap(function(result) {
      // Run after hook
      if (options.hooks) {
        return self.Model.runHooks('afterDestroy', self, options);
      }
    }).then(function(result) {
      return result;
    });
  };

  /**
   * Restore the row corresponding to this instance. Only available for paranoid models.
   *
   * @param {Object}      [options={}]
   * @param {Transaction} [options.transaction]
   *
   * @return {Promise<undefined>}
   */
  Instance.prototype.restore = function(options) {
    if (!this.Model._timestampAttributes.deletedAt) throw new Error("Model is not paranoid");

    options = Utils._.extend({
      hooks: true,
      force: false
    }, options || {});

    var self = this;

    return Promise.try(function() {
      // Run before hook
      if (options.hooks) {
        return self.Model.runHooks('beforeRestore', self, options);
      }
    }).then(function() {
      self.dataValues[self.Model._timestampAttributes.deletedAt] = null;
      return self.save(_.extend(_.clone(options), {hooks : false, omitNull : false}));
    }).tap(function(result) {
      // Run after hook
      if (options.hooks) {
        return self.Model.runHooks('afterRestore', self, options);
      }
    });
  };

  /**
   * Increment the value of one or more columns. This is done in the database, which means it does not use the values currently stored on the Instance. The increment is done using a
   * ```sql
   * SET column = column + X
   * ```
   * query. To get the correct value after an increment into the Instance you should do a reload.
   *
   *```js
   * instance.increment('number') // increment number by 1
   * instance.increment(['number', 'count'], { by: 2 }) // increment number and count by 2
   * instance.increment({ answer: 42, tries: 1}, { by: 2 }) // increment answer by 42, and tries by 1.
   *                                                        // `by` is ignored, since each column has its own value
   * ```
   *
   * @see {Instance#reload}
   * @param {String|Array|Object} fields If a string is provided, that column is incremented by the value of `by` given in options. If an array is provided, the same is true for each column. If and object is provided, each column is incremented by the value given
   * @param {Object} [options]
   * @param {Integer} [options.by=1] The number to increment by
   * @param {Transaction} [options.transaction]
   *
   * @return {Promise<this>}
   */
  Instance.prototype.increment = function(fields, countOrOptions) {
    Utils.validateParameter(countOrOptions, Object, {
      optional: true,
      deprecated: Number,
      deprecationWarning: 'Increment expects an object as second parameter. Please pass the incrementor as option! ~> instance.increment(' + JSON.stringify(fields) + ', { by: ' + countOrOptions + ' })'
    });

    var identifier = this.primaryKeyValues
      , updatedAtAttr = this.Model._timestampAttributes.updatedAt
      , values = {}
      , where;

    if (identifier) {
      for (var attrName in identifier) {
        // Field name mapping
        var rawAttribute = this.Model.rawAttributes[attrName];
        if (rawAttribute.field && rawAttribute.field !== rawAttribute.fieldName) {
          identifier[this.Model.rawAttributes[attrName].field] = identifier[attrName];
          delete identifier[attrName];
        }
      }
    }
    if (identifier === null && this.__options.whereCollection !== null) {
      identifier = this.__options.whereCollection;
    }

    if (countOrOptions === undefined) {
      countOrOptions = { by: 1 };
    } else if (typeof countOrOptions === 'number') {
      countOrOptions = { by: countOrOptions };
    }

    countOrOptions = Utils._.extend({
      by: 1,
      attributes: {},
      where: {}
    }, countOrOptions);

    where = _.extend(countOrOptions.where, identifier);

    if (Utils._.isString(fields)) {
      values[fields] = countOrOptions.by;
    } else if (Utils._.isArray(fields)) {
      Utils._.each(fields, function(field) {
        values[field] = countOrOptions.by;
      });
    } else { // Assume fields is key-value pairs
      values = fields;
    }

    if (updatedAtAttr && !values[updatedAtAttr]) {
      countOrOptions.attributes[updatedAtAttr] = this.Model.__getTimestamp(updatedAtAttr);
    }

    Object.keys(values).forEach(function(attr) {
      // Field name mapping
      if (this.Model.rawAttributes[attr] && this.Model.rawAttributes[attr].field && this.Model.rawAttributes[attr].field !== attr) {
        values[this.Model.rawAttributes[attr].field] = values[attr];
        delete values[attr];
      }
    }, this);

    return this.QueryInterface.increment(this, this.Model.getTableName(countOrOptions), values, where, countOrOptions).return(this);
  };

  /**
   * Decrement the value of one or more columns. This is done in the database, which means it does not use the values currently stored on the Instance. The decrement is done using a
   * ```sql
   * SET column = column - X
   * ```
   * query. To get the correct value after an decrement into the Instance you should do a reload.
   *
   * ```js
   * instance.decrement('number') // decrement number by 1
   * instance.decrement(['number', 'count'], { by: 2 }) // decrement number and count by 2
   * instance.decrement({ answer: 42, tries: 1}, { by: 2 }) // decrement answer by 42, and tries by 1.
   *                                                        // `by` is ignored, since each column has its own value
   * ```
   *
   * @see {Instance#reload}
   * @param {String|Array|Object} fields If a string is provided, that column is decremented by the value of `by` given in options. If an array is provided, the same is true for each column. If and object is provided, each column is decremented by the value given
   * @param {Object} [options]
   * @param {Integer} [options.by=1] The number to decrement by
   * @param {Transaction} [options.transaction]
   *
   * @return {Promise}
   */
  Instance.prototype.decrement = function(fields, countOrOptions) {
    Utils.validateParameter(countOrOptions, Object, {
      optional: true,
      deprecated: Number,
      deprecationWarning: 'Decrement expects an object as second parameter. Please pass the decrementor as option! ~> instance.decrement(' + JSON.stringify(fields) + ', { by: ' + countOrOptions + ' })'
    });

    if (countOrOptions === undefined) {
      countOrOptions = { by: 1 };
    } else if (typeof countOrOptions === 'number') {
      countOrOptions = { by: countOrOptions };
    }

    if (countOrOptions.by === undefined) {
      countOrOptions.by = 1;
    }

    if (!Utils._.isString(fields) && !Utils._.isArray(fields)) { // Assume fields is key-value pairs
      Utils._.each(fields, function(value, field) {
        fields[field] = -value;
      });
    }

    countOrOptions.by = 0 - countOrOptions.by;

    return this.increment(fields, countOrOptions);
  };

  /**
   * Check whether all values of this and `other` Instance are the same
   *
   * @param {Instance} other
   * @return {Boolean}
   */
  Instance.prototype.equals = function(other) {
    var result = true;

    Utils._.each(this.dataValues, function(value, key) {
      if (Utils._.isDate(value) && Utils._.isDate(other[key])) {
        result = result && (value.getTime() === other[key].getTime());
      } else {
        result = result && (value === other[key]);
      }
    });

    return result;
  };

  /**
   * Check if this is eqaul to one of `others` by calling equals
   *
   * @param {Array} others
   * @return {Boolean}
   */
  Instance.prototype.equalsOneOf = function(others) {
    var self = this;

    return _.any(others, function(other) {
      return self.equals(other);
    });
  };

  Instance.prototype.setValidators = function(attribute, validators) {
    this.validators[attribute] = validators;
  };

  /**
   * Convert the instance to a JSON representation. Proxies to calling `get` with no keys. This means get all values gotten from the DB, and apply all custom getters.
   *
   * @see {Instance#get}
   * @return {object}
   */
  Instance.prototype.toJSON = function() {
    return this.get({
      plain: true
    });
  };

  // private
  var initValues = function(values, options) {
    var defaults
      , key;

    values = values && _.clone(values) || {};

    if (options.isNewRecord) {
      defaults = {};

      if (this.Model._hasDefaultValues) {
        Utils._.each(this.Model._defaultValues, function(valueFn, key) {
          if (!defaults.hasOwnProperty(key)) {
            defaults[key] = valueFn();
          }
        });
      }

      // set id to null if not passed as value, a newly created dao has no id
      // removing this breaks bulkCreate
      // do after default values since it might have UUID as a default value
      if (!defaults.hasOwnProperty(this.Model.primaryKeyAttribute)) {
        defaults[this.Model.primaryKeyAttribute] = null;
      }

      if (this.Model._timestampAttributes.createdAt && defaults[this.Model._timestampAttributes.createdAt]) {
        this.dataValues[this.Model._timestampAttributes.createdAt] = Utils.toDefaultValue(defaults[this.Model._timestampAttributes.createdAt]);
        delete defaults[this.Model._timestampAttributes.createdAt];
      }

      if (this.Model._timestampAttributes.updatedAt && defaults[this.Model._timestampAttributes.updatedAt]) {
        this.dataValues[this.Model._timestampAttributes.updatedAt] = Utils.toDefaultValue(defaults[this.Model._timestampAttributes.updatedAt]);
        delete defaults[this.Model._timestampAttributes.updatedAt];
      }

      if (this.Model._timestampAttributes.deletedAt && defaults[this.Model._timestampAttributes.deletedAt]) {
        this.dataValues[this.Model._timestampAttributes.deletedAt] = Utils.toDefaultValue(defaults[this.Model._timestampAttributes.deletedAt]);
        delete defaults[this.Model._timestampAttributes.deletedAt];
      }

      if (Object.keys(defaults).length) {
        for (key in defaults) {
          if (!values.hasOwnProperty(key)) {
            this.set(key, Utils.toDefaultValue(defaults[key]), defaultsOptions);
          }
        }
      }
    }

    this.set(values, options);
  };

  return Instance;
})();


if (this.otherKeyDefault) this.otherKey = this.paired.foreignKey;

if (this.paired.otherKeyDefault) this.paired.otherKey = this.foreignKey;

if (typeof this.through.model === 'string') {

this.through.model = this.sequelize.define(this.through.model, {}, _.extend(this.options, {

keep keep keep replace replace keep keep keep keep keep

// If paired otherKey was inferred we should make sure to clean it up before adding a new one that matches the foreignKey

'use strict';

var chai = require('chai')
  , expect = chai.expect
  , Support = require(__dirname + '/../../support')
  , DataTypes = require(__dirname + '/../../../../lib/data-types');

chai.config.includeStack = true;

if (Support.dialectIsMySQL()) {
  describe('[MYSQL 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(done) {
          var Table2 = this.sequelize.define('wp_table2', {foo: DataTypes.STRING})
            , Table1 = this.sequelize.define('wp_table1', {foo: DataTypes.STRING})
            , self = this;

          Table1.hasMany(Table2);
          Table2.hasMany(Table1);
          Table1.sync({ force: true }).success(function() {
            Table2.sync({ force: true }).success(function() {
              expect(self.sequelize.daoFactoryManager.getDAO('wp_table1swp_table2s')).to.exist;
              done();
            });
          });
        });
      });

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

          Table1.hasMany(Table2, {joinTableName: 'table1_to_table2'});
          Table2.hasMany(Table1, {joinTableName: 'table1_to_table2'});
          Table1.sync({ force: true }).success(function() {
            Table2.sync({ force: true }).success(function() {
              done();
            });
          });
        });

        it('should not use only a specified name', function() {
          expect(this.sequelize.daoFactoryManager.getDAO('ms_table1sms_table2s')).not.to.exist;
          expect(this.sequelize.daoFactoryManager.getDAO('table1_to_table2')).to.exist;
        });
      });
    });

    describe('HasMany', function() {
      beforeEach(function(done) {
        //prevent periods from occurring in the table name since they are used to delimit (table.column)
        this.User = this.sequelize.define('User' + Math.ceil(Math.random() * 10000000), { name: DataTypes.STRING });
        this.Task = this.sequelize.define('Task' + Math.ceil(Math.random() * 10000000), { name: DataTypes.STRING });
        this.users = null;
        this.tasks = null;

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

        var self = this
          , 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()};
        }

        this.sequelize.sync({ force: true }).success(function() {
          self.User.bulkCreate(users).success(function() {
            self.Task.bulkCreate(tasks).success(function() {
              done();
            });
          });
        });
      });

      describe('addDAO / getDAO', function() {
        beforeEach(function(done) {
          var self = this;

          self.user = null;
          self.task = null;

          self.User.all().success(function(_users) {
            self.Task.all().success(function(_tasks) {
              self.user = _users[0];
              self.task = _tasks[0];
              done();
            });
          });
        });

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

          self.user.getTasks().on('success', function(_tasks) {
            expect(_tasks.length).to.equal(0);
            self.user.addTask(self.task).on('success', function() {
              self.user.getTasks().on('success', function(_tasks) {
                expect(_tasks.length).to.equal(1);
                done();
              });
            });
          });
        });
      });

      describe('removeDAO', function() {
        beforeEach(function(done) {
          var self = this;

          self.user = null;
          self.tasks = null;

          self.User.all().success(function(_users) {
            self.Task.all().success(function(_tasks) {
              self.user = _users[0];
              self.tasks = _tasks;
              done();
            });
          });
        });

        it('should correctly remove associated objects', function(done) {
          var self = this;

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


'use strict';

var chai = require('chai')
  , sinon = require('sinon')
  , Sequelize = require('../../../index')
  , Promise = Sequelize.Promise
  , expect = chai.expect
  , Support = require(__dirname + '/../support')
  , DataTypes = require(__dirname + '/../../../lib/data-types')
  , dialect = Support.getTestDialect()
  , datetime = require('chai-datetime')
  , _ = require('lodash')
  , assert = require('assert')
  , current = Support.sequelize;

chai.use(datetime);
chai.config.includeStack = true;

describe(Support.getTestDialectTeaser('Model'), function() {
  beforeEach(function() {
    return Support.prepareTransactionTest(this.sequelize).bind(this).then(function(sequelize) {
      this.sequelize = sequelize;

      this.User = this.sequelize.define('User', {
        username: DataTypes.STRING,
        secretValue: DataTypes.STRING,
        data: DataTypes.STRING,
        intVal: DataTypes.INTEGER,
        theDate: DataTypes.DATE,
        aBool: DataTypes.BOOLEAN,
        uniqueName: { type: DataTypes.STRING, unique: true }
      });
      this.Account = this.sequelize.define('Account', {
        accountName: DataTypes.STRING
      });
      this.Student = this.sequelize.define('Student', {
          no: {type: DataTypes.INTEGER, primaryKey: true},
          name: {type: DataTypes.STRING, allowNull: false}
      });

      return this.sequelize.sync({ force: true });
    });
  });

  describe('findOrCreate', function() {
    if (current.dialect.supports.transactions) {
      it('supports transactions', function(done) {
        var self = this;
        this.sequelize.transaction().then(function(t) {
          self.User.findOrCreate({ where: { username: 'Username' }, defaults: { data: 'some data' }}, { transaction: t }).then(function() {
            self.User.count().success(function(count) {
              expect(count).to.equal(0);
              t.commit().success(function() {
                self.User.count().success(function(count) {
                  expect(count).to.equal(1);
                  done();
                });
              });
            });
          });
        });
      });

      it('supports more than one models per transaction', function(done) {
        var self = this;
        this.sequelize.transaction().then(function(t) {
          self.User.findOrCreate({ where: { username: 'Username'}, defaults: { data: 'some data' }}, { transaction: t }).then(function() {
            self.Account.findOrCreate({ where: { accountName: 'accountName'}}, { transaction: t}).then(function() {
              t.commit().success(function() {
                done();
              });
            });
          });
        });
      });
    }

    it('should error correctly when defaults contain a unique key', function () {
      var User = this.sequelize.define('user', {
        objectId: {
          type: DataTypes.STRING,
          unique: true
        },
        username: {
          type: DataTypes.STRING,
          unique: true
        }
      });

      return User.sync({force: true}).then(function () {
        return User.create({
          username: 'gottlieb'
        });
      }).then(function () {
        return User.findOrCreate({
          where: {
            objectId: 'asdasdasd'
          },
          defaults: {
            username: 'gottlieb'
          }
        }).then(function () {
          throw new Error('I should have been rejected');
        }, function (err) {
          expect(err instanceof Sequelize.UniqueConstraintError).to.be.ok;
          expect(err.fields).to.be.ok;
        });
      });
    });

    it('should support special characters in defaults', function () {
      var User = this.sequelize.define('user', {
        objectId: {
          type: DataTypes.INTEGER,
          unique: true
        },
        description: {
          type: DataTypes.TEXT
        }
      });

      return User.sync({force: true}).then(function () {
        return User.findOrCreate({
          where: {
            objectId: 1
          },
          defaults: {
            description: '$$ and !! and :: and ? and ^ and * and \''
          }
        });
      });
    });

    it('should support bools in defaults', function () {
      var User = this.sequelize.define('user', {
        objectId: {
          type: DataTypes.INTEGER,
          unique: true
        },
        bool: DataTypes.BOOLEAN
      });

      return User.sync({force: true}).then(function () {
        return User.findOrCreate({
          where: {
            objectId: 1
          },
          defaults: {
            bool: false
          }
        });
      });
    });

    it('returns instance if already existent. Single find field.', function(done) {
      var self = this,
        data = {
          username: 'Username'
        };

      this.User.create(data).success(function(user) {
        self.User.findOrCreate({ where: {
          username: user.username
        }}).spread(function(_user, created) {
          expect(_user.id).to.equal(user.id);
          expect(_user.username).to.equal('Username');
          expect(created).to.be.false;
          done();
        });
      });
    });

    it('Returns instance if already existent. Multiple find fields.', function(done) {
      var self = this,
        data = {
          username: 'Username',
          data: 'ThisIsData'
        };

      this.User.create(data).success(function(user) {
        self.User.findOrCreate({where: data}).done(function(err, _user, created) {
          expect(_user.id).to.equal(user.id);
          expect(_user.username).to.equal('Username');
          expect(_user.data).to.equal('ThisIsData');
          expect(created).to.be.false;
          done();
        });
      });
    });

    it('creates new instance with default value.', function(done) {
      var data = {
          username: 'Username'
        },
        default_values = {
          data: 'ThisIsData'
        };

      this.User.findOrCreate({ where: data, defaults: default_values}).success(function(user, created) {
        expect(user.username).to.equal('Username');
        expect(user.data).to.equal('ThisIsData');
        expect(created).to.be.true;
        done();
      });
    });

    it('supports .or() (only using default values)', function(done) {
      this.User.findOrCreate({
        where: Sequelize.or({username: 'Fooobzz'}, {secretValue: 'Yolo'}),
        defaults: {username: 'Fooobzz', secretValue: 'Yolo'}
      }).done(function(err, user, created) {
        expect(err).not.to.be.ok;
        expect(user.username).to.equal('Fooobzz');
        expect(user.secretValue).to.equal('Yolo');
        expect(created).to.be.true;

        done();
      });
    });

    if (current.dialect.supports.transactions) {
      it('should release transaction when meeting errors', function() {
          var self = this;

          var test = function(times) {
              if (times > 10) {
                  return true;
              }
              return self.Student.findOrCreate({
                where: {
                  no: 1
                }
              })
              .timeout(1000)
              .catch (Promise.TimeoutError, function(e) {
                  throw new Error(e);
              })
              .catch (Sequelize.ValidationError, function(err) {
                  return test(times + 1);
              });
          };

          return test(0);
      });
    }

    describe('several concurrent calls', function() {
      if (current.dialect.supports.transactions) {
        it('works with a transaction', function() {
          return this.sequelize.transaction().bind(this).then(function(transaction) {
            return Promise.join(
              this.User.findOrCreate({ where: { uniqueName: 'winner' }}, { transaction: transaction }),
              this.User.findOrCreate({ where: { uniqueName: 'winner' }}, { transaction: transaction }),
              function(first, second) {
                 var firstInstance = first[0]
                , firstCreated = first[1]
                , secondInstance = second[0]
                , secondCreated = second[1];

                // Depending on execution order and MAGIC either the first OR the second call should return true
                expect(firstCreated ? !secondCreated : secondCreated).to.be.ok; // XOR

                expect(firstInstance).to.be.ok;
                expect(secondInstance).to.be.ok;

                expect(firstInstance.id).to.equal(secondInstance.id);

                return transaction.commit();
              }
            );
          });
        });
      }

      (dialect !== 'sqlite' ? it : it.skip)('should error correctly when defaults contain a unique key without a transaction', function () {
        var User = this.sequelize.define('user', {
          objectId: {
            type: DataTypes.STRING,
            unique: true
          },
          username: {
            type: DataTypes.STRING,
            unique: true
          }
        });

        return User.sync({force: true}).then(function () {
          return User.create({
            username: 'gottlieb'
          });
        }).then(function () {
          return Promise.join(
            User.findOrCreate({
              where: {
                objectId: 'asdasdasd'
              },
              defaults: {
                username: 'gottlieb'
              }
            }).then(function () {
              throw new Error('I should have ben rejected');
            }, function (err) {
              expect(err instanceof Sequelize.UniqueConstraintError).to.be.ok;
              expect(err.fields).to.be.ok;
            }),
            User.findOrCreate({
              where: {
                objectId: 'asdasdasd'
              },
              defaults: {
                username: 'gottlieb'
              }
            }).then(function () {
              throw new Error('I should have ben rejected');
            }, function (err) {
              expect(err instanceof Sequelize.UniqueConstraintError).to.be.ok;
              expect(err.fields).to.be.ok;
            })
          );
        });
      });

      // Creating two concurrent transactions and selecting / inserting from the same table throws sqlite off
      (dialect !== 'sqlite' ? it : it.skip)('works without a transaction', function() {
        return Promise.join(
          this.User.findOrCreate({ where: { uniqueName: 'winner' }}),
          this.User.findOrCreate({ where: { uniqueName: 'winner' }}),
          function(first, second) {
            var firstInstance = first[0]
              , firstCreated = first[1]
              , secondInstance = second[0]
              , secondCreated = second[1];

            // Depending on execution order and MAGIC either the first OR the second call should return true
            expect(firstCreated ? !secondCreated : secondCreated).to.be.ok; // XOR

            expect(firstInstance).to.be.ok;
            expect(secondInstance).to.be.ok;

            expect(firstInstance.id).to.equal(secondInstance.id);
          }
        );
      });
    });
  });

  describe('create', function() {
    it('works with non-integer primary keys with a default value', function(done) {
      var User = this.sequelize.define('User', {
        'id': {
          primaryKey: true,
          type: DataTypes.UUID,
          defaultValue: DataTypes.UUIDV4
        },
        'email': {
          type: DataTypes.UUID,
          defaultValue: DataTypes.UUIDV4
        }
      });

      this.sequelize.sync({force: true}).done(function(err) {
        expect(err).not.to.be.ok;
        User.create({}).done(function(err, user) {
          expect(err).not.to.be.ok;
          expect(user).to.be.ok;
          expect(user.id).to.be.ok;
          done();
        });
      });
    });

    it('should return an error for a unique constraint error', function(done) {
      var User = this.sequelize.define('User', {
        'email': {
          type: DataTypes.STRING,
          unique: { name: 'email', msg: 'Email is already registered.' },
          validate: {
            notEmpty: true,
            isEmail: true
          }
        }
      });

      this.sequelize.sync({force: true}).done(function(err) {
        expect(err).not.to.be.ok;
        User.create({email: 'hello@sequelize.com'}).done(function(err) {
          expect(err).not.to.be.ok;
          User.create({email: 'hello@sequelize.com'}).then(function() {
            assert(false);
          }, function(err) {
            expect(err).to.be.ok;
            expect(err).to.be.an.instanceof(Error);
            done();
          });
        });
      });
    });

    it('works without any primary key', function() {
      var Log = this.sequelize.define('log', {
        level: DataTypes.STRING
      });

      Log.removeAttribute('id');

      return this.sequelize.sync({force: true}).then(function() {
        return Promise.join(
          Log.create({level: 'info'}),
          Log.bulkCreate([
            {level: 'error'},
            {level: 'debug'}
          ])
        );
      }).then(function() {
        return Log.findAll();
      }).then(function(logs) {
        logs.forEach(function(log) {
          expect(log.get('id')).not.to.be.ok;
        });
      });
    });

    it('should be able to set createdAt and updatedAt if using silent: true', function () {
      var User = this.sequelize.define('user', {
        name: DataTypes.STRING
      }, {
        timestamps: true
      });

      var createdAt = new Date(2012, 10, 10, 10, 10, 10);
      var updatedAt = new Date(2011, 11, 11, 11, 11, 11);

      return User.sync({force: true}).then(function () {
        return User.create({
          createdAt: createdAt,
          updatedAt: updatedAt
        }, {
          silent: true
        }).then(function (user) {
          expect(createdAt.getTime()).to.equal(user.get('createdAt').getTime());
          expect(updatedAt.getTime()).to.equal(user.get('updatedAt').getTime());

          return User.findOne({
            updatedAt: {
              ne: null
            }
          }).then(function (user) {
            expect(createdAt.getTime()).to.equal(user.get('createdAt').getTime());
            expect(updatedAt.getTime()).to.equal(user.get('updatedAt').getTime());
          });
        });
      });
    });

    if (current.dialect.supports.transactions) {
      it('supports transactions', function(done) {
        var self = this;
        this.sequelize.transaction().then(function(t) {
          self.User.create({ username: 'user' }, { transaction: t }).success(function() {
            self.User.count().success(function(count) {
              expect(count).to.equal(0);
              t.commit().success(function() {
                self.User.count().success(function(count) {
                  expect(count).to.equal(1);
                  done();
                });
              });
            });
          });
        });
      });
    }

    if (current.dialect.supports.returnValues) {
      describe('return values', function () {
        it('should make the autoincremented values available on the returned instances', function () {
          var User = this.sequelize.define('user', {});

          return User.sync({force: true}).then(function () {
            return User.create({}, {returning: true}).then(function (user) {
              expect(user.get('id')).to.be.ok;
              expect(user.get('id')).to.equal(1);
            });
          });
        });

        it('should make the autoincremented values available on the returned instances with custom fields', function () {
          var User = this.sequelize.define('user', {
            maId: {
              type: DataTypes.INTEGER,
              primaryKey: true,
              autoIncrement: true,
              field: 'yo_id'
            }
          });

          return User.sync({force: true}).then(function () {
            return User.create({}, {returning: true}).then(function (user) {
              expect(user.get('maId')).to.be.ok;
              expect(user.get('maId')).to.equal(1);
            });
          });
        });
      });
    }

    it('is possible to use casting when creating an instance', function(done) {
      var self = this
        , type = Support.dialectIsMySQL() ? 'signed' : 'integer'
        , _done = _.after(2, function() {
          done();
        });

      this.User.create({
        intVal: this.sequelize.cast('1', type)
      }).on('sql', function(sql) {
        expect(sql).to.match(new RegExp("CAST\\('1' AS " + type.toUpperCase() + '\\)'));
        _done();
      })
      .success(function(user) {
        self.User.find(user.id).success(function(user) {
          expect(user.intVal).to.equal(1);
          _done();
        });
      });
    });

    it('is possible to use casting multiple times mixed in with other utilities', function(done) {
      var self = this
        , type = this.sequelize.cast(this.sequelize.cast(this.sequelize.literal('1-2'), 'integer'), 'integer')
        , _done = _.after(2, function() {
          done();
        });

      if (Support.dialectIsMySQL()) {
        type = this.sequelize.cast(this.sequelize.cast(this.sequelize.literal('1-2'), 'unsigned'), 'signed');
      }

      this.User.create({
        intVal: type
      }).on('sql', function(sql) {
        if (Support.dialectIsMySQL()) {
          expect(sql).to.contain('CAST(CAST(1-2 AS UNSIGNED) AS SIGNED)');
        } else {
          expect(sql).to.contain('CAST(CAST(1-2 AS INTEGER) AS INTEGER)');
        }

        _done();
      }).success(function(user) {
        self.User.find(user.id).success(function(user) {
          expect(user.intVal).to.equal(-1);
          _done();
        });
      });
    });

    it('is possible to just use .literal() to bypass escaping', function(done) {
      var self = this;

      this.User.create({
        intVal: this.sequelize.literal('CAST(1-2 AS ' + (Support.dialectIsMySQL() ? 'SIGNED' : 'INTEGER') + ')')
      }).success(function(user) {
        self.User.find(user.id).success(function(user) {
          expect(user.intVal).to.equal(-1);
          done();
        });
      });
    });

    it('is possible to use funtions when creating an instance', function(done) {
      var self = this;
      this.User.create({
        secretValue: this.sequelize.fn('upper', 'sequelize')
      }).success(function(user) {
        self.User.find(user.id).success(function(user) {
          expect(user.secretValue).to.equal('SEQUELIZE');
          done();
        });
      });
    });

    it('should work with a non-id named uuid primary key columns', function() {
      var Monkey = this.sequelize.define('Monkey', {
        monkeyId: { type: DataTypes.UUID, primaryKey: true, defaultValue: DataTypes.UUIDV4, allowNull: false }
      });

      return this.sequelize.sync({force: true}).then(function() {
        return Monkey.create();
      }).then(function(monkey) {
        expect(monkey.get('monkeyId')).to.be.ok;
      });
    });

    it('is possible to use functions as default values', function(done) {
      var self = this
        , userWithDefaults;

      if (dialect.indexOf('postgres') === 0) {
        this.sequelize.query('CREATE EXTENSION IF NOT EXISTS "uuid-ossp"').success(function() {
          userWithDefaults = self.sequelize.define('userWithDefaults', {
            uuid: {
              type: 'UUID',
              defaultValue: self.sequelize.fn('uuid_generate_v4')
            }
          });

          userWithDefaults.sync({force: true}).success(function() {
            userWithDefaults.create({}).success(function(user) {
              // uuid validation regex taken from http://stackoverflow.com/a/13653180/800016
              expect(user.uuid).to.match(/^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i);
              done();
            });
          });
        });
      } else if (dialect === 'sqlite') {
        // The definition here is a bit hacky. sqlite expects () around the expression for default values, so we call a function without a name
        // to enclose the date function in (). http://www.sqlite.org/syntaxdiagrams.html#column-constraint
        userWithDefaults = self.sequelize.define('userWithDefaults', {
          year: {
            type: Sequelize.STRING,
            defaultValue: self.sequelize.fn('', self.sequelize.fn('date', 'now'))
          }
        });

        userWithDefaults.sync({force: true}).done(function(err) {
          expect(err).not.to.be.ok;
          userWithDefaults.create({}).done(function(err, user) {
            expect(err).not.to.be.ok;
            userWithDefaults.find(user.id).done(function(err, user) {
              expect(err).not.to.be.ok;
              var now = new Date()
                , pad = function(number) {
                  if (number > 9) {
                    return number;
                  }
                  return '0' + number;
                };

              expect(user.year).to.equal(now.getUTCFullYear() + '-' + pad(now.getUTCMonth() + 1) + '-' + pad(now.getUTCDate()));
              done();
            });
          });
        });
      } else {
        // functions as default values are not supported in mysql, see http://stackoverflow.com/a/270338/800016
        done();
      }
    });

    it('casts empty arrays correctly for postgresql insert', function(done) {
      if (dialect !== 'postgres') {
        expect('').to.equal('');
        return done();
      }

      var User = this.sequelize.define('UserWithArray', {
        myvals: { type: Sequelize.ARRAY(Sequelize.INTEGER) },
        mystr: { type: Sequelize.ARRAY(Sequelize.STRING) }
      });

      User.sync({force: true}).success(function() {
        User.create({myvals: [], mystr: []}).on('sql', function(sql) {
          expect(sql.indexOf('ARRAY[]::INTEGER[]')).to.be.above(-1);
          expect(sql.indexOf('ARRAY[]::VARCHAR[]')).to.be.above(-1);
          done();
        });
      });
    });

    it('casts empty array correct for postgres update', function(done) {
      if (dialect !== 'postgres') {
        expect('').to.equal('');
        return done();
      }

      var User = this.sequelize.define('UserWithArray', {
        myvals: { type: Sequelize.ARRAY(Sequelize.INTEGER) },
        mystr: { type: Sequelize.ARRAY(Sequelize.STRING) }
      });

      User.sync({force: true}).success(function() {
        User.create({myvals: [1, 2, 3, 4], mystr: ['One', 'Two', 'Three', 'Four']}).on('success', function(user) {
         user.myvals = [];
          user.mystr = [];
          user.save().on('sql', function(sql) {
            expect(sql.indexOf('ARRAY[]::INTEGER[]')).to.be.above(-1);
            expect(sql.indexOf('ARRAY[]::VARCHAR[]')).to.be.above(-1);
            done();
          });
        });
      });
    });

    it("doesn't allow duplicated records with unique:true", function(done) {
      var self = this
        , User = this.sequelize.define('UserWithUniqueUsername', {
            username: { type: Sequelize.STRING, unique: true }
          });

      User.sync({ force: true }).success(function() {
        User.create({ username: 'foo' }).success(function() {
          User.create({ username: 'foo' }).catch (self.sequelize.UniqueConstraintError, function(err) {
            done();
          });
        });
      });
    });

    it('raises an error if created object breaks definition contraints', function(done) {
      var UserNull = this.sequelize.define('UserWithNonNullSmth', {
        username: { type: Sequelize.STRING, unique: true },
        smth: { type: Sequelize.STRING, allowNull: false }
      });

      this.sequelize.options.omitNull = false;

      UserNull.sync({ force: true }).success(function() {
        UserNull.create({ username: 'foo2', smth: null }).error(function(err) {
          expect(err).to.exist;

          expect(err.get('smth')[0].path).to.equal('smth');
          if (Support.dialectIsMySQL()) {
            // We need to allow two different errors for MySQL, see:
            // http://dev.mysql.com/doc/refman/5.0/en/server-sql-mode.html#sqlmode_strict_trans_tables
            expect(err.get('smth')[0].type).to.match(/notNull Violation/);
          }
          else if (dialect === 'sqlite') {
            expect(err.get('smth')[0].type).to.match(/notNull Violation/);
          } else {
            expect(err.get('smth')[0].type).to.match(/notNull Violation/);
          }
          done();
        });
      });
    });
    it('raises an error if created object breaks definition contraints', function(done) {
      var self = this
        , UserNull = this.sequelize.define('UserWithNonNullSmth', {
            username: { type: Sequelize.STRING, unique: true },
            smth: { type: Sequelize.STRING, allowNull: false }
          });

      this.sequelize.options.omitNull = false;

      UserNull.sync({ force: true }).success(function() {
        UserNull.create({ username: 'foo', smth: 'foo' }).success(function() {
          UserNull.create({ username: 'foo', smth: 'bar' }).catch (self.sequelize.UniqueConstraintError, function(err) {
            done();
          });
        });
      });
    });

    it('raises an error if saving an empty string into a column allowing null or URL', function(done) {
      var StringIsNullOrUrl = this.sequelize.define('StringIsNullOrUrl', {
        str: { type: Sequelize.STRING, allowNull: true, validate: { isURL: true } }
      });

      this.sequelize.options.omitNull = false;

      StringIsNullOrUrl.sync({ force: true }).success(function() {
        StringIsNullOrUrl.create({ str: null }).success(function(str1) {
          expect(str1.str).to.be.null;
          StringIsNullOrUrl.create({ str: 'http://sequelizejs.org' }).success(function(str2) {
            expect(str2.str).to.equal('http://sequelizejs.org');
            StringIsNullOrUrl.create({ str: '' }).error(function(err) {
              expect(err).to.exist;
              expect(err.get('str')[0].message).to.match(/Validation isURL failed/);

              done();
            });
          });
        }).error(done);
      });
    });

    it('raises an error if you mess up the datatype', function(done) {
      var self = this;
      expect(function() {
        self.sequelize.define('UserBadDataType', {
          activity_date: Sequelize.DATe
        });
      }).to.throw(Error, 'Unrecognized data type for field activity_date');

      expect(function() {
        self.sequelize.define('UserBadDataType', {
          activity_date: {type: Sequelize.DATe}
        });
      }).to.throw(Error, 'Unrecognized data type for field activity_date');
      done();
    });

    it('sets a 64 bit int in bigint', function(done) {
      var User = this.sequelize.define('UserWithBigIntFields', {
        big: Sequelize.BIGINT
      });

      User.sync({ force: true }).success(function() {
        User.create({ big: '9223372036854775807' }).on('success', function(user) {
          expect(user.big).to.be.equal('9223372036854775807');
          done();
        });
      });
    });

    it('sets auto increment fields', function(done) {
      var User = this.sequelize.define('UserWithAutoIncrementField', {
        userid: { type: Sequelize.INTEGER, autoIncrement: true, primaryKey: true, allowNull: false }
      });

      User.sync({ force: true }).success(function() {
        User.create({}).on('success', function(user) {
          expect(user.userid).to.equal(1);

          User.create({}).on('success', function(user) {
            expect(user.userid).to.equal(2);
            done();
          });
        });
      });
    });

    it('allows the usage of options as attribute', function(done) {
      var User = this.sequelize.define('UserWithNameAndOptions', {
        name: Sequelize.STRING,
        options: Sequelize.TEXT
      });

      var options = JSON.stringify({ foo: 'bar', bar: 'foo' });

      User.sync({ force: true }).success(function() {
        User
          .create({ name: 'John Doe', options: options })
          .success(function(user) {
            expect(user.options).to.equal(options);
            done();
          });
      });
    });

    it('allows sql logging', function(done) {
      var User = this.sequelize.define('UserWithUniqueNameAndNonNullSmth', {
        name: {type: Sequelize.STRING, unique: true},
        smth: {type: Sequelize.STRING, allowNull: false}
      });

      User.sync({ force: true }).success(function() {
        User
          .create({ name: 'Fluffy Bunny', smth: 'else' })
          .on('sql', function(sql) {
            expect(sql).to.exist;
            expect(sql.toUpperCase().indexOf('INSERT')).to.be.above(-1);
            done();
          });
      });
    });

    it('should only store the values passed in the whitelist', function(done) {
      var self = this
        , data = { username: 'Peter', secretValue: '42' };

      this.User.create(data, { fields: ['username'] }).success(function(user) {
        self.User.find(user.id).success(function(_user) {
          expect(_user.username).to.equal(data.username);
          expect(_user.secretValue).not.to.equal(data.secretValue);
          expect(_user.secretValue).to.equal(null);
          done();
        });
      });
    });

    it('should store all values if no whitelist is specified', function(done) {
      var self = this
        , data = { username: 'Peter', secretValue: '42' };

      this.User.create(data).success(function(user) {
        self.User.find(user.id).success(function(_user) {
          expect(_user.username).to.equal(data.username);
          expect(_user.secretValue).to.equal(data.secretValue);
          done();
        });
      });
    });

    it('can omit autoincremental columns', function(done) {
      var self = this
        , data = { title: 'Iliad' }
        , dataTypes = [Sequelize.INTEGER, Sequelize.BIGINT]
        , chain = new Sequelize.Utils.QueryChainer()
        , chain2 = new Sequelize.Utils.QueryChainer()
        , books = [];

      dataTypes.forEach(function(dataType, index) {
        books[index] = self.sequelize.define('Book' + index, {
          id: { type: dataType, primaryKey: true, autoIncrement: true },
          title: Sequelize.TEXT
        });
      });

      books.forEach(function(b) {
        chain.add(b.sync({ force: true }));
      });

      chain.run().success(function() {
        books.forEach(function(b) {
          chain2.add(b.create(data));
        });
        chain2.run().success(function(results) {
          results.forEach(function(book, index) {
            expect(book.title).to.equal(data.title);
            expect(book.author).to.equal(data.author);
            expect(books[index].rawAttributes.id.type instanceof dataTypes[index]).to.be.ok;
          });
          done();
        });
      });
    });

    it('saves data with single quote', function(done) {
      var quote = "single'quote"
        , self = this;

      this.User.create({ data: quote }).success(function(user) {
        expect(user.data).to.equal(quote);

        self.User.find({where: { id: user.id }}).success(function(user) {
          expect(user.data).to.equal(quote);
          done();
        });
      });
    });

    it('saves data with double quote', function(done) {
      var quote = 'double"quote'
        , self = this;

      this.User.create({ data: quote }).success(function(user) {
        expect(user.data).to.equal(quote);

        self.User.find({where: { id: user.id }}).success(function(user) {
          expect(user.data).to.equal(quote);
          done();
        });
      });
    });

    it('saves stringified JSON data', function(done) {
      var json = JSON.stringify({ key: 'value' })
        , self = this;

      this.User.create({ data: json }).success(function(user) {
        expect(user.data).to.equal(json);
        self.User.find({where: { id: user.id }}).success(function(user) {
          expect(user.data).to.equal(json);
          done();
        });
      });
    });

    it('stores the current date in createdAt', function(done) {
      this.User.create({ username: 'foo' }).success(function(user) {
        expect(parseInt(+user.createdAt / 5000, 10)).to.be.closeTo(parseInt(+new Date() / 5000, 10), 1.5);
        done();
      });
    });

    it('allows setting custom IDs', function(done) {
      var self = this;
      this.User.create({ id: 42 }).success(function(user) {
        expect(user.id).to.equal(42);

        self.User.find(42).success(function(user) {
          expect(user).to.exist;
          done();
        });
      });
    });

    it('should allow blank creates (with timestamps: false)', function(done) {
      var Worker = this.sequelize.define('Worker', {}, {timestamps: false});
      Worker.sync().done(function() {
        Worker.create({}, {fields: []}).done(function(err, worker) {
          expect(err).not.to.be.ok;
          expect(worker).to.be.ok;
          done();
        });
      });
    });

    it('should allow truly blank creates', function(done) {
      var Worker = this.sequelize.define('Worker', {}, {timestamps: false});
      Worker.sync().done(function() {
        Worker.create({}, {fields: []}).done(function(err, worker) {
          expect(err).not.to.be.ok;
          expect(worker).to.be.ok;
          done();
        });
      });
    });

    it('should only set passed fields', function(done) {
      var User = this.sequelize.define('User', {
        'email': {
          type: DataTypes.STRING
        },
        'name': {
          type: DataTypes.STRING
        }
      });

      this.sequelize.sync({force: true}).done(function(err) {
        expect(err).not.to.be.ok;

        User.create({
          name: 'Yolo Bear',
          email: 'yolo@bear.com'
        }, {
          fields: ['name']
        }).done(function(err, user) {
          expect(err).not.to.be.ok;
          expect(user.name).to.be.ok;
          expect(user.email).not.to.be.ok;

          User.find(user.id).done(function(err, user) {
            expect(err).not.to.be.ok;
            expect(user.name).to.be.ok;
            expect(user.email).not.to.be.ok;
            done();
          });
        });
      });
    });

    describe('enums', function() {
      it('correctly restores enum values', function(done) {
        var self = this
          , Item = self.sequelize.define('Item', {
              state: { type: Sequelize.ENUM, values: ['available', 'in_cart', 'shipped'] }
            });

        Item.sync({ force: true }).success(function() {
          Item.create({ state: 'available' }).success(function(_item) {
            Item.find({ where: { state: 'available' }}).success(function(item) {
              expect(item.id).to.equal(_item.id);
              done();
            });
          });
        });
      });

      it('allows null values', function(done) {
        var Enum = this.sequelize.define('Enum', {
          state: {
            type: Sequelize.ENUM,
            values: ['happy', 'sad'],
            allowNull: true
          }
        });

        Enum.sync({ force: true }).success(function() {
          Enum.create({state: null}).success(function(_enum) {
            expect(_enum.state).to.be.null;
            done();
          });
        });
      });

      describe('when defined via { field: Sequelize.ENUM }', function() {
        it('allows values passed as parameters', function(done) {
          var Enum = this.sequelize.define('Enum', {
            state: Sequelize.ENUM('happy', 'sad')
          });

          Enum.sync({ force: true }).success(function() {
            Enum.create({ state: 'happy' }).success(function() {
              done();
            });
          });
        });

        it('allows values passed as an array', function(done) {
          var Enum = this.sequelize.define('Enum', {
            state: Sequelize.ENUM(['happy', 'sad'])
          });

          Enum.sync({ force: true }).success(function() {
            Enum.create({ state: 'happy' }).success(function() {
              done();
            });
          });
        });
      });

      describe('when defined via { field: { type: Sequelize.ENUM } }', function() {
        it('allows values passed as parameters', function(done) {
          var Enum = this.sequelize.define('Enum', {
            state: {
              type: Sequelize.ENUM('happy', 'sad')
            }
          });

          Enum.sync({ force: true }).success(function() {
            Enum.create({ state: 'happy' }).success(function() {
              done();
            });
          });
        });

        it('allows values passed as an array', function(done) {
          var Enum = this.sequelize.define('Enum', {
            state: {
              type: Sequelize.ENUM(['happy', 'sad'])
            }
          });

          Enum.sync({ force: true }).success(function() {
            Enum.create({ state: 'happy' }).success(function() {
              done();
            });
          });
        });
      });

      describe('can safely sync multiple times', function() {
        it('through the factory', function(done) {
          var Enum = this.sequelize.define('Enum', {
            state: {
              type: Sequelize.ENUM,
              values: ['happy', 'sad'],
              allowNull: true
            }
          });

          Enum.sync({ force: true }).success(function() {
            Enum.sync().success(function() {
              Enum.sync({ force: true }).complete(done);
            });
          });
        });

        it('through sequelize', function(done) {
          var self = this
            , Enum = this.sequelize.define('Enum', {
                state: {
                  type: Sequelize.ENUM,
                  values: ['happy', 'sad'],
                  allowNull: true
                }
              });

          this.sequelize.sync({ force: true }).success(function() {
            self.sequelize.sync().success(function() {
              self.sequelize.sync({ force: true }).complete(done);
            });
          });
        });
      });
    });
  });

  describe('bulkCreate', function() {
    if (current.dialect.supports.transactions) {
      it('supports transactions', function(done) {
        var self = this;
        this.sequelize.transaction().then(function(t) {
          self.User
            .bulkCreate([{ username: 'foo' }, { username: 'bar' }], { transaction: t })
            .success(function() {
              self.User.count().success(function(count1) {
                self.User.count({ transaction: t }).success(function(count2) {
                  expect(count1).to.equal(0);
                  expect(count2).to.equal(2);
                  t.rollback().success(function() { done(); });
                });
              });
            });
        });
      });
    }

    it('should be able to set createdAt and updatedAt if using silent: true', function () {
      var User = this.sequelize.define('user', {
        name: DataTypes.STRING
      }, {
        timestamps: true
      });

      var createdAt = new Date(2012, 10, 10, 10, 10, 10);
      var updatedAt = new Date(2011, 11, 11, 11, 11, 11);
      var values = _.map(new Array(10), function () {
        return {
          createdAt: createdAt,
          updatedAt: updatedAt
        };
      });

      return User.sync({force: true}).then(function () {
        return User.bulkCreate(values, {
          silent: true
        }).then(function () {
          return User.findAll({
            updatedAt: {
              ne: null
            }
          }).then(function (users) {
            users.forEach(function (user) {
              expect(createdAt.getTime()).to.equal(user.get('createdAt').getTime());
              expect(updatedAt.getTime()).to.equal(user.get('updatedAt').getTime());
            });
          });
        });
      });
    });

    it('should not fail on validate: true and individualHooks: true', function () {
      var User = this.sequelize.define('user', {
        name: Sequelize.STRING
      });

      return User.sync({force: true}).then(function () {
        return User.bulkCreate([
          {name: 'James'}
        ], {validate: true, individualHooks: true});
      });
    });

    it('properly handles disparate field lists', function(done) {
      var self = this
        , data = [{username: 'Peter', secretValue: '42', uniqueName: '1' },
                  {username: 'Paul', uniqueName: '2'},
                  {username: 'Steve', uniqueName: '3'}];

      this.User.bulkCreate(data).success(function() {
        self.User.findAll({where: {username: 'Paul'}}).success(function(users) {
          expect(users.length).to.equal(1);
          expect(users[0].username).to.equal('Paul');
          expect(users[0].secretValue).to.be.null;
          done();
        });
      });
    });

    it('inserts multiple values respecting the white list', function(done) {
      var self = this
        , data = [{ username: 'Peter', secretValue: '42', uniqueName: '1' },
                  { username: 'Paul', secretValue: '23', uniqueName: '2'}];

      this.User.bulkCreate(data, { fields: ['username', 'uniqueName'] }).success(function() {
        self.User.findAll({order: 'id'}).success(function(users) {
          expect(users.length).to.equal(2);
          expect(users[0].username).to.equal('Peter');
          expect(users[0].secretValue).to.be.null;
          expect(users[1].username).to.equal('Paul');
          expect(users[1].secretValue).to.be.null;
          done();
        });
      });
    });

    it('should store all values if no whitelist is specified', function(done) {
      var self = this
        , data = [{ username: 'Peter', secretValue: '42', uniqueName: '1' },
                  { username: 'Paul', secretValue: '23', uniqueName: '2'}];

      this.User.bulkCreate(data).success(function() {
        self.User.findAll({order: 'id'}).success(function(users) {
          expect(users.length).to.equal(2);
          expect(users[0].username).to.equal('Peter');
          expect(users[0].secretValue).to.equal('42');
          expect(users[1].username).to.equal('Paul');
          expect(users[1].secretValue).to.equal('23');
          done();
        });
      });
    });

    it('saves data with single quote', function(done) {
      var self = this
        , quote = "Single'Quote"
        , data = [{ username: 'Peter', data: quote, uniqueName: '1'},
                  { username: 'Paul', data: quote, uniqueName: '2'}];

      this.User.bulkCreate(data).success(function() {
        self.User.findAll({order: 'id'}).success(function(users) {
          expect(users.length).to.equal(2);
          expect(users[0].username).to.equal('Peter');
          expect(users[0].data).to.equal(quote);
          expect(users[1].username).to.equal('Paul');
          expect(users[1].data).to.equal(quote);
          done();
        });
      });
    });

    it('saves data with double quote', function(done) {
      var self = this
        , quote = 'Double"Quote'
        , data = [{ username: 'Peter', data: quote, uniqueName: '1'},
                  { username: 'Paul', data: quote, uniqueName: '2'}];

      this.User.bulkCreate(data).success(function() {
        self.User.findAll({order: 'id'}).success(function(users) {
          expect(users.length).to.equal(2);
          expect(users[0].username).to.equal('Peter');
          expect(users[0].data).to.equal(quote);
          expect(users[1].username).to.equal('Paul');
          expect(users[1].data).to.equal(quote);
          done();
        });
      });
    });

    it('saves stringified JSON data', function(done) {
      var self = this
        , json = JSON.stringify({ key: 'value' })
        , data = [{ username: 'Peter', data: json, uniqueName: '1'},
                  { username: 'Paul', data: json, uniqueName: '2'}];

      this.User.bulkCreate(data).success(function() {
        self.User.findAll({order: 'id'}).success(function(users) {
          expect(users.length).to.equal(2);
          expect(users[0].username).to.equal('Peter');
          expect(users[0].data).to.equal(json);
          expect(users[1].username).to.equal('Paul');
          expect(users[1].data).to.equal(json);
          done();
        });
      });
    });

    it('properly handles a model with a length column', function(done) {
      var UserWithLength = this.sequelize.define('UserWithLength', {
        length: Sequelize.INTEGER
      });

      UserWithLength.sync({force: true}).success(function() {
        UserWithLength.bulkCreate([{ length: 42}, {length: 11}]).success(function() {
          done();
        });
      });
    });

    it('stores the current date in createdAt', function(done) {
      var self = this
        , data = [{ username: 'Peter', uniqueName: '1'},
                  { username: 'Paul', uniqueName: '2'}];

      this.User.bulkCreate(data).success(function() {
        self.User.findAll({order: 'id'}).success(function(users) {
          expect(users.length).to.equal(2);
          expect(users[0].username).to.equal('Peter');
          expect(parseInt(+users[0].createdAt / 5000, 10)).to.be.closeTo(parseInt(+new Date() / 5000, 10), 1.5);
          expect(users[1].username).to.equal('Paul');
          expect(parseInt(+users[1].createdAt / 5000, 10)).to.be.closeTo(parseInt(+new Date() / 5000, 10), 1.5);
          done();
        });
      });
    });

    it('emits an error when validate is set to true', function(done) {
      var Tasks = this.sequelize.define('Task', {
        name: {
          type: Sequelize.STRING,
          allowNull: false
        },
        code: {
          type: Sequelize.STRING,
          validate: {
            len: [3, 10]
          }
        }
      });

      Tasks.sync({ force: true }).success(function() {
        Tasks.bulkCreate([
          {name: 'foo', code: '123'},
          {code: '1234'},
          {name: 'bar', code: '1'}
        ], { validate: true }).error(function(errors) {
          expect(errors).to.not.be.null;
          expect(errors).to.be.an('Array');
          expect(errors).to.have.length(2);
          expect(errors[0].record.code).to.equal('1234');
          expect(errors[0].errors.get('name')[0].type).to.equal('notNull Violation');
          expect(errors[1].record.name).to.equal('bar');
          expect(errors[1].record.code).to.equal('1');
          expect(errors[1].errors.get('code')[0].message).to.equal('Validation len failed');
          done();
        });
      });
    });

    it("doesn't emit an error when validate is set to true but our selectedValues are fine", function(done) {
      var Tasks = this.sequelize.define('Task', {
        name: {
          type: Sequelize.STRING,
          validate: {
            notEmpty: true
          }
        },
        code: {
          type: Sequelize.STRING,
          validate: {
            len: [3, 10]
          }
        }
      });

      Tasks.sync({ force: true }).success(function() {
        Tasks.bulkCreate([
          {name: 'foo', code: '123'},
          {code: '1234'}
        ], { fields: ['code'], validate: true }).success(function() {
          // we passed!
          done();
        });
      });
    });

    it('should allow blank arrays (return immediatly)', function(done) {
      var Worker = this.sequelize.define('Worker', {});
      Worker.sync().done(function() {
        Worker.bulkCreate([]).done(function(err, workers) {
          expect(err).not.to.be.ok;
          expect(workers).to.be.ok;
          expect(workers.length).to.equal(0);
          done();
        });
      });
    });

    it('should allow blank creates (with timestamps: false)', function(done) {
      var Worker = this.sequelize.define('Worker', {}, {timestamps: false});
      Worker.sync().done(function() {
        Worker.bulkCreate([{}, {}]).done(function(err, workers) {
          expect(err).not.to.be.ok;
          expect(workers).to.be.ok;
          done();
        });
      });
    });

    it('should allow autoincremented attributes to be set', function(done) {
      var Worker = this.sequelize.define('Worker', {}, {timestamps: false});
      Worker.sync().done(function() {
        Worker.bulkCreate([
          {id: 5},
          {id: 10}
        ]).done(function(err) {
          expect(err).not.to.be.ok;
          Worker.findAll({order: 'id ASC'}).done(function(err, workers) {
            expect(workers[0].id).to.equal(5);
            expect(workers[1].id).to.equal(10);
            done();
          });
        });
      });
    });

    it('should support schemas', function() {
      var Dummy = this.sequelize.define('Dummy', {
        foo: DataTypes.STRING,
        bar: DataTypes.STRING
      }, {
        schema: 'space1',
        tableName: 'Dummy'
      });

      return this.sequelize.dropAllSchemas().bind(this).then(function() {
        return this.sequelize.createSchema('space1');
      }).then(function() {
        return Dummy.sync({force: true});
      }).then(function() {
        return Dummy.bulkCreate([
          {foo: 'a', bar: 'b'},
          {foo: 'c', bar: 'd'}
        ]);
      });
    });

    if (dialect !== 'postgres' && dialect !== 'mssql') {
      it('should support the ignoreDuplicates option', function(done) {
        var self = this
          , data = [{ uniqueName: 'Peter', secretValue: '42' },
                    { uniqueName: 'Paul', secretValue: '23' }];

        this.User.bulkCreate(data, { fields: ['uniqueName', 'secretValue'] }).success(function() {
          data.push({ uniqueName: 'Michael', secretValue: '26' });
          self.User.bulkCreate(data, { fields: ['uniqueName', 'secretValue'], ignoreDuplicates: true }).success(function() {
            self.User.findAll({order: 'id'}).success(function(users) {
              expect(users.length).to.equal(3);
              expect(users[0].uniqueName).to.equal('Peter');
              expect(users[0].secretValue).to.equal('42');
              expect(users[1].uniqueName).to.equal('Paul');
              expect(users[1].secretValue).to.equal('23');
              expect(users[2].uniqueName).to.equal('Michael');
              expect(users[2].secretValue).to.equal('26');
              done();
            });
          });
        });
      });
    } else {
      it('should throw an error when the ignoreDuplicates option is passed', function(done) {
        var self = this
          , data = [{ uniqueName: 'Peter', secretValue: '42' },
                    { uniqueName: 'Paul', secretValue: '23' }];

        this.User.bulkCreate(data, { fields: ['uniqueName', 'secretValue'] }).success(function() {
          data.push({ uniqueName: 'Michael', secretValue: '26' });

          self.User.bulkCreate(data, { fields: ['uniqueName', 'secretValue'], ignoreDuplicates: true }).error(function(err) {
            expect(err).to.exist;
            if (dialect === 'mssql') {
              console.log(err.message);
              expect(err.message).to.match(/mssql does not support the \'ignoreDuplicates\' option./);
            } else {
              expect(err.message).to.match(/postgres does not support the \'ignoreDuplicates\' option./);
            }

            done();
          });
        });
      });
    }

    if (current.dialect.supports.returnValues) {
      describe('return values', function () {
        it('should make the autoincremented values available on the returned instances', function () {
          var User = this.sequelize.define('user', {});

          return User.sync({force: true}).then(function () {
            return User.bulkCreate([
              {},
              {},
              {}
            ], {returning: true}).then(function (users) {
              expect(users.length).to.be.ok;
              users.forEach(function (user, i) {
                expect(user.get('id')).to.be.ok;
                expect(user.get('id')).to.equal(i+1);
              });
            });
          });
        });

        it('should make the autoincremented values available on the returned instances with custom fields', function () {
          var User = this.sequelize.define('user', {
            maId: {
              type: DataTypes.INTEGER,
              primaryKey: true,
              autoIncrement: true,
              field: 'yo_id'
            }
          });

          return User.sync({force: true}).then(function () {
            return User.bulkCreate([
              {},
              {},
              {}
            ], {returning: true}).then(function (users) {
              expect(users.length).to.be.ok;
              users.forEach(function (user, i) {
                expect(user.get('maId')).to.be.ok;
                expect(user.get('maId')).to.equal(i+1);
              });
            });
          });
        });
      });
    }

    describe('enums', function() {
      it('correctly restores enum values', function(done) {
        var self = this
          , Item = self.sequelize.define('Item', {
              state: { type: Sequelize.ENUM, values: ['available', 'in_cart', 'shipped'] },
              name: Sequelize.STRING
            });

        Item.sync({ force: true }).success(function() {
          Item.bulkCreate([{state: 'in_cart', name: 'A'}, { state: 'available', name: 'B'}]).success(function() {
            Item.find({ where: { state: 'available' }}).success(function(item) {
              expect(item.name).to.equal('B');
              done();
            });
          });
        });
      });
    });
  });

  it('should support logging', function () {
    var spy = sinon.spy();

    return this.User.create({}, {
      logging: spy
    }).then(function () {
      expect(spy.called).to.be.ok;
    });
  });
});