'use strict';

const chai = require('chai'),
  expect = chai.expect,
  Support = require('../support'),
  Sequelize = require('../../../index'),
  Promise = Sequelize.Promise,
  current = Support.sequelize,
  dialect = Support.getTestDialect();

describe(Support.getTestDialectTeaser('HasOne'), () => {
  describe('Model.associations', () => {
    it('should store all associations when associating to the same table multiple times', function() {
      const User = this.sequelize.define('User', {}),
        Group = this.sequelize.define('Group', {});

      Group.hasOne(User);
      Group.hasOne(User, { foreignKey: 'primaryGroupId', as: 'primaryUsers' });
      Group.hasOne(User, { foreignKey: 'secondaryGroupId', as: 'secondaryUsers' });

      expect(
        Object.keys(Group.associations)
      ).to.deep.equal(['User', 'primaryUsers', 'secondaryUsers']);
    });
  });

  describe('get', () => {
    describe('multiple', () => {
      it('should fetch associations for multiple instances', function() {
        const User = this.sequelize.define('User', {}),
          Player = this.sequelize.define('Player', {});

        Player.User = Player.hasOne(User, { as: 'user' });

        return this.sequelize.sync({ force: true }).then(() => {
          return Promise.join(
            Player.create({
              id: 1,
              user: {}
            }, {
              include: [Player.User]
            }),
            Player.create({
              id: 2,
              user: {}
            }, {
              include: [Player.User]
            }),
            Player.create({
              id: 3
            })
          );
        }).then(players => {
          return Player.User.get(players).then(result => {
            expect(result[players[0].id].id).to.equal(players[0].user.id);
            expect(result[players[1].id].id).to.equal(players[1].user.id);
            expect(result[players[2].id]).to.equal(null);
          });
        });
      });
    });
  });

  describe('getAssociation', () => {
    if (current.dialect.supports.transactions) {
      it('supports transactions', function() {
        return Support.prepareTransactionTest(this.sequelize).then(sequelize => {
          const User = sequelize.define('User', { username: Support.Sequelize.STRING }),
            Group = sequelize.define('Group', { name: Support.Sequelize.STRING });

          Group.hasOne(User);

          return sequelize.sync({ force: true }).then(() => {
            return User.create({ username: 'foo' }).then(fakeUser => {
              return User.create({ username: 'foo' }).then(user => {
                return Group.create({ name: 'bar' }).then(group => {
                  return sequelize.transaction().then(t => {
                    return group.setUser(user, { transaction: t }).then(() => {
                      return Group.findAll().then(groups => {
                        return groups[0].getUser().then(associatedUser => {
                          expect(associatedUser).to.be.null;
                          return Group.findAll({ transaction: t }).then(groups => {
                            return groups[0].getUser({ transaction: t }).then(associatedUser => {
                              expect(associatedUser).not.to.be.null;
                              expect(associatedUser.id).to.equal(user.id);
                              expect(associatedUser.id).not.to.equal(fakeUser.id);
                              return t.rollback();
                            });
                          });
                        });
                      });
                    });
                  });
                });
              });
            });
          });
        });
      });
    }

    it('should be able to handle a where object that\'s a first class citizen.', function() {
      const User = this.sequelize.define('UserXYZ', { username: Sequelize.STRING }),
        Task = this.sequelize.define('TaskXYZ', { title: Sequelize.STRING, status: Sequelize.STRING });

      User.hasOne(Task);

      return User.sync({ force: true }).then(() => {
        return Task.sync({ force: true }).then(() => {
          return User.create({ username: 'foo' }).then(user => {
            return Task.create({ title: 'task', status: 'inactive' }).then(task => {
              return user.setTaskXYZ(task).then(() => {
                return user.getTaskXYZ({ where: { status: 'active' } }).then(task => {
                  expect(task).to.be.null;
                });
              });
            });
          });
        });
      });
    });

    it('supports schemas', function() {
      const User = this.sequelize.define('User', { username: Support.Sequelize.STRING }).schema('admin'),
        Group = this.sequelize.define('Group', { name: Support.Sequelize.STRING }).schema('admin');

      Group.hasOne(User);

      return Support.dropTestSchemas(this.sequelize).then(() => {
        return this.sequelize.createSchema('admin');
      }).then(() => {
        return Group.sync({ force: true });
      }).then(() => {
        return User.sync({ force: true });
      }).then(() => {
        return Promise.all([
          User.create({ username: 'foo' }),
          User.create({ username: 'foo' }),
          Group.create({ name: 'bar' })
        ]);
      }).then(([fakeUser, user, group]) => {
        return group.setUser(user).then(() => {
          return Group.findAll().then(groups => {
            return groups[0].getUser().then(associatedUser => {
              expect(associatedUser).not.to.be.null;
              expect(associatedUser.id).to.equal(user.id);
              expect(associatedUser.id).not.to.equal(fakeUser.id);
            });
          });
        });
      }).then(() => {
        return this.sequelize.dropSchema('admin').then(() => {
          return this.sequelize.showAllSchemas().then(schemas => {
            if (dialect === 'postgres' || dialect === 'mssql' || dialect === 'mariadb') {
              expect(schemas).to.not.have.property('admin');
            }
          });
        });
      });
    });
  });

  describe('setAssociation', () => {
    if (current.dialect.supports.transactions) {
      it('supports transactions', function() {
        return Support.prepareTransactionTest(this.sequelize).then(sequelize => {
          const User = sequelize.define('User', { username: Support.Sequelize.STRING }),
            Group = sequelize.define('Group', { name: Support.Sequelize.STRING });

          Group.hasOne(User);

          return sequelize.sync({ force: true }).then(() => {
            return User.create({ username: 'foo' }).then(user => {
              return Group.create({ name: 'bar' }).then(group => {
                return sequelize.transaction().then(t => {
                  return group.setUser(user, { transaction: t }).then(() => {
                    return Group.findAll().then(groups => {
                      return groups[0].getUser().then(associatedUser => {
                        expect(associatedUser).to.be.null;
                        return t.rollback();
                      });
                    });
                  });
                });
              });
            });
          });
        });
      });
    }

    it('can set an association with predefined primary keys', function() {
      const User = this.sequelize.define('UserXYZZ', { userCoolIdTag: { type: Sequelize.INTEGER, primaryKey: true }, username: Sequelize.STRING }),
        Task = this.sequelize.define('TaskXYZZ', { taskOrSomething: { type: Sequelize.INTEGER, primaryKey: true }, title: Sequelize.STRING });

      User.hasOne(Task, { foreignKey: 'userCoolIdTag' });

      return User.sync({ force: true }).then(() => {
        return Task.sync({ force: true }).then(() => {
          return User.create({ userCoolIdTag: 1, username: 'foo' }).then(user => {
            return Task.create({ taskOrSomething: 1, title: 'bar' }).then(task => {
              return user.setTaskXYZZ(task).then(() => {
                return user.getTaskXYZZ().then(task => {
                  expect(task).not.to.be.null;

                  return user.setTaskXYZZ(null).then(() => {
                    return user.getTaskXYZZ().then(_task => {
                      expect(_task).to.be.null;
                    });
                  });
                });
              });
            });
          });
        });
      });
    });

    it('clears the association if null is passed', function() {
      const User = this.sequelize.define('UserXYZ', { username: Sequelize.STRING }),
        Task = this.sequelize.define('TaskXYZ', { title: Sequelize.STRING });

      User.hasOne(Task);

      return User.sync({ force: true }).then(() => {
        return Task.sync({ force: true }).then(() => {
          return User.create({ username: 'foo' }).then(user => {
            return Task.create({ title: 'task' }).then(task => {
              return user.setTaskXYZ(task).then(() => {
                return user.getTaskXYZ().then(task => {
                  expect(task).not.to.equal(null);

                  return user.setTaskXYZ(null).then(() => {
                    return user.getTaskXYZ().then(task => {
                      expect(task).to.equal(null);
                    });
                  });
                });
              });
            });
          });
        });
      });
    });

    it('should throw a ForeignKeyConstraintError if the associated record does not exist', function() {
      const User = this.sequelize.define('UserXYZ', { username: Sequelize.STRING }),
        Task = this.sequelize.define('TaskXYZ', { title: Sequelize.STRING });

      User.hasOne(Task);

      return User.sync({ force: true }).then(() => {
        return Task.sync({ force: true }).then(() => {
          return expect(Task.create({ title: 'task', UserXYZId: 5 })).to.be.rejectedWith(Sequelize.ForeignKeyConstraintError).then(() => {
            return Task.create({ title: 'task' }).then(task => {
              return expect(Task.update({ title: 'taskUpdate', UserXYZId: 5 }, { where: { id: task.id } })).to.be.rejectedWith(Sequelize.ForeignKeyConstraintError);
            });
          });
        });
      });
    });

    it('supports passing the primary key instead of an object', function() {
      const User = this.sequelize.define('UserXYZ', { username: Sequelize.STRING }),
        Task = this.sequelize.define('TaskXYZ', { title: Sequelize.STRING });

      User.hasOne(Task);

      return this.sequelize.sync({ force: true }).then(() => {
        return User.create({}).then(user => {
          return Task.create({ id: 19, title: 'task it!' }).then(task => {
            return user.setTaskXYZ(task.id).then(() => {
              return user.getTaskXYZ().then(task => {
                expect(task.title).to.equal('task it!');
              });
            });
          });
        });
      });
    });

    it('supports updating with a primary key instead of an object', function() {
      const User = this.sequelize.define('UserXYZ', { username: Sequelize.STRING }),
        Task = this.sequelize.define('TaskXYZ', { title: Sequelize.STRING });

      User.hasOne(Task);

      return this.sequelize.sync({ force: true }).then(() => {
        return Promise.all([
          User.create({ id: 1, username: 'foo' }),
          Task.create({ id: 20, title: 'bar' })
        ]);
      })
        .then(([user, task]) => {
          return user.setTaskXYZ(task.id)
            .then(() => user.getTaskXYZ())
            .then(task => {
              expect(task).not.to.be.null;
              return Promise.all([
                user,
                Task.create({ id: 2, title: 'bar2' })
              ]);
            });
        })
        .then(([user, task2]) => {
          return user.setTaskXYZ(task2.id)
            .then(() => user.getTaskXYZ())
            .then(task => {
              expect(task).not.to.be.null;
            });
        });
    });

    it('supports setting same association twice', function() {
      const Home = this.sequelize.define('home', {}),
        User = this.sequelize.define('user');

      User.hasOne(Home);

      const ctx = {};
      return this.sequelize.sync({ force: true }).then(() => {
        return Promise.all([
          Home.create(),
          User.create()
        ]);
      }).then(([home, user]) => {
        ctx.home = home;
        ctx.user = user;
        return user.setHome(home);
      }).then(() => {
        return ctx.user.setHome(ctx.home);
      }).then(() => {
        return expect(ctx.user.getHome()).to.eventually.have.property('id', ctx.home.get('id'));
      });
    });
  });

  describe('createAssociation', () => {
    it('creates an associated model instance', function() {
      const User = this.sequelize.define('User', { username: Sequelize.STRING }),
        Task = this.sequelize.define('Task', { title: Sequelize.STRING });

      User.hasOne(Task);

      return this.sequelize.sync({ force: true }).then(() => {
        return User.create({ username: 'bob' }).then(user => {
          return user.createTask({ title: 'task' }).then(() => {
            return user.getTask().then(task => {
              expect(task).not.to.be.null;
              expect(task.title).to.equal('task');
            });
          });
        });
      });
    });

    if (current.dialect.supports.transactions) {
      it('supports transactions', function() {
        return Support.prepareTransactionTest(this.sequelize).then(sequelize => {
          const User = sequelize.define('User', { username: Sequelize.STRING }),
            Group = sequelize.define('Group', { name: Sequelize.STRING });

          User.hasOne(Group);

          return sequelize.sync({ force: true }).then(() => {
            return User.create({ username: 'bob' }).then(user => {
              return sequelize.transaction().then(t => {
                return user.createGroup({ name: 'testgroup' }, { transaction: t }).then(() => {
                  return User.findAll().then(users => {
                    return users[0].getGroup().then(group => {
                      expect(group).to.be.null;
                      return User.findAll({ transaction: t }).then(users => {
                        return users[0].getGroup({ transaction: t }).then(group => {
                          expect(group).to.be.not.null;
                          return t.rollback();
                        });
                      });
                    });
                  });
                });
              });
            });
          });
        });
      });
    }

  });

  describe('foreign key', () => {
    it('should setup underscored field with foreign keys when using underscored', function() {
      const User = this.sequelize.define('User', { username: Sequelize.STRING }, { underscored: true }),
        Account = this.sequelize.define('Account', { name: Sequelize.STRING }, { underscored: true });

      Account.hasOne(User);

      expect(User.rawAttributes.AccountId).to.exist;
      expect(User.rawAttributes.AccountId.field).to.equal('account_id');
    });

    it('should use model name when using camelcase', function() {
      const User = this.sequelize.define('User', { username: Sequelize.STRING }, { underscored: false }),
        Account = this.sequelize.define('Account', { name: Sequelize.STRING }, { underscored: false });

      Account.hasOne(User);

      expect(User.rawAttributes.AccountId).to.exist;
      expect(User.rawAttributes.AccountId.field).to.equal('AccountId');
    });

    it('should support specifying the field of a foreign key', function() {
      const User = this.sequelize.define('UserXYZ', { username: Sequelize.STRING, gender: Sequelize.STRING }),
        Task = this.sequelize.define('TaskXYZ', { title: Sequelize.STRING, status: Sequelize.STRING });

      Task.hasOne(User, {
        foreignKey: {
          name: 'taskId',
          field: 'task_id'
        }
      });

      expect(User.rawAttributes.taskId).to.exist;
      expect(User.rawAttributes.taskId.field).to.equal('task_id');
      return Task.sync({ force: true }).then(() => {
        // Can't use Promise.all cause of foreign key references
        return User.sync({ force: true });
      }).then(() => {
        return Promise.all([
          User.create({ username: 'foo', gender: 'male' }),
          Task.create({ title: 'task', status: 'inactive' })
        ]);
      }).then(([user, task]) => {
        return task.setUserXYZ(user).then(() => {
          return task.getUserXYZ();
        });
      }).then(user => {
        // the sql query should correctly look at task_id instead of taskId
        expect(user).to.not.be.null;
        return Task.findOne({
          where: { title: 'task' },
          include: [User]
        });
      }).then(task => {
        expect(task.UserXYZ).to.exist;
      });
    });
  });

  describe('foreign key constraints', () => {
    it('are enabled by default', function() {
      const Task = this.sequelize.define('Task', { title: Sequelize.STRING }),
        User = this.sequelize.define('User', { username: Sequelize.STRING });

      User.hasOne(Task); // defaults to set NULL

      return User.sync({ force: true }).then(() => {
        return Task.sync({ force: true }).then(() => {
          return User.create({ username: 'foo' }).then(user => {
            return Task.create({ title: 'task' }).then(task => {
              return user.setTask(task).then(() => {
                return user.destroy().then(() => {
                  return task.reload().then(() => {
                    expect(task.UserId).to.equal(null);
                  });
                });
              });
            });
          });
        });
      });
    });

    it('sets to CASCADE if allowNull: false', function() {
      const Task = this.sequelize.define('Task', { title: Sequelize.STRING }),
        User = this.sequelize.define('User', { username: Sequelize.STRING });

      User.hasOne(Task, { foreignKey: { allowNull: false } }); // defaults to CASCADE

      return this.sequelize.sync({ force: true }).then(() => {
        return User.create({ username: 'foo' }).then(user => {
          return Task.create({ title: 'task', UserId: user.id }).then(() => {
            return user.destroy().then(() => {
              return Task.findAll();
            });
          });
        }).then(tasks => {
          expect(tasks).to.be.empty;
        });
      });
    });

    it('should be possible to disable them', function() {
      const Task = this.sequelize.define('Task', { title: Sequelize.STRING }),
        User = this.sequelize.define('User', { username: Sequelize.STRING });

      User.hasOne(Task, { constraints: false });

      return User.sync({ force: true }).then(() => {
        return Task.sync({ force: true }).then(() => {
          return User.create({ username: 'foo' }).then(user => {
            return Task.create({ title: 'task' }).then(task => {
              return user.setTask(task).then(() => {
                return user.destroy().then(() => {
                  return task.reload().then(() => {
                    expect(task.UserId).to.equal(user.id);
                  });
                });
              });
            });
          });
        });
      });
    });

    it('can cascade deletes', function() {
      const Task = this.sequelize.define('Task', { title: Sequelize.STRING }),
        User = this.sequelize.define('User', { username: Sequelize.STRING });

      User.hasOne(Task, { onDelete: 'cascade' });

      return User.sync({ force: true }).then(() => {
        return Task.sync({ force: true }).then(() => {
          return User.create({ username: 'foo' }).then(user => {
            return Task.create({ title: 'task' }).then(task => {
              return user.setTask(task).then(() => {
                return user.destroy().then(() => {
                  return Task.findAll().then(tasks => {
                    expect(tasks).to.have.length(0);
                  });
                });
              });
            });
          });
        });
      });
    });

    it('works when cascading a delete with hooks but there is no associate (i.e. "has zero")', function() {
      const Task = this.sequelize.define('Task', { title: Sequelize.STRING }),
        User = this.sequelize.define('User', { username: Sequelize.STRING });

      User.hasOne(Task, { onDelete: 'cascade', hooks: true });

      return User.sync({ force: true }).then(() => {
        return Task.sync({ force: true }).then(() => {
          return User.create({ username: 'foo' }).then(user => {
            return user.destroy();
          });
        });
      });
    });

    // NOTE: mssql does not support changing an autoincrement primary key
    if (Support.getTestDialect() !== 'mssql') {
      it('can cascade updates', function() {
        const Task = this.sequelize.define('Task', { title: Sequelize.STRING }),
          User = this.sequelize.define('User', { username: Sequelize.STRING });

        User.hasOne(Task, { onUpdate: 'cascade' });

        return User.sync({ force: true }).then(() => {
          return Task.sync({ force: true }).then(() => {
            return User.create({ username: 'foo' }).then(user => {
              return Task.create({ title: 'task' }).then(task => {
                return user.setTask(task).then(() => {

                  // Changing the id of a DAO requires a little dance since
                  // the `UPDATE` query generated by `save()` uses `id` in the
                  // `WHERE` clause

                  const tableName = user.sequelize.getQueryInterface().QueryGenerator.addSchema(user.constructor);
                  return user.sequelize.getQueryInterface().update(user, tableName, { id: 999 }, { id: user.id }).then(() => {
                    return Task.findAll().then(tasks => {
                      expect(tasks).to.have.length(1);
                      expect(tasks[0].UserId).to.equal(999);
                    });
                  });
                });
              });
            });
          });
        });
      });
    }

    if (current.dialect.supports.constraints.restrict) {

      it('can restrict deletes', function() {
        const Task = this.sequelize.define('Task', { title: Sequelize.STRING }),
          User = this.sequelize.define('User', { username: Sequelize.STRING });

        User.hasOne(Task, { onDelete: 'restrict' });

        return User.sync({ force: true }).then(() => {
          return Task.sync({ force: true }).then(() => {
            return User.create({ username: 'foo' }).then(user => {
              return Task.create({ title: 'task' }).then(task => {
                return user.setTask(task).then(() => {
                  return expect(user.destroy()).to.eventually.be.rejectedWith(Sequelize.ForeignKeyConstraintError).then(() => {
                    return Task.findAll().then(tasks => {
                      expect(tasks).to.have.length(1);
                    });
                  });
                });
              });
            });
          });
        });
      });

      it('can restrict updates', function() {
        const Task = this.sequelize.define('Task', { title: Sequelize.STRING }),
          User = this.sequelize.define('User', { username: Sequelize.STRING });

        User.hasOne(Task, { onUpdate: 'restrict' });

        return User.sync({ force: true }).then(() => {
          return Task.sync({ force: true }).then(() => {
            return User.create({ username: 'foo' }).then(user => {
              return Task.create({ title: 'task' }).then(task => {
                return user.setTask(task).then(() => {

                  // Changing the id of a DAO requires a little dance since
                  // the `UPDATE` query generated by `save()` uses `id` in the
                  // `WHERE` clause

                  const tableName = user.sequelize.getQueryInterface().QueryGenerator.addSchema(user.constructor);
                  return expect(
                    user.sequelize.getQueryInterface().update(user, tableName, { id: 999 }, { id: user.id })
                  ).to.eventually.be.rejectedWith(Sequelize.ForeignKeyConstraintError).then(() => {
                    // Should fail due to FK restriction
                    return Task.findAll().then(tasks => {
                      expect(tasks).to.have.length(1);
                    });
                  });
                });
              });
            });
          });
        });
      });

    }

  });

  describe('association column', () => {
    it('has correct type for non-id primary keys with non-integer type', function() {
      const User = this.sequelize.define('UserPKBT', {
        username: {
          type: Sequelize.STRING
        }
      });

      const Group = this.sequelize.define('GroupPKBT', {
        name: {
          type: Sequelize.STRING,
          primaryKey: true
        }
      });

      Group.hasOne(User);

      return this.sequelize.sync({ force: true }).then(() => {
        expect(User.rawAttributes.GroupPKBTName.type).to.an.instanceof(Sequelize.STRING);
      });
    });

    it('should support a non-primary key as the association column on a target with custom primary key', function() {
      const User = this.sequelize.define('User', {
        user_name: {
          unique: true,
          type: Sequelize.STRING
        }
      });

      const Task = this.sequelize.define('Task', {
        title: Sequelize.STRING,
        username: Sequelize.STRING
      });

      User.hasOne(Task, { foreignKey: 'username', sourceKey: 'user_name' });

      return this.sequelize.sync({ force: true }).then(() => {
        return User.create({ user_name: 'bob' }).then(newUser => {
          return Task.create({ title: 'some task' }).then(newTask => {
            return newUser.setTask(newTask).then(() => {
              return User.findOne({ where: { user_name: 'bob' } }).then(foundUser => {
                return foundUser.getTask().then(foundTask => {
                  expect(foundTask.title).to.equal('some task');
                });
              });
            });
          });
        });
      });
    });

    it('should support a non-primary unique key as the association column', function() {
      const User = this.sequelize.define('User', {
        username: {
          type: Sequelize.STRING,
          unique: true
        }
      });

      const Task = this.sequelize.define('Task', {
        title: Sequelize.STRING,
        username: Sequelize.STRING
      });

      User.hasOne(Task, { foreignKey: 'username', sourceKey: 'username' });

      return this.sequelize.sync({ force: true }).then(() => {
        return User.create({ username: 'bob' }).then(newUser => {
          return Task.create({ title: 'some task' }).then(newTask => {
            return newUser.setTask(newTask).then(() => {
              return User.findOne({ where: { username: 'bob' } }).then(foundUser => {
                return foundUser.getTask().then(foundTask => {
                  expect(foundTask.title).to.equal('some task');
                });
              });
            });
          });
        });
      });
    });

    it('should support a non-primary unique key as the association column with a field option', function() {
      const User = this.sequelize.define('User', {
        username: {
          type: Sequelize.STRING,
          unique: true,
          field: 'the_user_name_field'
        }
      });

      const Task = this.sequelize.define('Task', {
        title: Sequelize.STRING,
        username: Sequelize.STRING
      });

      User.hasOne(Task, { foreignKey: 'username', sourceKey: 'username' });

      return this.sequelize.sync({ force: true }).then(() => {
        return User.create({ username: 'bob' }).then(newUser => {
          return Task.create({ title: 'some task' }).then(newTask => {
            return newUser.setTask(newTask).then(() => {
              return User.findOne({ where: { username: 'bob' } }).then(foundUser => {
                return foundUser.getTask().then(foundTask => {
                  expect(foundTask.title).to.equal('some task');
                });
              });
            });
          });
        });
      });
    });
  });

  describe('Association options', () => {
    it('can specify data type for autogenerated relational keys', function() {
      const User = this.sequelize.define('UserXYZ', { username: Sequelize.STRING }),
        dataTypes = [Sequelize.INTEGER, Sequelize.BIGINT, Sequelize.STRING],
        Tasks = {};

      return Promise.map(dataTypes, dataType => {
        const tableName = `TaskXYZ_${dataType.key}`;
        Tasks[dataType] = this.sequelize.define(tableName, { title: Sequelize.STRING });

        User.hasOne(Tasks[dataType], { foreignKey: 'userId', keyType: dataType, constraints: false });

        return Tasks[dataType].sync({ force: true }).then(() => {
          expect(Tasks[dataType].rawAttributes.userId.type).to.be.an.instanceof(dataType);
        });
      });
    });

    describe('allows the user to provide an attribute definition object as foreignKey', () => {
      it('works with a column that hasnt been defined before', function() {
        const User = this.sequelize.define('user', {});
        let Profile = this.sequelize.define('project', {});

        User.hasOne(Profile, {
          foreignKey: {
            allowNull: false,
            name: 'uid'
          }
        });

        expect(Profile.rawAttributes.uid).to.be.ok;
        expect(Profile.rawAttributes.uid.references.model).to.equal(User.getTableName());
        expect(Profile.rawAttributes.uid.references.key).to.equal('id');
        expect(Profile.rawAttributes.uid.allowNull).to.be.false;

        // Let's clear it
        Profile = this.sequelize.define('project', {});
        User.hasOne(Profile, {
          foreignKey: {
            allowNull: false,
            name: 'uid'
          }
        });

        expect(Profile.rawAttributes.uid).to.be.ok;
        expect(Profile.rawAttributes.uid.references.model).to.equal(User.getTableName());
        expect(Profile.rawAttributes.uid.references.key).to.equal('id');
        expect(Profile.rawAttributes.uid.allowNull).to.be.false;
      });

      it('works when taking a column directly from the object', function() {
        const User = this.sequelize.define('user', {
            uid: {
              type: Sequelize.INTEGER,
              primaryKey: true
            }
          }),
          Profile = this.sequelize.define('project', {
            user_id: {
              type: Sequelize.INTEGER,
              allowNull: false
            }
          });

        User.hasOne(Profile, { foreignKey: Profile.rawAttributes.user_id });

        expect(Profile.rawAttributes.user_id).to.be.ok;
        expect(Profile.rawAttributes.user_id.references.model).to.equal(User.getTableName());
        expect(Profile.rawAttributes.user_id.references.key).to.equal('uid');
        expect(Profile.rawAttributes.user_id.allowNull).to.be.false;
      });

      it('works when merging with an existing definition', function() {
        const User = this.sequelize.define('user', {
            uid: {
              type: Sequelize.INTEGER,
              primaryKey: true
            }
          }),
          Project = this.sequelize.define('project', {
            userUid: {
              type: Sequelize.INTEGER,
              defaultValue: 42
            }
          });

        User.hasOne(Project, { foreignKey: { allowNull: false } });

        expect(Project.rawAttributes.userUid).to.be.ok;
        expect(Project.rawAttributes.userUid.allowNull).to.be.false;
        expect(Project.rawAttributes.userUid.references.model).to.equal(User.getTableName());
        expect(Project.rawAttributes.userUid.references.key).to.equal('uid');
        expect(Project.rawAttributes.userUid.defaultValue).to.equal(42);
      });
    });

    it('should throw an error if an association clashes with the name of an already define attribute', function() {
      const User = this.sequelize.define('user', {
          attribute: Sequelize.STRING
        }),
        Attribute = this.sequelize.define('attribute', {});

      expect(User.hasOne.bind(User, Attribute)).to
        .throw('Naming collision between attribute \'attribute\' and association \'attribute\' on model user. To remedy this, change either foreignKey or as in your association definition');
    });
  });

  describe('Counter part', () => {
    describe('BelongsTo', () => {
      it('should only generate one foreign key', function() {
        const Orders = this.sequelize.define('Orders', {}, { timestamps: false }),
          InternetOrders = this.sequelize.define('InternetOrders', {}, { timestamps: false });

        InternetOrders.belongsTo(Orders, {
          foreignKeyConstraint: true
        });
        Orders.hasOne(InternetOrders, {
          foreignKeyConstraint: true
        });

        expect(Object.keys(InternetOrders.rawAttributes).length).to.equal(2);
        expect(InternetOrders.rawAttributes.OrderId).to.be.ok;
        expect(InternetOrders.rawAttributes.OrdersId).not.to.be.ok;
      });
    });
  });

  describe('Eager loading', () => {
    beforeEach(function() {
      this.Individual = this.sequelize.define('individual', {
        name: Sequelize.STRING
      });
      this.Hat = this.sequelize.define('hat', {
        name: Sequelize.STRING
      });
      this.Individual.hasOne(this.Hat, {
        as: 'personwearinghat'
      });
    });

    it('should load with an alias', function() {
      return this.sequelize.sync({ force: true }).then(() => {
        return Promise.join(
          this.Individual.create({ name: 'Foo Bar' }),
          this.Hat.create({ name: 'Baz' }));
      }).then(([individual, hat]) => {
        return individual.setPersonwearinghat(hat);
      }).then(() => {
        return this.Individual.findOne({
          where: { name: 'Foo Bar' },
          include: [{ model: this.Hat, as: 'personwearinghat' }]
        });
      }).then(individual => {
        expect(individual.name).to.equal('Foo Bar');
        expect(individual.personwearinghat.name).to.equal('Baz');
      }).then(() => {
        return this.Individual.findOne({
          where: { name: 'Foo Bar' },
          include: [{
            model: this.Hat,
            as: { singular: 'personwearinghat' }
          }]
        });
      }).then(individual => {
        expect(individual.name).to.equal('Foo Bar');
        expect(individual.personwearinghat.name).to.equal('Baz');
      });
    });

    it('should load all', function() {
      return this.sequelize.sync({ force: true }).then(() => {
        return Promise.join(
          this.Individual.create({ name: 'Foo Bar' }),
          this.Hat.create({ name: 'Baz' }));
      }).then(([individual, hat]) => {
        return individual.setPersonwearinghat(hat);
      }).then(() => {
        return this.Individual.findOne({
          where: { name: 'Foo Bar' },
          include: [{ all: true }]
        });
      }).then(individual => {
        expect(individual.name).to.equal('Foo Bar');
        expect(individual.personwearinghat.name).to.equal('Baz');
      });
    });
  });
});


