Source: src/jsMultiSelect.js

/*globals ObjectRow,Environment,sqlFun,objectRow,Context,MsgParser, jsDataQuery */

'use strict';
/**
 * provides a mechanism to make multiple select with a single sql command
 * @module optimizeComparing
 */
const util = require('util');
const _ = require('lodash');

const $dq = require('./../client/components/metadata/jsDataQuery');


/**
 * Used to compose query
 * @class MultiCompare
 */


/**
 * Multi compare is a class indicating the comparison of n given fields with n given values
 * @method MultiCompare
 * @param {String[]} fields
 * @param {Object[]} values
 * @constructor
 */
function MultiCompare(fields, values) {
  this.fields = fields;
  this.values = values;
}

MultiCompare.prototype = {
  constructor: MultiCompare,
  /**
   * List of fields to compare
   * @public
   * @property fields
   * @type String[]
   **/
  fields:null,

  /**
   * List of values to match
   * @public
   * @property values
   * {Object[]}
   **/
  values:null,

  /**
   * checks if this has same comparison fields of another multi compare
   * @method sameFieldsAs
   * @param {MultiCompare} multiComp
   * @returns {boolean}
   */
  sameFieldsAs : function(multiComp) {
    return _.isEqual(this.fields, multiComp.fields);
  }
};





/**
 * Optimized multi compare. It is a multi-field-comparator that eventually has multiple values for some field.
 * @class OptimizedMultiCompare
 */



/**
 * creates an OptimizedMultiCompare starting from a MultiCompare
 * @param {MultiCompare} multiComp
 * @constructor
 */
function OptimizedMultiCompare(multiComp) {
  this.fields= multiComp.fields;
  this.multiValPosition = null;
  this.multiValArray = null;
  this.values = _.clone(multiComp.values);
}


OptimizedMultiCompare.prototype = {
  constructor: OptimizedMultiCompare,

  /**
   * @public
   * @property fields
   * @type String[]
   */
  fields: null,

  /**
   * @public
   * @property  multiValPosition
   * {int|null}
   */
  multiValPosition: null,

  /**
   * @public
   * @property values
   * {Object[]|null}
   */
  values: null,

  /**
   * @public
   * @property  multiValArray
   * {Object[]|null}
   */
  multiValArray: null,

  /**
   * checks if this is a simple comparator or multi-value comparator
   * @public
   * @method isMultiValue
   * @returns {boolean}
   */
  isMultiValue: function () {
    return this.multiValPosition !== null;
  },

  /**
   * Gets the overall filter for this multi select
   * @method getFilter
   * @public
   * @returns {sqlFun}
   */
  getFilter: function () {
    if (!this.isMultiValue) {
      return $dq.mcmp(this.fields, this.values);
    }
    const that = this;
    return $dq.and(_.map(this.fields, function (el, index) {
      if (index === that.multiValPosition) {
        return $dq.isIn(el, that.multiValArray);
      }
      return $dq.eq(el, that.values[index]);
    }));
  },

  /**
   * @method sameFieldsAs
   * @param {OptimizedMultiCompare} optimizedComparer
   * @returns {boolean}
   */
  sameFieldsAs: function (optimizedComparer) {
    return _.isEqual(this.fields, optimizedComparer.fields);
  },


  /**
   * check if this comparison has a specified value for the index-th field
   * @method hasValue
   * @param {object} value
   * @param {int} index
   * @returns {boolean}
   */
  hasValue: function (value, index) {
    if (index !== this.multiValPosition) {
      return this.values[index] === value;
    }
    return _.includes(this.multiValArray, value);
  },



  /***
   * Join this multicomparator with another one, if it is possible. Returns false if it is not possible.
   * @public
   * @method joinWith
   * @param  {OptimizedMultiCompare} other
   * @return {boolean}
   */
  joinWith: function (other) {
    let posDiff = null,
        len = this.fields.length,
        i;
    if (!this.sameFieldsAs(other)) {
      return false;
    }
    if (other.isMultiValue()) {
      return false;
    }
    if (this.multiValPosition === null) {
      //Checks there is 0 or 1 differences
      for (i = 0; i < len; i++) {
        if (!this.hasValue(other.values[i], i)) {
          if (posDiff !== null) {
            return false; //more than one difference was found
          }
          posDiff = i;
        }
      }
    } else {
      //there is already a multi value, so there must be at most a difference and it must be in  multiValPosition
      for (i = 0; i < len; i++) {
        if (!this.hasValue(other.values[i], i)) {
          if (i !== this.multiValPosition) {
            return false; //a difference was found not in desired position
          }
          posDiff = i;
        }
      }
    }
    if (posDiff === null) {
      return true;
    }
    if (posDiff === this.multiValPosition) {
      this.multiValArray.push(other.values[posDiff]);
      return true;
    }
    this.multiValPosition = posDiff;
    this.multiValArray = [this.values[posDiff], other.values[posDiff]];
    return true;
  }
};










