query.js 5.73 KB
'use strict';

var Utils = require('../../utils')
  , AbstractQuery = require('../abstract/query')
  , uuid = require('uuid')
  , sequelizeErrors = require('../../errors.js')
  , _ = require('lodash');

var Query = function(connection, sequelize, options) {
  this.connection = connection;
  this.instance = options.instance;
  this.model = options.model;
  this.sequelize = sequelize;
  this.uuid = uuid.v4();
  this.options = Utils._.extend({
    logging: console.log,
    plain: false,
    raw: false
  }, options || {});

  this.checkLoggingOption();
};

Utils.inherit(Query, AbstractQuery);
Query.formatBindParameters = AbstractQuery.formatBindParameters;
Query.prototype.run = function(sql, parameters) {
  var self = this;
  this.sql = sql;

  //do we need benchmark for this query execution
  var benchmark = this.sequelize.options.benchmark || this.options.benchmark;

  if (benchmark) {
    var queryBegin = Date.now();
  } else {
    this.sequelize.log('Executing (' + (this.connection.uuid || 'default') + '): ' + this.sql, this.options);
  }

  var promise = new Utils.Promise(function(resolve, reject) {
    self.connection.query(self.sql, function(err, results) {

      if (benchmark) {
        self.sequelize.log('Executed (' + (self.connection.uuid || 'default') + '): ' + self.sql, (Date.now() - queryBegin), self.options);
      }

      if (err) {
        err.sql = sql;

        reject(self.formatError(err));
      } else {
        resolve(self.formatResults(results));
      }
    }).setMaxListeners(100);
  });

  return promise;
};

  /**
 * High level function that handles the results of a query execution.
 *
 *
 * Example:
 *  query.formatResults([
 *    {
 *      id: 1,              // this is from the main table
 *      attr2: 'snafu',     // this is from the main table
 *      Tasks.id: 1,        // this is from the associated table
 *      Tasks.title: 'task' // this is from the associated table
 *    }
 *  ])
 *
 * @param {Array} data - The result of the query execution.
 */
Query.prototype.formatResults = function(data) {
  var result = this.instance;

  if (this.isInsertQuery(data)) {
    this.handleInsertQuery(data);

    if (!this.instance) {
      result = data[this.getInsertIdField()];
    }
  }

  if (this.isSelectQuery()) {
    result = this.handleSelectQuery(data);
  } else if (this.isShowTablesQuery()) {
    result = this.handleShowTablesQuery(data);
  } else if (this.isDescribeQuery()) {
    result = {};

    data.forEach(function(_result) {
      var enumRegex = /^enum/i;
      result[_result.Field] = {
        type: enumRegex.test(_result.Type) ? _result.Type.replace(enumRegex, 'ENUM') : _result.Type.toUpperCase(),
        allowNull: (_result.Null === 'YES'),
        defaultValue: _result.Default,
        primaryKey: _result.Key === 'PRI'
      };
    });
  } else if (this.isShowIndexesQuery()) {
    result = this.handleShowIndexesQuery(data);

  } else if (this.isCallQuery()) {
    result = data[0];
  } else if (this.isBulkUpdateQuery() || this.isBulkDeleteQuery() || this.isUpsertQuery()) {
    result = data.affectedRows;
  } else if (this.isVersionQuery()) {
    result = data[0].version;
  } else if (this.isForeignKeysQuery()) {
    result = data;
  } else if (this.isRawQuery()) {
    // MySQL returns row data and metadata (affected rows etc) in a single object - let's standarize it, sorta
    result = [data, data];
  }

  return result;
};


Query.prototype.formatError = function (err) {
  var match;

  switch (err.errno || err.code) {
    case 1062:
      match = err.message.match(/Duplicate entry '(.*)' for key '?((.|\s)*?)'?$/);

      var values = match ? match[1].split('-') : undefined
        , fields = {}
        , message = 'Validation error'
        , uniqueKey = this.model && this.model.uniqueKeys[match[2]];

      if (!!uniqueKey) {
        if (!!uniqueKey.msg) message = uniqueKey.msg;
        fields = Utils._.zipObject(uniqueKey.fields, values);
      } else {
        fields[match[2]] = match[1];
      }

      var errors = [];
      var self = this;
      Utils._.forOwn(fields, function(value, field) {
        errors.push(new sequelizeErrors.ValidationErrorItem(
          self.getUniqueConstraintErrorMessage(field),
          'unique violation', field, value));
      });

      return new sequelizeErrors.UniqueConstraintError({
        message: message,
        errors: errors,
        parent: err,
        fields: fields
      });

    case 1451:
      match = err.message.match(/FOREIGN KEY \(`(.*)`\) REFERENCES `(.*)` \(`(.*)`\)(?: ON .*)?\)$/);

      return new sequelizeErrors.ForeignKeyConstraintError({
        fields: null,
        index: match ? match[3] : undefined,
        parent: err
      });

    case 1452:
      match = err.message.match(/FOREIGN KEY \(`(.*)`\) REFERENCES `(.*)` \(`(.*)`\)(.*)\)$/);

      return new sequelizeErrors.ForeignKeyConstraintError({
        fields: null,
        index: match ? match[1] : undefined,
        parent: err
      });

    default:
      return new sequelizeErrors.DatabaseError(err);
  }
};

Query.prototype.handleShowIndexesQuery = function (data) {
  // Group by index name, and collect all fields
  data = _.reduce(data, function (acc, item) {
    if (!(item.Key_name in acc)) {
      acc[item.Key_name] = item;
      item.fields = [];
    }

    acc[item.Key_name].fields[item.Seq_in_index - 1] = {
      attribute: item.Column_name,
      length: item.Sub_part || undefined,
      order: item.Collation === 'A' ? 'ASC' : undefined
    };
    delete item.column_name;

    return acc;
  }, {});

  return Utils._.map(data, function(item) {
    return {
      primary: item.Key_name === 'PRIMARY',
      fields: item.fields,
      name: item.Key_name,
      tableName: item.Table,
      unique: (item.Non_unique !== 1),
      type: item.Index_type,
    };
  });
};

module.exports = Query;