return `CREATE FUNCTION ${functionName}(${paramList}) RETURNS ${returnType} AS $func$ BEGIN ${body} END; $func$ language '${language}'${expandedOptions};`;

return `${statement} ${functionName}(${paramList}) RETURNS ${returnType} AS $func$ BEGIN ${body} END; $func$ language '${language}'${expandedOptionsArray};`;

'use strict';

const stores = new Map();

module.exports = dialect => {
  if (!stores.has(dialect)) {
    stores.set(dialect, new Map());
  }

  return {
    clear() {
      stores.get(dialect).clear();
    },
    refresh(dataType) {
      for (const type of dataType.types[dialect]) {
        stores.get(dialect).set(type, dataType.parse);
      }
    },
    get(type) {
      return stores.get(dialect).get(type);
    }
  };
};


keep keep keep replace replace replace replace replace replace replace keep keep keep keep

* @param {boolean} options.force If force is true, any existing functions with the same parameters will be replaced. For postgres, this means using `CREATE OR REPLACE FUNCTION` instead of `CREATE FUNCTION`. Default is false

'use strict';

const _ = require('lodash');

const Utils = require('./utils');
const DataTypes = require('./data-types');
const SQLiteQueryInterface = require('./dialects/sqlite/query-interface');
const MSSQLQueryInterface = require('./dialects/mssql/query-interface');
const MySQLQueryInterface = require('./dialects/mysql/query-interface');
const PostgresQueryInterface = require('./dialects/postgres/query-interface');
const Transaction = require('./transaction');
const Promise = require('./promise');
const QueryTypes = require('./query-types');
const Op = require('./operators');