/**
 * A class representing a single sql select command
 * @class Select
 */

/**
 * Creates a select providing an optional column list
 * @param {string} columnList
 * @constructor
 */
function Select(columnList) {
  /**
   * string containing the list  of all columns to read, usually comma separated
   * @public
   * @property {String} Select.columns
   */
  this.columns = columnList || '*';



  
  this.omc = null;

  this.isOptimized = false;



  this.staticF = null;



  this.filter = null;



  this.alias = null;
}



Select.prototype = {
  constructor: Select,
  /**
   * @public
   * @property omc
   * {OptimizedMultiCompare|null}
   */
  omc: null,

  /**
   * @public
   * @property  alias
   * {string|null}
   */
  alias: null,

  /**
   * @public
   * @property  filter
   * {sqlFun|null}
   */
  filter: null,


  /**
   * @public
   * @property staticF
   * {sqlFun|null}
   */
  staticF: null,

  /**
   * States if a Select is 'optimized', i.e. it is attached to a multicomparator. A select attached to a manual
   *  filter is considered not-optimized
   * @property  isOptimized
   * {boolean}
   */
  isOptimized: false,


  /**
   * Table to which this select is applied
   * @property tableName
   * {string|null}
   */
  tableName: null,

  /**
   * @property myTop
   * {string|null}
   * @protected
   */
  myTop: null,

  /**
   * get the partial filter (excluding static filter) associated with this Select
   * @method getPartialFilter
   * @return {?sqlFun}
   */
  getPartialFilter: function () {
    if (this.filter) {
      return this.filter;
    }

    if (this.omc) {
      return this.omc.getFilter();
    }
    return null;
  },

  /**
   * Gets the overall filter for this multi select
   * @method getFilter
   * @returns {sqlFun}
   */
  getFilter: function () {
    if (this.staticF) {
      return $dq.and(this.staticF, this.getPartialFilter());
    }
    return this.getPartialFilter();
  },


  getTop: function(){
    return this.myTop;
  },

  /**
   * Sets the table associated to this select
   * @method from
   * @param {string} tableName
   * @returns {Select}
   */
  from: function (tableName) {
    this.tableName = tableName;
    if (this.alias === null) {
      this.alias = tableName;
    }
    return this;
  },


  /**
   * sets the top options for the query
   * @method top
   * @param {string} [n]
   * @returns {string|Select}
   */
  top: function (n) {
    if (n !== undefined) {
      this.myTop = n;
      return this;
    }
    return this;
  },


  /**
   * Check if this Select can be appended to another one, i.e., has same tableName and alias
   * @method canAppendTo
   * @param {Select} other
   * @returns {boolean}
   */
  canAppendTo: function (other) {
    return this.tableName === other.tableName && this.alias === other.alias;
  },


  /**
   * Tries to append this Select to another one in an optimized way and returns true on success
   * An optimized Append is possible only if two select are both optimized
   * @method optimizedAppendTo
   * @param {Select} other
   * @returns {boolean}
   */
  optimizedAppendTo: function (other) {
    if (!this.canAppendTo(other)) {
      return false;
    }

    if (this.omc === null || other.omc === null) {
      return false;
    }
    if (!this.omc.joinWith(other.omc)) {
      return false;
    }
    this.filter = null;
    return true;
  },


  /**
   * appends this Select to another one or-joining their conditions, returns true if appending succeeded
   * @method appendTo
   * @param {Select} other
   * @returns {boolean}
   */
  appendTo: function (other) {
    if (!this.canAppendTo(other)) {
      return false;
    }

    if (this.getPartialFilter().isTrue) {
      return true;
    }
    if (other.getPartialFilter().isTrue) {
      this.omc = null;
      this.isOptimized = false;
      this.filter = other.getPartialFilter();
      return true;
    }

    if (this.getPartialFilter().toString() === other.getPartialFilter().toString()) {
      return true;
    }

    this.filter = $dq.or(this.getPartialFilter(), other.getPartialFilter());
    this.omc = null;
    this.isOptimized = false;
    return true;
  }


};






