text: 'use strict';
  
  const sequelizeErrors = require('../../errors');
  const { QueryTypes } = require('../../query-types');
  const { QueryInterface } = require('../abstract/query-interface');
  const { cloneDeep } = require('../../utils');
  const _ = require('lodash');
  
  /**
   * The interface that Sequelize uses to talk with SQLite database
   */
  class SQLiteQueryInterface extends QueryInterface {
    /**
     * A wrapper that fixes SQLite's inability to remove columns from existing tables.
     * It will create a backup of the table, drop the table afterwards and create a
     * new table with the same name but without the obsolete column.
     *
     * @override
     */
    async removeColumn(tableName, attributeName, options) {
      options = options || {};
  
      const fields = await this.describeTable(tableName, options);
      delete fields[attributeName];
  
      const sql = this.queryGenerator.removeColumnQuery(tableName, fields);
      const subQueries = sql.split(';').filter(q => q !== '');
  
      for (const subQuery of subQueries) {
        await this.sequelize.query(`${subQuery};`, { raw: true, ...options });
      }
    }
  
    /**
     * A wrapper that fixes SQLite's inability to change columns from existing tables.
     * It will create a backup of the table, drop the table afterwards and create a
     * new table with the same name but with a modified version of the respective column.
     *
     * @override
     */
    async changeColumn(tableName, attributeName, dataTypeOrOptions, options) {
      options = options || {};
  
      const fields = await this.describeTable(tableName, options);
      Object.assign(fields[attributeName], this.normalizeAttribute(dataTypeOrOptions));
  
      const sql = this.queryGenerator.removeColumnQuery(tableName, fields);
      const subQueries = sql.split(';').filter(q => q !== '');
  
      for (const subQuery of subQueries) {
        await this.sequelize.query(`${subQuery};`, { raw: true, ...options });
      }
    }
  
    /**
     * A wrapper that fixes SQLite's inability to rename columns from existing tables.
     * It will create a backup of the table, drop the table afterwards and create a
     * new table with the same name but with a renamed version of the respective column.
     *
     * @override
     */
    async renameColumn(tableName, attrNameBefore, attrNameAfter, options) {
      options = options || {};
      const fields = await this.assertTableHasColumn(tableName, attrNameBefore, options);
  
      fields[attrNameAfter] = { ...fields[attrNameBefore] };
      delete fields[attrNameBefore];
  
      const sql = this.queryGenerator.renameColumnQuery(tableName, attrNameBefore, attrNameAfter, fields);
      const subQueries = sql.split(';').filter(q => q !== '');
  
      for (const subQuery of subQueries) {
        await this.sequelize.query(`${subQuery};`, { raw: true, ...options });
      }
    }
  
    /**
     * @override
     */
    async removeConstraint(tableName, constraintName, options) {
      let createTableSql;
  
      const constraints = await this.showConstraint(tableName, constraintName);
      // sqlite can't show only one constraint, so we find here the one to remove
      const constraint = constraints.find(constaint => constaint.constraintName === constraintName);
  
      if (!constraint) {
        throw new sequelizeErrors.UnknownConstraintError({
          message: `Constraint ${constraintName} on table ${tableName} does not exist`,
          constraint: constraintName,
          table: tableName,
        });
      }
  
      createTableSql = constraint.sql;
      constraint.constraintName = this.queryGenerator.quoteIdentifier(constraint.constraintName);
      let constraintSnippet = `, CONSTRAINT ${constraint.constraintName} ${constraint.constraintType} ${constraint.constraintCondition}`;
  
      if (constraint.constraintType === 'FOREIGN KEY') {
        const referenceTableName = this.queryGenerator.quoteTable(constraint.referenceTableName);
        constraint.referenceTableKeys = constraint.referenceTableKeys.map(columnName => this.queryGenerator.quoteIdentifier(columnName));
        const referenceTableKeys = constraint.referenceTableKeys.join(', ');
        constraintSnippet += ` REFERENCES ${referenceTableName} (${referenceTableKeys})`;
        constraintSnippet += ` ON UPDATE ${constraint.updateAction}`;
        constraintSnippet += ` ON DELETE ${constraint.deleteAction}`;
      }
  
      createTableSql = createTableSql.replace(constraintSnippet, '');
      createTableSql += ';';
  
      const fields = await this.describeTable(tableName, options);
  
      const sql = this.queryGenerator._alterConstraintQuery(tableName, fields, createTableSql);
      const subQueries = sql.split(';').filter(q => q !== '');
  
      for (const subQuery of subQueries) {
        await this.sequelize.query(`${subQuery};`, { raw: true, ...options });
      }
    }
  
    /**
     * @override
     */
    async addConstraint(tableName, options) {
      if (!options.fields) {
        throw new Error('Fields must be specified through options.fields');
      }
  
      if (!options.type) {
        throw new Error('Constraint type must be specified through options.type');
      }
  
      options = cloneDeep(options);
  
      const constraintSnippet = this.queryGenerator.getConstraintSnippet(tableName, options);
      const describeCreateTableSql = this.queryGenerator.describeCreateTableQuery(tableName);
  
      const constraints = await this.sequelize.query(describeCreateTableSql, { ...options, type: QueryTypes.SELECT, raw: true });
      let sql = constraints[0].sql;
      const index = sql.length - 1;
      // Replace ending ')' with constraint snippet - Simulates String.replaceAt
      // http://stackoverflow.com/questions/1431094
      const createTableSql = `${sql.slice(0, Math.max(0, index))}, ${constraintSnippet})${sql.slice(index + 1)};`;
  
      const fields = await this.describeTable(tableName, options);
      sql = this.queryGenerator._alterConstraintQuery(tableName, fields, createTableSql);
      const subQueries = sql.split(';').filter(q => q !== '');
  
      for (const subQuery of subQueries) {
        await this.sequelize.query(`${subQuery};`, { raw: true, ...options });
      }
    }
  
    /**
     * @override
     */
    async getForeignKeyReferencesForTable(tableName, options) {
      const database = this.sequelize.config.database;
      const query = this.queryGenerator.getForeignKeysQuery(tableName, database);
      const result = await this.sequelize.query(query, options);
  
      return result.map(row => ({
        tableName,
        columnName: row.from,
        referencedTableName: row.table,
        referencedColumnName: row.to,
        tableCatalog: database,
        referencedTableCatalog: database,
      }));
    }
  
    /**
     * @override
     */
    async dropAllTables(options) {
      options = options || {};
      const skip = options.skip || [];
  
      const tableNames = await this.showAllTables(options);
      await this.sequelize.query('PRAGMA foreign_keys = OFF', options);
      await this._dropAllTables(tableNames, skip, options);
      await this.sequelize.query('PRAGMA foreign_keys = ON', options);
    }
  
    /**
     * @override
     */
    async 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 = { ...options, type: QueryTypes.DESCRIBE };
      const sqlIndexes = this.queryGenerator.showIndexesQuery(tableName);
  
      try {
        const data = await this.sequelize.query(sql, options);
        /*
         * 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.`);
        }
  
        const indexes = await this.sequelize.query(sqlIndexes, options);
        for (const prop in data) {
          data[prop].unique = false;
        }
  
        for (const index of indexes) {
          for (const field of index.fields) {
            if (index.unique !== undefined) {
              data[field.attribute].unique = index.unique;
            }
          }
        }
  
        const foreignKeys = await this.getForeignKeyReferencesForTable(tableName, options);
        for (const foreignKey of foreignKeys) {
          data[foreignKey.columnName].references = {
            model: foreignKey.referencedTableName,
            key: foreignKey.referencedColumnName,
          };
        }
  
        return data;
      } catch (error) {
        if (error.original && error.original.code === 'ER_NO_SUCH_TABLE') {
          throw new Error(`No description found for "${tableName}" table. Check the table name and schema; remember, they _are_ case sensitive.`);
        }
  
        throw error;
      }
    }
  }
  
  exports.SQLiteQueryInterface = SQLiteQueryInterface;
  