/**
 * The interface that Sequelize uses to talk to all databases
 *
 * @class QueryInterface
 */
class QueryInterface {
  constructor(sequelize) {
    this.sequelize = sequelize;
    this.QueryGenerator = this.sequelize.dialect.QueryGenerator;
  }

  /**
   * Create a database
   *
   * @param {string} database  Database name to create
   * @param {Object} [options] Query options
   * @param {string} [options.charset] Database default character set, MYSQL only
   * @param {string} [options.collate] Database default collation
   * @param {string} [options.encoding] Database default character set, PostgreSQL only
   * @param {string} [options.ctype] Database character classification, PostgreSQL only
   * @param {string} [options.template] The name of the template from which to create the new database, PostgreSQL only
   *
   * @returns {Promise}
   */
  createDatabase(database, options) {
    options = options || {};
    const sql = this.QueryGenerator.createDatabaseQuery(database, options);
    return this.sequelize.query(sql, options);
  }

  /**
   * Drop a database
   *
   * @param {string} database  Database name to drop
   * @param {Object} [options] Query options
   *
   * @returns {Promise}
   */
  dropDatabase(database, options) {
    options = options || {};
    const sql = this.QueryGenerator.dropDatabaseQuery(database);
    return this.sequelize.query(sql, options);
  }