/**
 * sets the manual filter for this Select. We call this kind of filtering  not-optimized
 * @method where
 * @param {sqlFun} filter
 * @returns {Select} this
 */
Select.prototype.where = function(filter){
  this.filter = filter;
  this.isOptimized = false;
  return this;
};

/**
 * Sets a static filter for this condition
 * @param {sqlFun} filter
 * @returns {Select} this
 */
Select.prototype.staticFilter = function (filter) {
  this.staticF = filter;
  return this;
};


/**
 * Sets the filter as a multi comparator. Here we call it 'optimized'
 * @method multiCompare
 * @param {MultiCompare} multiComp
 * @returns {Select}
 */
Select.prototype.multiCompare = function (multiComp) {
  this.omc = new OptimizedMultiCompare(multiComp);
  this.isOptimized = true; /* alias for this.omc !== null */
  return this;
};



/**
 * Sets a destination table for this select (alias)
 * @method intoTable
 * @param {string} alias
 * @returns {Select}
 */
Select.prototype.intoTable = function(alias){
  this.alias = alias;
  return this;
};

/**
 * set the sorting for the select
 * @method orderBy
 * @param sorting
 * @returns {Select}
 */
Select.prototype.orderBy = function (sorting) {
  this.sorting = sorting;
  return this;
};




/**
 * Tries to group the selectList into res using the specified joinMethod.
 * @method groupSelect
 * @private
 * @param {Select[]} selectList
 * @param {string} joinMethod method to use to try the join. It is  'appendTo' or 'optimizedAppendTo'
 * @return {Select[]} array of joined Select
 */
function groupSelectStep(selectList,  joinMethod){
  var result = [];
  _.forEach(selectList,
      function (select) {
      if (!_.find(result,function(g){return g[joinMethod](select);})){
        result.push(select);
      }
    });
  return result;
}

/**
 * Takes a list of Select to same table and evaluates an equivalent Select joining all input filters
 * @method groupSelect
 * @param {Select[]} selectList
 */
function groupSelect(selectList){
  //try to group optimized Select each other with optimizedAppendTo
  var grouped = groupSelectStep(_.filter(selectList, {isOptimized:true}), 'optimizedAppendTo');

  //then group all the rest using appendTo
  return groupSelectStep(grouped.concat(_.filter(selectList, {isOptimized: false})), 'appendTo');
}


module.exports = {
  Select: Select,
  groupSelect: groupSelect,
  groupSelectStep: groupSelectStep, //exported only for unit testing
  MultiCompare: MultiCompare,
  OptimizedMultiCompare: OptimizedMultiCompare //exported only for unit testing
};