  /**
   * Create a schema
   *
   * @param {string} schema    Schema name to create
   * @param {Object} [options] Query options
   *
   * @returns {Promise}
   */
  createSchema(schema, options) {
    options = options || {};
    const sql = this.QueryGenerator.createSchema(schema);
    return this.sequelize.query(sql, options);
  }

  /**
   * Drop a schema
   *
   * @param {string} schema    Schema name to drop
   * @param {Object} [options] Query options
   *
   * @returns {Promise}
   */
  dropSchema(schema, options) {
    options = options || {};
    const sql = this.QueryGenerator.dropSchema(schema);
    return this.sequelize.query(sql, options);
  }

  /**
   * Drop all schemas
   *
   * @param {Object} [options] Query options
   *
   * @returns {Promise}
   */
  dropAllSchemas(options) {
    options = options || {};

    if (!this.QueryGenerator._dialect.supports.schemas) {
      return this.sequelize.drop(options);
    }
    return this.showAllSchemas(options).map(schemaName => this.dropSchema(schemaName, options));
  }

  /**
   * Show all schemas
   *
   * @param {Object} [options] Query options
   *
   * @returns {Promise<Array>}
   */
  showAllSchemas(options) {
    options = Object.assign({}, options, {
      raw: true,
      type: this.sequelize.QueryTypes.SELECT
    });

    const showSchemasSql = this.QueryGenerator.showSchemasQuery(options);

    return this.sequelize.query(showSchemasSql, options).then(schemaNames => _.flatten(
      schemaNames.map(value => value.schema_name ? value.schema_name : value)
    ));
  }

  /**
   * Return database version
   *
   * @param {Object}    [options]      Query options
   * @param {QueryType} [options.type] Query type
   *
   * @returns {Promise}
   * @private
   */
  databaseVersion(options) {
    return this.sequelize.query(
      this.QueryGenerator.versionQuery(),
      Object.assign({}, options, { type: QueryTypes.VERSION })
    );
  }

  /**
   * Create a table with given set of attributes
   *
   * ```js
   * queryInterface.createTable(
   *   'nameOfTheNewTable',
   *   {
   *     id: {
   *       type: Sequelize.INTEGER,
   *       primaryKey: true,
   *       autoIncrement: true
   *     },
   *     createdAt: {
   *       type: Sequelize.DATE
   *     },
   *     updatedAt: {
   *       type: Sequelize.DATE
   *     },
   *     attr1: Sequelize.STRING,
   *     attr2: Sequelize.INTEGER,
   *     attr3: {
   *       type: Sequelize.BOOLEAN,
   *       defaultValue: false,
   *       allowNull: false
   *     },
   *     //foreign key usage
   *     attr4: {
   *       type: Sequelize.INTEGER,
   *       references: {
   *         model: 'another_table_name',
   *         key: 'id'
   *       },
   *       onUpdate: 'cascade',
   *       onDelete: 'cascade'
   *     }
   *   },
   *   {
   *     engine: 'MYISAM',    // default: 'InnoDB'
   *     charset: 'latin1',   // default: null
   *     schema: 'public',    // default: public, PostgreSQL only.
   *     comment: 'my table', // comment for table
   *     collate: 'latin1_danish_ci' // collation, MYSQL only
   *   }
   * )
   * ```
   *
   * @param {string} tableName  Name of table to create
   * @param {Object} attributes Object representing a list of table attributes to create
   * @param {Object} [options] create table and query options
   * @param {Model}  [model] model class
   *
   * @returns {Promise}
   */
  createTable(tableName, attributes, options, model) {
    let sql = '';
    let promise;

    options = _.clone(options) || {};

    if (options && options.uniqueKeys) {
      _.forOwn(options.uniqueKeys, uniqueKey => {
        if (uniqueKey.customIndex === undefined) {
          uniqueKey.customIndex = true;
        }
      });
    }

    if (model) {
      options.uniqueKeys = options.uniqueKeys || model.uniqueKeys;
    }

    attributes = _.mapValues(
      attributes,
      attribute => this.sequelize.normalizeAttribute(attribute)
    );

    // Postgres requires special SQL commands for ENUM/ENUM[]
    if (this.sequelize.options.dialect === 'postgres') {
      promise = PostgresQueryInterface.ensureEnums(this, tableName, attributes, options, model);
    } else {
      promise = Promise.resolve();
    }

    if (
      !tableName.schema &&
      (options.schema || !!model && model._schema)
    ) {
      tableName = this.QueryGenerator.addSchema({
        tableName,
        _schema: !!model && model._schema || options.schema
      });
    }

    attributes = this.QueryGenerator.attributesToSQL(attributes, { table: tableName, context: 'createTable' });
    sql = this.QueryGenerator.createTableQuery(tableName, attributes, options);

    return promise.then(() => this.sequelize.query(sql, options));
  }

  /**
   * Drop a table from database
   *
   * @param {string} tableName Table name to drop
   * @param {Object} options   Query options
   *
   * @returns {Promise}
   */
  dropTable(tableName, options) {
    // if we're forcing we should be cascading unless explicitly stated otherwise
    options = _.clone(options) || {};
    options.cascade = options.cascade || options.force || false;

    let sql = this.QueryGenerator.dropTableQuery(tableName, options);

    return this.sequelize.query(sql, options).then(() => {
      const promises = [];

      // Since postgres has a special case for enums, we should drop the related
      // enum type within the table and attribute
      if (this.sequelize.options.dialect === 'postgres') {
        const instanceTable = this.sequelize.modelManager.getModel(tableName, { attribute: 'tableName' });

        if (instanceTable) {
          const getTableName = (!options || !options.schema || options.schema === 'public' ? '' : `${options.schema}_`) + tableName;

          const keys = Object.keys(instanceTable.rawAttributes);
          const keyLen = keys.length;

          for (let i = 0; i < keyLen; i++) {
            if (instanceTable.rawAttributes[keys[i]].type instanceof DataTypes.ENUM) {
              sql = this.QueryGenerator.pgEnumDrop(getTableName, keys[i]);
              options.supportsSearchPath = false;
              promises.push(this.sequelize.query(sql, Object.assign({}, options, { raw: true })));
            }
          }
        }
      }

      return Promise.all(promises).get(0);
    });
  }

  /**
   * Drop all tables from database
   *
   * @param {Object} [options] query options
   * @param {Array}  [options.skip] List of table to skip
   *
   * @returns {Promise}
   */
  dropAllTables(options) {
    options = options || {};
    const skip = options.skip || [];

    const dropAllTables = tableNames => Promise.each(tableNames, tableName => {
      // if tableName is not in the Array of tables names then don't drop it
      if (!skip.includes(tableName.tableName || tableName)) {
        return this.dropTable(tableName, Object.assign({}, options, { cascade: true }) );
      }
    });

    return this.showAllTables(options).then(tableNames => {
      if (this.sequelize.options.dialect === 'sqlite') {
        return this.sequelize.query('PRAGMA foreign_keys;', options).then(result => {
          const foreignKeysAreEnabled = result.foreign_keys === 1;

          if (foreignKeysAreEnabled) {
            return this.sequelize.query('PRAGMA foreign_keys = OFF', options)
              .then(() => dropAllTables(tableNames))
              .then(() => this.sequelize.query('PRAGMA foreign_keys = ON', options));
          }
          return dropAllTables(tableNames);
        });
      }
      return this.getForeignKeysForTables(tableNames, options).then(foreignKeys => {
        const queries = [];

        tableNames.forEach(tableName => {
          let normalizedTableName = tableName;
          if (_.isObject(tableName)) {
            normalizedTableName = `${tableName.schema}.${tableName.tableName}`;
          }

          foreignKeys[normalizedTableName].forEach(foreignKey => {
            queries.push(this.QueryGenerator.dropForeignKeyQuery(tableName, foreignKey));
          });
        });

        return Promise.each(queries, q => this.sequelize.query(q, options))
          .then(() => dropAllTables(tableNames));
      });
    });
  }

  /**
   * Drop specified enum from database (Postgres only)
   *
   * @param {string} [enumName]  Enum name to drop
   * @param {Object} options Query options
   *
   * @returns {Promise}
   * @private
   */
  dropEnum(enumName, options) {
    if (this.sequelize.getDialect() !== 'postgres') {
      return Promise.resolve();
    }

    options = options || {};

    return this.sequelize.query(
      this.QueryGenerator.pgEnumDrop(null, null, this.QueryGenerator.pgEscapeAndQuote(enumName)),
      Object.assign({}, options, { raw: true })
    );
  }

  /**
   * Drop all enums from database (Postgres only)
   *
   * @param {Object} options Query options
   *
   * @returns {Promise}
   * @private
   */
  dropAllEnums(options) {
    if (this.sequelize.getDialect() !== 'postgres') {
      return Promise.resolve();
    }

    options = options || {};

    return this.pgListEnums(null, options).map(result => this.sequelize.query(
      this.QueryGenerator.pgEnumDrop(null, null, this.QueryGenerator.pgEscapeAndQuote(result.enum_name)),
      Object.assign({}, options, { raw: true })
    ));
  }

  /**
   * List all enums (Postgres only)
   *
   * @param {string} [tableName]  Table whose enum to list
   * @param {Object} [options]    Query options
   *
   * @returns {Promise}
   * @private
   */
  pgListEnums(tableName, options) {
    options = options || {};
    const sql = this.QueryGenerator.pgListEnums(tableName);
    return this.sequelize.query(sql, Object.assign({}, options, { plain: false, raw: true, type: QueryTypes.SELECT }));
  }

  /**
   * Rename a table
   *
   * @param {string} before    Current name of table
   * @param {string} after     New name from table
   * @param {Object} [options] Query options
   *
   * @returns {Promise}
   */
  renameTable(before, after, options) {
    options = options || {};
    const sql = this.QueryGenerator.renameTableQuery(before, after);
    return this.sequelize.query(sql, options);
  }

  /**
   * Get all tables in current database
   *
   * @param {Object}    [options] Query options
   * @param {boolean}   [options.raw=true] Run query in raw mode
   * @param {QueryType} [options.type=QueryType.SHOWTABLE] query type
   *
   * @returns {Promise<Array>}
   * @private
   */
  showAllTables(options) {
    options = Object.assign({}, options, {
      raw: true,
      type: QueryTypes.SHOWTABLES
    });

    const showTablesSql = this.QueryGenerator.showTablesQuery();
    return this.sequelize.query(showTablesSql, options).then(tableNames => _.flatten(tableNames));
  }

  /**
   * Describe a table structure
   *
   * This method returns an array of hashes containing information about all attributes in the table.
   *
   * ```js
   * {
   *    name: {
   *      type:         'VARCHAR(255)', // this will be 'CHARACTER VARYING' for pg!
   *      allowNull:    true,
   *      defaultValue: null
   *    },
   *    isBetaMember: {
   *      type:         'TINYINT(1)', // this will be 'BOOLEAN' for pg!
   *      allowNull:    false,
   *      defaultValue: false
   *    }
   * }
   * ```
   *
   * @param {string} tableName table name
   * @param {Object} [options] Query options
   *
   * @returns {Promise<Object>}
   */
  describeTable(tableName, options) {
    let schema = null;
    let schemaDelimiter = null;

    if (typeof options === 'string') {
      schema = options;
    } else if (typeof options === 'object' && options !== null) {
      schema = options.schema || null;
      schemaDelimiter = options.schemaDelimiter || null;
    }

    if (typeof tableName === 'object' && tableName !== null) {
      schema = tableName.schema;
      tableName = tableName.tableName;
    }

    const sql = this.QueryGenerator.describeTableQuery(tableName, schema, schemaDelimiter);
    options = Object.assign({}, options, { type: QueryTypes.DESCRIBE });

    return this.sequelize.query(sql, options).then(data => {
      /*
       * If no data is returned from the query, then the table name may be wrong.
       * Query generators that use information_schema for retrieving table info will just return an empty result set,
       * it will not throw an error like built-ins do (e.g. DESCRIBE on MySql).
       */
      if (_.isEmpty(data)) {
        throw new Error(`No description found for "${tableName}" table. Check the table name and schema; remember, they _are_ case sensitive.`);
      }

      return data;
    }).catch(e => {
      if (e.original && e.original.code === 'ER_NO_SUCH_TABLE') {
        throw Error(`No description found for "${tableName}" table. Check the table name and schema; remember, they _are_ case sensitive.`);
      }

      throw e;
    });
  }

  /**
   * Add a new column to a table
   *
   * ```js
   * queryInterface.addColumn('tableA', 'columnC', Sequelize.STRING, {
   *    after: 'columnB' // after option is only supported by MySQL
   * });
   * ```
   *
   * @param {string} table     Table to add column to
   * @param {string} key       Column name
   * @param {Object} attribute Attribute definition
   * @param {Object} [options] Query options
   *
   * @returns {Promise}
   */
  addColumn(table, key, attribute, options) {
    if (!table || !key || !attribute) {
      throw new Error('addColumn takes at least 3 arguments (table, attribute name, attribute definition)');
    }

    options = options || {};
    attribute = this.sequelize.normalizeAttribute(attribute);
    return this.sequelize.query(this.QueryGenerator.addColumnQuery(table, key, attribute), options);
  }

  /**
   * Remove a column from a table
   *
   * @param {string} tableName      Table to remove column from
   * @param {string} attributeName  Columns name to remove
   * @param {Object} [options]      Query options
   *
   * @returns {Promise}
   */
  removeColumn(tableName, attributeName, options) {
    options = options || {};
    switch (this.sequelize.options.dialect) {
      case 'sqlite':
        // sqlite needs some special treatment as it cannot drop a column
        return SQLiteQueryInterface.removeColumn(this, tableName, attributeName, options);
      case 'mssql':
        // mssql needs special treatment as it cannot drop a column with a default or foreign key constraint
        return MSSQLQueryInterface.removeColumn(this, tableName, attributeName, options);
      case 'mysql':
      case 'mariadb':
        // mysql/mariadb need special treatment as it cannot drop a column with a foreign key constraint
        return MySQLQueryInterface.removeColumn(this, tableName, attributeName, options);
      default:
        return this.sequelize.query(this.QueryGenerator.removeColumnQuery(tableName, attributeName), options);
    }
  }

  /**
   * Change a column definition
   *
   * @param {string} tableName          Table name to change from
   * @param {string} attributeName      Column name
   * @param {Object} dataTypeOrOptions  Attribute definition for new column
   * @param {Object} [options]          Query options
   *
   * @returns {Promise}
   */
  changeColumn(tableName, attributeName, dataTypeOrOptions, options) {
    const attributes = {};
    options = options || {};

    if (_.values(DataTypes).includes(dataTypeOrOptions)) {
      attributes[attributeName] = { type: dataTypeOrOptions, allowNull: true };
    } else {
      attributes[attributeName] = dataTypeOrOptions;
    }

    attributes[attributeName] = this.sequelize.normalizeAttribute(attributes[attributeName]);

    if (this.sequelize.options.dialect === 'sqlite') {
      // sqlite needs some special treatment as it cannot change a column
      return SQLiteQueryInterface.changeColumn(this, tableName, attributes, options);
    }
    const query = this.QueryGenerator.attributesToSQL(attributes, {
      context: 'changeColumn',
      table: tableName
    });
    const sql = this.QueryGenerator.changeColumnQuery(tableName, query);

    return this.sequelize.query(sql, options);
  }

  /**
   * Rename a column
   *
   * @param {string} tableName        Table name whose column to rename
   * @param {string} attrNameBefore   Current column name
   * @param {string} attrNameAfter    New column name
   * @param {Object} [options]        Query option
   *
   * @returns {Promise}
   */
  renameColumn(tableName, attrNameBefore, attrNameAfter, options) {
    options = options || {};
    return this.describeTable(tableName, options).then(data => {
      if (!data[attrNameBefore]) {
        throw new Error(`Table ${tableName} doesn't have the column ${attrNameBefore}`);
      }

      data = data[attrNameBefore] || {};

      const _options = {};

      _options[attrNameAfter] = {
        attribute: attrNameAfter,
        type: data.type,
        allowNull: data.allowNull,
        defaultValue: data.defaultValue
      };

      // fix: a not-null column cannot have null as default value
      if (data.defaultValue === null && !data.allowNull) {
        delete _options[attrNameAfter].defaultValue;
      }

      if (this.sequelize.options.dialect === 'sqlite') {
        // sqlite needs some special treatment as it cannot rename a column
        return SQLiteQueryInterface.renameColumn(this, tableName, attrNameBefore, attrNameAfter, options);
      }
      const sql = this.QueryGenerator.renameColumnQuery(
        tableName,
        attrNameBefore,
        this.QueryGenerator.attributesToSQL(_options)
      );
      return this.sequelize.query(sql, options);
    });
  }

  /**
   * Add an index to a column
   *
   * @param {string|Object}  tableName Table name to add index on, can be a object with schema
   * @param {Array}   [attributes]     Use options.fields instead, List of attributes to add index on
   * @param {Object}  options          indexes options
   * @param {Array}   options.fields   List of attributes to add index on
   * @param {boolean} [options.concurrently] Pass CONCURRENT so other operations run while the index is created
   * @param {boolean} [options.unique] Create a unique index
   * @param {string}  [options.using]  Useful for GIN indexes
   * @param {string}  [options.operator] Index operator
   * @param {string}  [options.type]   Type of index, available options are UNIQUE|FULLTEXT|SPATIAL
   * @param {string}  [options.name]   Name of the index. Default is <table>_<attr1>_<attr2>
   * @param {Object}  [options.where]  Where condition on index, for partial indexes
   * @param {string}  [rawTablename]   table name, this is just for backward compatibiity
   *
   * @returns {Promise}
   */
  addIndex(tableName, attributes, options, rawTablename) {
    // Support for passing tableName, attributes, options or tableName, options (with a fields param which is the attributes)
    if (!Array.isArray(attributes)) {
      rawTablename = options;
      options = attributes;
      attributes = options.fields;
    }

    if (!rawTablename) {
      // Map for backwards compat
      rawTablename = tableName;
    }

    options = Utils.cloneDeep(options);
    options.fields = attributes;
    const sql = this.QueryGenerator.addIndexQuery(tableName, options, rawTablename);
    return this.sequelize.query(sql, Object.assign({}, options, { supportsSearchPath: false }));
  }

  /**
   * Show indexes on a table
   *
   * @param {string} tableName table name
   * @param {Object} [options]   Query options
   *
   * @returns {Promise<Array>}
   * @private
   */
  showIndex(tableName, options) {
    const sql = this.QueryGenerator.showIndexesQuery(tableName, options);
    return this.sequelize.query(sql, Object.assign({}, options, { type: QueryTypes.SHOWINDEXES }));
  }

  getForeignKeysForTables(tableNames, options) {
    if (tableNames.length === 0) {
      return Promise.resolve({});
    }

    options = Object.assign({}, options || {}, { type: QueryTypes.FOREIGNKEYS });

    return Promise.map(tableNames, tableName =>
      this.sequelize.query(this.QueryGenerator.getForeignKeysQuery(tableName, this.sequelize.config.database), options)
    ).then(results => {
      const result = {};

      tableNames.forEach((tableName, i) => {
        if (_.isObject(tableName)) {
          tableName = `${tableName.schema}.${tableName.tableName}`;
        }

        result[tableName] = Array.isArray(results[i])
          ? results[i].map(r => r.constraint_name)
          : [results[i] && results[i].constraint_name];

        result[tableName] = result[tableName].filter(_.identity);
      });

      return result;
    });
  }

  /**
   * Get foreign key references details for the table
   *
   * Those details contains constraintSchema, constraintName, constraintCatalog
   * tableCatalog, tableSchema, tableName, columnName,
   * referencedTableCatalog, referencedTableCatalog, referencedTableSchema, referencedTableName, referencedColumnName.
   * Remind: constraint informations won't return if it's sqlite.
   *
   * @param {string} tableName table name
   * @param {Object} [options]  Query options
   *
   * @returns {Promise}
   */
  getForeignKeyReferencesForTable(tableName, options) {
    const queryOptions = Object.assign({}, options, {
      type: QueryTypes.FOREIGNKEYS
    });
    const catalogName = this.sequelize.config.database;
    switch (this.sequelize.options.dialect) {
      case 'sqlite':
        // sqlite needs some special treatment.
        return SQLiteQueryInterface.getForeignKeyReferencesForTable(this, tableName, queryOptions);
      case 'postgres':
      {
        // postgres needs some special treatment as those field names returned are all lowercase
        // in order to keep same result with other dialects.
        const query = this.QueryGenerator.getForeignKeyReferencesQuery(tableName, catalogName);
        return this.sequelize.query(query, queryOptions)
          .then(result => result.map(Utils.camelizeObjectKeys));
      }
      case 'mssql':
      case 'mysql':
      case 'mariadb':
      default: {
        const query = this.QueryGenerator.getForeignKeysQuery(tableName, catalogName);
        return this.sequelize.query(query, queryOptions);
      }
    }
  }

  /**
   * Remove an already existing index from a table
   *
   * @param {string} tableName             Table name to drop index from
   * @param {string} indexNameOrAttributes Index name
   * @param {Object} [options]             Query options
   *
   * @returns {Promise}
   */
  removeIndex(tableName, indexNameOrAttributes, options) {
    options = options || {};
    const sql = this.QueryGenerator.removeIndexQuery(tableName, indexNameOrAttributes);
    return this.sequelize.query(sql, options);
  }

  /**
   * Add a constraint to a table
   *
   * Available constraints:
   * - UNIQUE
   * - DEFAULT (MSSQL only)
   * - CHECK (MySQL - Ignored by the database engine )
   * - FOREIGN KEY
   * - PRIMARY KEY
   *
   * @example <caption>UNIQUE</caption>
   * queryInterface.addConstraint('Users', ['email'], {
   *   type: 'unique',
   *   name: 'custom_unique_constraint_name'
   * });
   *
   * @example <caption>CHECK</caption>
   * queryInterface.addConstraint('Users', ['roles'], {
   *   type: 'check',
   *   where: {
   *      roles: ['user', 'admin', 'moderator', 'guest']
   *   }
   * });
   *
   * @example <caption>Default - MSSQL only</caption>
   * queryInterface.addConstraint('Users', ['roles'], {
   *    type: 'default',
   *    defaultValue: 'guest'
   * });
   *
   * @example <caption>Primary Key</caption>
   * queryInterface.addConstraint('Users', ['username'], {
   *    type: 'primary key',
   *    name: 'custom_primary_constraint_name'
   * });
   *
   * @example <caption>Foreign Key</caption>
   * queryInterface.addConstraint('Posts', ['username'], {
   *   type: 'foreign key',
   *   name: 'custom_fkey_constraint_name',
   *   references: { //Required field
   *     table: 'target_table_name',
   *     field: 'target_column_name'
   *   },
   *   onDelete: 'cascade',
   *   onUpdate: 'cascade'
   * });
   *
   * @param {string} tableName                  Table name where you want to add a constraint
   * @param {Array}  attributes                 Array of column names to apply the constraint over
   * @param {Object} options                    An object to define the constraint name, type etc
   * @param {string} options.type               Type of constraint. One of the values in available constraints(case insensitive)
   * @param {string} [options.name]             Name of the constraint. If not specified, sequelize automatically creates a named constraint based on constraint type, table & column names
   * @param {string} [options.defaultValue]     The value for the default constraint
   * @param {Object} [options.where]            Where clause/expression for the CHECK constraint
   * @param {Object} [options.references]       Object specifying target table, column name to create foreign key constraint
   * @param {string} [options.references.table] Target table name
   * @param {string} [options.references.field] Target column name
   * @param {string} [rawTablename]             Table name, for backward compatibility
   *
   * @returns {Promise}
   */
  addConstraint(tableName, attributes, options, rawTablename) {
    if (!Array.isArray(attributes)) {
      rawTablename = options;
      options = attributes;
      attributes = options.fields;
    }

    if (!options.type) {
      throw new Error('Constraint type must be specified through options.type');
    }

    if (!rawTablename) {
      // Map for backwards compat
      rawTablename = tableName;
    }

    options = Utils.cloneDeep(options);
    options.fields = attributes;

    if (this.sequelize.dialect.name === 'sqlite') {
      return SQLiteQueryInterface.addConstraint(this, tableName, options, rawTablename);
    }
    const sql = this.QueryGenerator.addConstraintQuery(tableName, options, rawTablename);
    return this.sequelize.query(sql, options);
  }

  showConstraint(tableName, constraintName, options) {
    const sql = this.QueryGenerator.showConstraintsQuery(tableName, constraintName);
    return this.sequelize.query(sql, Object.assign({}, options, { type: QueryTypes.SHOWCONSTRAINTS }));
  }

  /**
   * Remove a constraint from a table
   *
   * @param {string} tableName       Table name to drop constraint from
   * @param {string} constraintName  Constraint name
   * @param {Object} options         Query options
   *
   * @returns {Promise}
   */
  removeConstraint(tableName, constraintName, options) {
    options = options || {};

    switch (this.sequelize.options.dialect) {
      case 'mysql':
      case 'mariadb':
        //does not support DROP CONSTRAINT. Instead DROP PRIMARY, FOREIGN KEY, INDEX should be used
        return MySQLQueryInterface.removeConstraint(this, tableName, constraintName, options);
      case 'sqlite':
        return SQLiteQueryInterface.removeConstraint(this, tableName, constraintName, options);
      default:
        const sql = this.QueryGenerator.removeConstraintQuery(tableName, constraintName);
        return this.sequelize.query(sql, options);
    }
  }

  insert(instance, tableName, values, options) {
    options = Utils.cloneDeep(options);
    options.hasTrigger = instance && instance.constructor.options.hasTrigger;
    const sql = this.QueryGenerator.insertQuery(tableName, values, instance && instance.constructor.rawAttributes, options);

    options.type = QueryTypes.INSERT;
    options.instance = instance;

    return this.sequelize.query(sql, options).then(results => {
      if (instance) results[0].isNewRecord = false;
      return results;
    });
  }

  /**
   * Upsert
   *
   * @param {string} tableName    table to upsert on
   * @param {Object} insertValues values to be inserted, mapped to field name
   * @param {Object} updateValues values to be updated, mapped to field name
   * @param {Object} where        various conditions
   * @param {Model}  model        Model to upsert on
   * @param {Object} options      query options
   *
   * @returns {Promise<boolean,?number>} Resolves an array with <created, primaryKey>
   */
  upsert(tableName, insertValues, updateValues, where, model, options) {
    const wheres = [];
    const attributes = Object.keys(insertValues);
    let indexes = [];
    let indexFields;

    options = _.clone(options);

    if (!Utils.isWhereEmpty(where)) {
      wheres.push(where);
    }

    // Lets combine unique keys and indexes into one
    indexes = _.map(model.uniqueKeys, value => {
      return value.fields;
    });

    model._indexes.forEach(value => {
      if (value.unique) {
        // fields in the index may both the strings or objects with an attribute property - lets sanitize that
        indexFields = value.fields.map(field => {
          if (_.isPlainObject(field)) {
            return field.attribute;
          }
          return field;
        });
        indexes.push(indexFields);
      }
    });

    for (const index of indexes) {
      if (_.intersection(attributes, index).length === index.length) {
        where = {};
        for (const field of index) {
          where[field] = insertValues[field];
        }
        wheres.push(where);
      }
    }

    where = { [Op.or]: wheres };

    options.type = QueryTypes.UPSERT;
    options.raw = true;

    const sql = this.QueryGenerator.upsertQuery(tableName, insertValues, updateValues, where, model, options);
    return this.sequelize.query(sql, options).then(result => {
      switch (this.sequelize.options.dialect) {
        case 'postgres':
          return [result.created, result.primary_key];

        case 'mssql':
          return [
            result.$action === 'INSERT',
            result[model.primaryKeyField]
          ];

        // MySQL returns 1 for inserted, 2 for updated
        // http://dev.mysql.com/doc/refman/5.0/en/insert-on-duplicate.html.
        case 'mysql':
        case 'mariadb':
          return [result === 1, undefined];

        default:
          return [result, undefined];
      }
    });
  }

  /**
   * Insert multiple records into a table
   *
   * @example
   * queryInterface.bulkInsert('roles', [{
   *    label: 'user',
   *    createdAt: new Date(),
   *    updatedAt: new Date()
   *  }, {
   *    label: 'admin',
   *    createdAt: new Date(),
   *    updatedAt: new Date()
   *  }]);
   *
   * @param {string} tableName   Table name to insert record to
   * @param {Array}  records     List of records to insert
   * @param {Object} options     Various options, please see Model.bulkCreate options
   * @param {Object} attributes  Various attributes mapped by field name
   *
   * @returns {Promise}
   */
  bulkInsert(tableName, records, options, attributes) {
    options = _.clone(options) || {};
    options.type = QueryTypes.INSERT;

    return this.sequelize.query(
      this.QueryGenerator.bulkInsertQuery(tableName, records, options, attributes),
      options
    ).then(results => results[0]);
  }

  update(instance, tableName, values, identifier, options) {
    options = _.clone(options || {});
    options.hasTrigger = !!(instance && instance._modelOptions && instance._modelOptions.hasTrigger);

    const sql = this.QueryGenerator.updateQuery(tableName, values, identifier, options, instance.constructor.rawAttributes);

    options.type = QueryTypes.UPDATE;

    options.instance = instance;
    return this.sequelize.query(sql, options);
  }

  /**
   * Update multiple records of a table
   *
   * @example
   * queryInterface.bulkUpdate('roles', {
   *     label: 'admin',
   *   }, {
   *     userType: 3,
   *   },
   * );
   *
   * @param {string} tableName     Table name to update
   * @param {Object} values        Values to be inserted, mapped to field name
   * @param {Object} identifier    A hash with conditions OR an ID as integer OR a string with conditions
   * @param {Object} [options]     Various options, please see Model.bulkCreate options
   * @param {Object} [attributes]  Attributes on return objects if supported by SQL dialect
   *
   * @returns {Promise}
   */
  bulkUpdate(tableName, values, identifier, options, attributes) {
    options = Utils.cloneDeep(options);
    if (typeof identifier === 'object') identifier = Utils.cloneDeep(identifier);

    const sql = this.QueryGenerator.updateQuery(tableName, values, identifier, options, attributes);
    const table = _.isObject(tableName) ? tableName : { tableName };
    const model = _.find(this.sequelize.modelManager.models, { tableName: table.tableName });

    options.model = model;
    return this.sequelize.query(sql, options);
  }

  delete(instance, tableName, identifier, options) {
    const cascades = [];
    const sql = this.QueryGenerator.deleteQuery(tableName, identifier, {}, instance.constructor);

    options = _.clone(options) || {};

    // Check for a restrict field
    if (!!instance.constructor && !!instance.constructor.associations) {
      const keys = Object.keys(instance.constructor.associations);
      const length = keys.length;
      let association;

      for (let i = 0; i < length; i++) {
        association = instance.constructor.associations[keys[i]];
        if (association.options && association.options.onDelete &&
          association.options.onDelete.toLowerCase() === 'cascade' &&
          association.options.useHooks === true) {
          cascades.push(association.accessors.get);
        }
      }
    }

    return Promise.each(cascades, cascade => {
      return instance[cascade](options).then(instances => {
        // Check for hasOne relationship with non-existing associate ("has zero")
        if (!instances) {
          return Promise.resolve();
        }

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

        return Promise.each(instances, instance => instance.destroy(options));
      });
    }).then(() => {
      options.instance = instance;
      return this.sequelize.query(sql, options);
    });
  }

  /**
   * Delete multiple records from a table
   *
   * @param {string}  tableName            table name from where to delete records
   * @param {Object}  where                where conditions to find records to delete
   * @param {Object}  [options]            options
   * @param {boolean} [options.truncate]  Use truncate table command
   * @param {Model}   [model]             Model
   *
   * @returns {Promise}
   */
  bulkDelete(tableName, where, options, model) {
    options = Utils.cloneDeep(options);
    options = _.defaults(options, { limit: null });

    if (options.truncate === true) {
      return this.sequelize.query(
        this.QueryGenerator.truncateTableQuery(tableName, options),
        options
      );
    }

    if (typeof identifier === 'object') where = Utils.cloneDeep(where);

    return this.sequelize.query(
      this.QueryGenerator.deleteQuery(tableName, where, options, model),
      options
    );
  }

  select(model, tableName, options) {
    options = Utils.cloneDeep(options);
    options.type = QueryTypes.SELECT;
    options.model = model;

    return this.sequelize.query(
      this.QueryGenerator.selectQuery(tableName, options, model),
      options
    );
  }

  increment(model, tableName, values, identifier, options) {
    options = Utils.cloneDeep(options);

    const sql = this.QueryGenerator.arithmeticQuery('+', tableName, values, identifier, options, options.attributes);

    options.type = QueryTypes.UPDATE;
    options.model = model;

    return this.sequelize.query(sql, options);
  }

  decrement(model, tableName, values, identifier, options) {
    options = Utils.cloneDeep(options);

    const sql = this.QueryGenerator.arithmeticQuery('-', tableName, values, identifier, options, options.attributes);

    options.type = QueryTypes.UPDATE;
    options.model = model;

    return this.sequelize.query(sql, options);
  }

  rawSelect(tableName, options, attributeSelector, Model) {
    options = Utils.cloneDeep(options);
    options = _.defaults(options, {
      raw: true,
      plain: true,
      type: QueryTypes.SELECT
    });

    const sql = this.QueryGenerator.selectQuery(tableName, options, Model);

    if (attributeSelector === undefined) {
      throw new Error('Please pass an attribute selector!');
    }

    return this.sequelize.query(sql, options).then(data => {
      if (!options.plain) {
        return data;
      }

      const result = data ? data[attributeSelector] : null;

      if (!options || !options.dataType) {
        return result;
      }

      const dataType = options.dataType;

      if (dataType instanceof DataTypes.DECIMAL || dataType instanceof DataTypes.FLOAT) {
        if (result !== null) {
          return parseFloat(result);
        }
      }
      if (dataType instanceof DataTypes.INTEGER || dataType instanceof DataTypes.BIGINT) {
        return parseInt(result, 10);
      }
      if (dataType instanceof DataTypes.DATE) {
        if (result !== null && !(result instanceof Date)) {
          return new Date(result);
        }
      }
      return result;
    });
  }

  createTrigger(tableName, triggerName, timingType, fireOnArray, functionName, functionParams, optionsArray, options) {
    const sql = this.QueryGenerator.createTrigger(tableName, triggerName, timingType, fireOnArray, functionName, functionParams, optionsArray);
    options = options || {};
    if (sql) {
      return this.sequelize.query(sql, options);
    }
    return Promise.resolve();
  }

  dropTrigger(tableName, triggerName, options) {
    const sql = this.QueryGenerator.dropTrigger(tableName, triggerName);
    options = options || {};

    if (sql) {
      return this.sequelize.query(sql, options);
    }
    return Promise.resolve();
  }

  renameTrigger(tableName, oldTriggerName, newTriggerName, options) {
    const sql = this.QueryGenerator.renameTrigger(tableName, oldTriggerName, newTriggerName);
    options = options || {};

    if (sql) {
      return this.sequelize.query(sql, options);
    }
    return Promise.resolve();
  }

  /**
   * Create an SQL function
   *
   * @example
   * queryInterface.createFunction(
   *   'someFunction',
   *   [
   *     {type: 'integer', name: 'param', direction: 'IN'}
   *   ],
   *   'integer',
   *   'plpgsql',
   *   'RETURN param + 1;',
   *   [
   *     'IMMUTABLE',
   *     'LEAKPROOF'
   *   ]
   * );
   *
   * @param {string} functionName Name of SQL function to create
   * @param {Array}  params       List of parameters declared for SQL function
   * @param {string} returnType   SQL type of function returned value
   * @param {string} language     The name of the language that the function is implemented in
   * @param {string} body         Source code of function
   * @param {Array}  optionsArray Extra-options for creation
   * @param {Object} [options]    query options
   *
   * @returns {Promise}
   */
  createFunction(functionName, params, returnType, language, body, optionsArray, options) {
    const sql = this.QueryGenerator.createFunction(functionName, params, returnType, language, body, optionsArray);
    options = options || {};

    if (sql) {
      return this.sequelize.query(sql, options);
    }
    return Promise.resolve();
  }

  /**
   * Drop an SQL function
   *
   * @example
   * queryInterface.dropFunction(
   *   'someFunction',
   *   [
   *     {type: 'varchar', name: 'param1', direction: 'IN'},
   *     {type: 'integer', name: 'param2', direction: 'INOUT'}
   *   ]
   * );
   *
   * @param {string} functionName Name of SQL function to drop
   * @param {Array}  params       List of parameters declared for SQL function
   * @param {Object} [options]    query options
   *
   * @returns {Promise}
   */
  dropFunction(functionName, params, options) {
    const sql = this.QueryGenerator.dropFunction(functionName, params);
    options = options || {};

    if (sql) {
      return this.sequelize.query(sql, options);
    }
    return Promise.resolve();
  }

  /**
   * Rename an SQL function
   *
   * @example
   * queryInterface.renameFunction(
   *   'fooFunction',
   *   [
   *     {type: 'varchar', name: 'param1', direction: 'IN'},
   *     {type: 'integer', name: 'param2', direction: 'INOUT'}
   *   ],
   *   'barFunction'
   * );
   *
   * @param {string} oldFunctionName  Current name of function
   * @param {Array}  params           List of parameters declared for SQL function
   * @param {string} newFunctionName  New name of function
   * @param {Object} [options]        query options
   *
   * @returns {Promise}
   */
  renameFunction(oldFunctionName, params, newFunctionName, options) {
    const sql = this.QueryGenerator.renameFunction(oldFunctionName, params, newFunctionName);
    options = options || {};

    if (sql) {
      return this.sequelize.query(sql, options);
    }
    return Promise.resolve();
  }

  // Helper methods useful for querying

  /**
   * Escape an identifier (e.g. a table or attribute name)
   *
   * @param {string} identifier identifier to quote
   * @param {boolean} [force]   If force is true,the identifier will be quoted even if the `quoteIdentifiers` option is false.
   *
   * @private
   */
  quoteIdentifier(identifier, force) {
    return this.QueryGenerator.quoteIdentifier(identifier, force);
  }

  quoteTable(identifier) {
    return this.QueryGenerator.quoteTable(identifier);
  }

  /**
   * Quote array of identifiers at once
   *
   * @param {string[]} identifiers array of identifiers to quote
   * @param {boolean} [force]   If force is true,the identifier will be quoted even if the `quoteIdentifiers` option is false.
   *
   * @private
   */
  quoteIdentifiers(identifiers, force) {
    return this.QueryGenerator.quoteIdentifiers(identifiers, force);
  }

  /**
   * Escape a value (e.g. a string, number or date)
   *
   * @param {string} value string to escape
   *
   * @private
   */
  escape(value) {
    return this.QueryGenerator.escape(value);
  }

  setIsolationLevel(transaction, value, options) {
    if (!transaction || !(transaction instanceof Transaction)) {
      throw new Error('Unable to set isolation level for a transaction without transaction object!');
    }

    if (transaction.parent || !value) {
      // Not possible to set a separate isolation level for savepoints
      return Promise.resolve();
    }

    options = Object.assign({}, options, {
      transaction: transaction.parent || transaction
    });

    const sql = this.QueryGenerator.setIsolationLevelQuery(value, {
      parent: transaction.parent
    });

    if (!sql) return Promise.resolve();

    return this.sequelize.query(sql, options);
  }

  startTransaction(transaction, options) {
    if (!transaction || !(transaction instanceof Transaction)) {
      throw new Error('Unable to start a transaction without transaction object!');
    }

    options = Object.assign({}, options, {
      transaction: transaction.parent || transaction
    });
    options.transaction.name = transaction.parent ? transaction.name : undefined;
    const sql = this.QueryGenerator.startTransactionQuery(transaction);

    return this.sequelize.query(sql, options);
  }

  deferConstraints(transaction, options) {
    options = Object.assign({}, options, {
      transaction: transaction.parent || transaction
    });

    const sql = this.QueryGenerator.deferConstraintsQuery(options);

    if (sql) {
      return this.sequelize.query(sql, options);
    }

    return Promise.resolve();
  }

  commitTransaction(transaction, options) {
    if (!transaction || !(transaction instanceof Transaction)) {
      throw new Error('Unable to commit a transaction without transaction object!');
    }
    if (transaction.parent) {
      // Savepoints cannot be committed
      return Promise.resolve();
    }

    options = Object.assign({}, options, {
      transaction: transaction.parent || transaction,
      supportsSearchPath: false,
      completesTransaction: true
    });

    const sql = this.QueryGenerator.commitTransactionQuery(transaction);
    const promise = this.sequelize.query(sql, options);

    transaction.finished = 'commit';

    return promise;
  }

  rollbackTransaction(transaction, options) {
    if (!transaction || !(transaction instanceof Transaction)) {
      throw new Error('Unable to rollback a transaction without transaction object!');
    }

    options = Object.assign({}, options, {
      transaction: transaction.parent || transaction,
      supportsSearchPath: false,
      completesTransaction: true
    });
    options.transaction.name = transaction.parent ? transaction.name : undefined;
    const sql = this.QueryGenerator.rollbackTransactionQuery(transaction);
    const promise = this.sequelize.query(sql, options);

    transaction.finished = 'rollback';

    return promise;
  }
}

module.exports = QueryInterface;
module.exports.QueryInterface = QueryInterface;
module.exports.default = QueryInterface;


import { ValidationOptions } from './instance-validator';
import Model, {
  BulkCreateOptions,
  CountOptions,
  CreateOptions,
  DestroyOptions,
  FindOptions,
  InstanceDestroyOptions,
  InstanceUpdateOptions,
  ModelAttributes,
  ModelOptions,
  UpdateOptions,
} from './model';
import { Config, Options, Sequelize, SyncOptions } from './sequelize';

export type HookReturn = Promise<void> | void;

/**
 * Options for Model.init. We mostly duplicate the Hooks here, since there is no way to combine the two
 * interfaces.
 */
export interface ModelHooks<M extends Model = Model> {
  beforeValidate(instance: M, options: ValidationOptions): HookReturn;
  afterValidate(instance: M, options: ValidationOptions): HookReturn;
  beforeCreate(attributes: M, options: CreateOptions): HookReturn;
  afterCreate(attributes: M, options: CreateOptions): HookReturn;
  beforeDestroy(instance: M, options: InstanceDestroyOptions): HookReturn;
  afterDestroy(instance: M, options: InstanceDestroyOptions): HookReturn;
  beforeUpdate(instance: M, options: InstanceUpdateOptions): HookReturn;
  afterUpdate(instance: M, options: InstanceUpdateOptions): HookReturn;
  beforeSave(instance: M, options: InstanceUpdateOptions | CreateOptions): HookReturn;
  afterSave(instance: M, options: InstanceUpdateOptions | CreateOptions): HookReturn;
  beforeBulkCreate(instances: M[], options: BulkCreateOptions): HookReturn;
  afterBulkCreate(instances: M[], options: BulkCreateOptions): HookReturn;
  beforeBulkDestroy(options: DestroyOptions): HookReturn;
  afterBulkDestroy(options: DestroyOptions): HookReturn;
  beforeBulkUpdate(options: UpdateOptions): HookReturn;
  afterBulkUpdate(options: UpdateOptions): HookReturn;
  beforeFind(options: FindOptions): HookReturn;
  beforeCount(options: CountOptions): HookReturn;
  beforeFindAfterExpandIncludeAll(options: FindOptions): HookReturn;
  beforeFindAfterOptions(options: FindOptions): HookReturn;
  afterFind(instancesOrInstance: M[] | M | null, options: FindOptions): HookReturn;
  beforeSync(options: SyncOptions): HookReturn;
  afterSync(options: SyncOptions): HookReturn;
  beforeBulkSync(options: SyncOptions): HookReturn;
  afterBulkSync(options: SyncOptions): HookReturn;
}

export interface SequelizeHooks extends ModelHooks {
  beforeDefine(attributes: ModelAttributes, options: ModelOptions<Model>): void;
  afterDefine(model: typeof Model): void;
  beforeInit(config: Config, options: Options): void;
  afterInit(sequelize: Sequelize): void;
  beforeConnect(config: Config): HookReturn;
  afterConnect(connection: unknown, config: Config): HookReturn;
  beforeDisconnect(connection: unknown): HookReturn;
  afterDisconnect(connection: unknown): HookReturn;
}

/**
 * Virtual class for deduplication
 */
export class Hooks {
  /**
   * Add a hook to the model
   *
   * @param name Provide a name for the hook function. It can be used to remove the hook later or to order
   *   hooks based on some sort of priority system in the future.
   */
  public static addHook<C extends typeof Hooks, K extends keyof SequelizeHooks>(
    hookType: K,
    name: string,
    fn: SequelizeHooks[K]
  ): C;
  public static addHook<C extends typeof Hooks, K extends keyof SequelizeHooks>(
    hookType: K,
    fn: SequelizeHooks[K]
  ): C;

  /**
   * Remove hook from the model
   */
  public static removeHook<C extends typeof Hooks, K extends keyof SequelizeHooks>(hookType: K, name: string): C;

  /**
   * Check whether the mode has any hooks of this type
   */
  public static hasHook<K extends keyof SequelizeHooks>(hookType: K): boolean;
  public static hasHooks<K extends keyof SequelizeHooks>(hookType: K): boolean;

  /**
   * Add a hook to the model
   *
   * @param name Provide a name for the hook function. It can be used to remove the hook later or to order
   *   hooks based on some sort of priority system in the future.
   */
  public addHook<K extends keyof SequelizeHooks>(hookType: K, name: string, fn: SequelizeHooks[K]): this;
  public addHook<K extends keyof SequelizeHooks>(hookType: K, fn: SequelizeHooks[K]): this;
  /**
   * Remove hook from the model
   */
  public removeHook<K extends keyof SequelizeHooks>(hookType: K, name: string): this;

  /**
   * Check whether the mode has any hooks of this type
   */
  public hasHook<K extends keyof SequelizeHooks>(hookType: K): boolean;
  public hasHooks<K extends keyof SequelizeHooks>(hookType: K): boolean;
}