Source: src/jsMySqlFormatter.js

/*global Environment,global,define,sqlFun */
/*jslint nomen: true*/
const _ = require('lodash');


/**
 * Class to be used with jsDataQuery in order to format expression for MS SQL SERVER database
 */

/**
 * provides formatting facilities for Microsoft Sql Server query creation
 * @module sqlFormatter
 */

/**
 * provides formatting facilities for Microsoft Sql Server query creation
 * @class sqlFormatter
 */
var $sqlf = {};


/**
 * Check if obj is null or undefined
 * @private
 * @method isNullOrUndefined
 * @param obj
 * @returns {boolean}
 */
function isNullOrUndefined(obj) {
    return _.isUndefined(obj) || _.isNull(obj);
}

/**
 * Check if obj is not a real condition, giving true if it is null, undefined or empty string
 * @method isEmptyCondition
 * @param {string|sqlFun} cond
 * @returns {boolean}
 */
function isEmptyCondition(cond) {
    return _.isUndefined(cond) || _.isNull(cond) || cond === '';
}


function leftPad(s, n, filler) {
    var o = s.toString();
    while (o.length < n) {
        o = filler + o;
    }
    return o;
}

/**
 * Gives the sql string representation of an object
 * @method quote
 * @public
 * @param v {object} literal constant
 * @param [noSurroundQuotes] if true strings are not surrounded with quotes
 * @returns {string}
 */
function quote(v, noSurroundQuotes) {
    //should differentiate basing on v type (string / number / date /boolean)
    if (_.isString(v)) {
        if (noSurroundQuotes) {
            return v.replace(/'/g, "''");
        }
        return "'" + v.replace(/'/g, "''") + "'";
    }
    if (_.isNumber(v)) {
        return v.toString();
    }
    if (_.isBoolean(v)) {
        return v.toString();
    }
    if (_.isDate(v)) {
        if (v.getHours() === 0 && v.getMinutes() === 0 && v.getSeconds() === 0 && v.getMilliseconds() === 0) {
            return '{d \'' +
                leftPad(v.getFullYear(), 4, '0') +'-'+
                leftPad(v.getMonth() + 1, 2, '0') +'-'+ //javascripts counts months starting from 0!!!
                leftPad(v.getDate(), 2, '0') + '\'}';
        }
        return '{ts \'' +
            leftPad(v.getFullYear(), 4, '0') + '-' +
            leftPad(v.getMonth() + 1, 2, '0') + '-' + //javascripts counts months starting from 0!!!
            leftPad(v.getDate(), 2, '0') + ' ' +
            leftPad(v.getHours(), 2, '0') + ':' +
            leftPad(v.getMinutes(), 2, '0') + ':' +
            leftPad(v.getSeconds(), 2, '0') + '.' +
            leftPad(v.getMilliseconds(), 3, '0') + '\'}';
    }
    if (isNullOrUndefined(v)) {
        return 'null';
    }
    return v.toString();
}

/**
 * Converts a function into a sql expression. The result is meant to be used as conditional expression in
 *  sql WHERE clauses. Usually you will not use this function, but instead you will use the toSql method of
 *  dataquery objects. This can be used to manage slightly more generic objects like null values,
 *  undefined, arrays. Arrays are converted into lists.
 * @method toSql
 * @public
 * @param {sqlFun|Array|object|null|undefined} v  function to be converted
 * @param {Environment} context  context into which the expression has to be evaluated
 * @return {string}
 * @example eq('a',1) is converted into 'a=1'
 *  eq('a','1') is converted into 'a=\'1\'' i.e. strings are quoted when evaluated
 *  [1,2,3] is converted into (1,2,3)
 */
function toSql(v, context) {
    if (isNullOrUndefined(v)) {
        return 'null';
    }
    if (v.toSql) {
        return v.toSql($sqlf, context);
    }
    if (_.isArray(v)) {
        return '(' +
            _.map(v, function (el) {
                return toSql(el, context);
            }).join(',') + ')';
    }
    return quote(v);
}

/**
 * Get the string filter from a sqlFunction
 * @method conditionToSql
 * @public
 * @param {sqlFun|string|null|undefined} cond
 * @param {Environment} context
 * @return {string}
 */
function conditionToSql(cond, context) {
    if (isEmptyCondition(cond)) {
        return null;
    }
    if (cond.toSql) {
        return cond.toSql($sqlf, context);
    }
    if (_.isString(cond)) {
        return cond;
    }
    throw 'Illegal parameter passed to conditionToSql:' + JSON.stringify(cond);
}

/**
 * Surround expression in parenthesis
 * @method doPar
 * @public
 * @param {string} expr
 * @returns {string}
 */
function doPar(expr) {
    return "(" + expr + ")";
}


/**
 * get the 'is null' condition over object o
 * @method isNull
 * @public
 * @param {sqlFun|Array|object|null|undefined} o
 * @param {Environment} context
 * @returns {string}
 * @example isnull('f') would be converted as 'f is null'
 */
$sqlf.isNull = function (o, context) {
    return doPar(toSql(o, context) + " is null");
};

/**
 * get the 'is not null' condition over object o
 * @method isNull
 * @public
 * @param {sqlFun|Array|object|null|undefined} o
 * @param {Environment} context
 * @returns {string}
 * @example isNotnull('f') would be converted as 'f is not null'
 */
$sqlf.isNotNull = function (o, context) {
    return doPar(toSql(o, context) + " is not null");
};

/**
 * gets the field name eventually prefixed by an alias table name
 * @param {string} field
 * @param {string} [alias]
 * @returns {string}
 * @example field('id','customer') would be converted into 'customer.id',
 *  while field('id') would be converted into 'id'
 */
$sqlf.field = function (field, alias) {
    if (alias) {
        return alias + '.' + field;
    }
    return field;
};

/**
 * gets the 'object are equal' representation for the db
 * @method eq
 * @param {sqlFun|Array|object|null|undefined} a
 * @param {sqlFun|Array|object|null|undefined} b
 * @param {Environment} context
 * @returns {string}
 */
$sqlf.eq = function (a, b, context) {
    return doPar(toSql(a, context) + "=" + toSql(b, context));
};


/**
 * gets the 'object are not equal' representation for the db
 * @method ne
 * @param {sqlFun|Array|object|null|undefined} a
 * @param {sqlFun|Array|object|null|undefined} b
 * @param {Environment} context
 * @returns {string}
 */
$sqlf.ne = function (a, b, context) {
    return doPar(toSql(a, context) + "<>" + toSql(b, context));
};

/**
 * gets the 'a > b' representation for the db
 * @method gt
 * @public
 * @param {sqlFun|object|null|undefined} a
 * @param {sqlFun|object|null|undefined} b
 * @param {Environment} context
 * @returns {string}
 * @example gt('a','b') would be converted into 'a>b'
 */
$sqlf.gt = function (a, b, context) {
    return doPar(toSql(a, context) + ">" + toSql(b, context));
};

/**
 * gets the aggregates of mimimum value
 * @method min
 * @public
 * @param {sqlFun|Array|object|null|undefined} expr
 * @param {Environment} context
 * @returns {string}
 */
$sqlf.min = function (expr, context) {
    return 'min' + doPar(toSql(expr, context));
};

/**
 * gets the aggregates of max value
 * @method max
 * @public
 * @param {sqlFun|Array|object|null|undefined} expr
 * @param {Environment} context
 * @returns {string}
 */
$sqlf.max = function (expr, context) {
    return 'max' + doPar(toSql(expr, context));
};

//see http://msdn.microsoft.com/it-it/library/ms187748.aspx for details
/**
 * gets a substring from the expression
 * @method substring
 * @public
 * @param {sqlFun|Array|object|null|undefined} expr
 * @param {number} start
 * @param {number} len
 * @param {Environment} context
 * @returns {string}
 */
$sqlf.substring = function (expr, start, len, context) {
    return 'SUBSTRING' + doPar(toSql([expr, start, len], context));
};
/**
 * returns the  first object of the array that is not null
 * @public
 * @param {object[]} arr
 * @param {Environment} context
 * @returns {string}
 */
$sqlf.coalesce = function (arr,context) {
    if (arr.length===2){
        return 'ifnull' + doPar(toSql(arr, context)); //THIS IS DIFFERENT FOR MYSQL
    }
    return 'coalesce' + doPar(toSql(arr, context));
};

/**
 * Convert an expression into integer
 * @method convertToInt
 * @public
 * @param {sqlFun|Array|object|null|undefined} expr
 * @param {Environment} context
 * @return {string}
 */
$sqlf.convertToInt = function (expr, context) {
    return 'CONVERT(int,' + toSql(expr, context) + ')';
};

/**
 * Convert an expression into integer
 * @method convertToString
 * @param {sqlFun|string|null|undefined} expr
 * @param {number} maxLen
 * @param {Environment} context
 * @return {string}
 */
$sqlf.convertToString = function (expr, maxLen, context) {
    return 'CONVERT(varchar(' + maxLen + '),' + toSql(expr, context) + ')';
};

/**
 * gets the 'a >= b' representation for the db
 * @method ge
 * @param {sqlFun|Array|object|null|undefined} a
 * @param {sqlFun|Array|object|null|undefined} b
 * @param {Environment} context
 * @returns {string}
 * @example ge('a','b') would be converted into 'a>=b'
 */
$sqlf.ge = function (a, b, context) {
    return doPar(toSql(a, context) + ">=" + toSql(b, context));
};

/**
 * gets the 'a < b' representation for the db
 * @method lt
 * @param {sqlFun|Array|object|null|undefined} a
 * @param {sqlFun|Array|object|null|undefined} b
 * @param {Environment} context
 * @returns {string}
 * @example lt('a','b') would be converted into 'a<b'
 */
$sqlf.lt = function (a, b, context) {
    return doPar(toSql(a, context) + "<" + toSql(b, context));
};

/**
 * gets the 'a <= b' representation for the db
 * @method le
 * @param {sqlFun|Array|object|null|undefined} a
 * @param {sqlFun|Array|object|null|undefined} b
 * @param {Environment} context
 * @returns {string}
 * @example le('a','b') would be converted into 'a<=b'
 */
$sqlf.le = function (a, b, context) {
    return doPar(toSql(a, context) + "<=" + toSql(b, context));
};


/**
 * gets the 'test if Nth bit is set' representation for the db
 * @method bitSet
 * @param {sqlFun|Array|object|null|undefined} a
 * @param {sqlFun|Array|object|null|undefined} b
 * @param {Environment} context
 * @returns {string}
 * @example bitSet('a','3') would be converted into '(a&(1<<3))<>0'
 */
$sqlf.bitSet = function (a, b, context) {
    return "((" + toSql(a, context) + "&(1<<" + toSql(b, context) + "))<>0";
};


/**
 * gets the 'test if Nth bit is not set' representation for the db
 * @method bitClear
 * @param {sqlFun|Array|object|null|undefined} a
 * @param {sqlFun|Array|object|null|undefined} b
 * @param {Environment} context
 * @returns {string}
 * @example bitClear('a','3') would be converted into '(a&(1<<3))=0'
 */
$sqlf.bitClear = function (a, b, context) {
    return "((" + toSql(a, context) + "&(1<<" + toSql(b, context) + "))=0";
};


/**
 * gets the 'not expression' representation for the db
 * @method not
 * @param {sqlFun|Array|object|null|undefined} a
 * @param {Environment} context
 * @returns {string}
 * @example not('a') would be converted into 'not(a)'
 */
$sqlf.not = function (a, context) {
    return "not" + doPar(toSql(a, context));
};


/**
 * gets the 'not expression' representation for the db
 * @method minus
 * @param {sqlFun|Array|object|null|undefined} a
 * @param {Environment} context
 * @returns {string}
 * @example -('a') would be converted into '-a'
 */
$sqlf.minus = function (a, context) {
    return "-" + doPar(toSql(a, context));
};


/**
 * gets the result of boolean "and" between an array of condition
 * @method joinAnd
 * @param {string[]} arr
 * @returns {string}
 * @example joinAnd(['a','b','c']) would give 'a and b and c'
 */
$sqlf.joinAnd = function (arr) {
    return doPar(_.filter(arr, function (cond) {
        return !isEmptyCondition(cond);
    }).
    join(" and "));
};

/**
 * gets the result of boolean "or" between an array of condition
 * @method joinOr
 * @param {string[]} arr
 * @returns {string}
 * @example joinOr(['a','b','c']) would give 'a or b or c'
 */
$sqlf.joinOr = function (arr) {
    return doPar(_.filter(arr, function (cond) {
        return !isEmptyCondition(cond);
    })
        .join(" or "));
};


/**
 * gets the result of the sum of an array of expression
 * @method add
 * @param {Array.<sqlFun|Array|object|null|undefined>} arr
 * @param {Environment} context
 * @returns {string}
 * @example add(['a','b','c']) would give 'a+b+c'
 */
$sqlf.add = function (arr, context) {
    return doPar(_.map(arr, function (a) {
        return toSql(a, context);
    }).join("+"));
};

/**
 * gets the result of the multiply of an array of expression
 * @method mul
 * @param {Array.<sqlFun|Array|object|null|undefined>} arr
 * @param {Environment} context
 * @returns {string}
 * @example mul(['a','b','c']) would give 'a*b*c'
 */
$sqlf.mul = function (arr, context) {
    return doPar(_.map(arr, function (a) {
        return toSql(a, context);
    }).join("*"));
};

/**
 * gets the result of the sum of an array of expression
 * @method concat
 * @param {Array.<sqlFun|Array|object|null|undefined>} arr
 * @param {Environment} context
 * @returns {string}
 * @example add(['a','b','c']) would give 'a+b+c'
 */
$sqlf.concat = function (arr, context) {
    return doPar(_.map(arr, function (a) {
        return toSql(a, context);
    }).join("+"));
};

/**
 * gets the expression a-b
 * @method sub
 * @param {sqlFun|object|null|undefined} a
 * @param {sqlFun|object|null|undefined} b
 * @param {Environment} context
 * @returns {string}
 */
$sqlf.sub = function (a, b, context) {
    return doPar([toSql(a, context), toSql(b, context)].join("-"));
};


/**
 * gets the expression a/b
 * @method div
 * @param {sqlFun|object|null|undefined} a
 * @param {sqlFun|object|null|undefined} b
 * @param {Environment} context
 * @returns {string}
 */
$sqlf.div = function (a, b, context) {
    return doPar([toSql(a, context), toSql(b, context)].join("/"));
};

/**
 * gets the expression sum(expr)
 * @method sum
 * @param {sqlFun|object|null|undefined} expr
 * @param {Environment} context
 * @returns {string}
 */
$sqlf.sum = function (expr, context) {
    return 'sum' + doPar(toSql(expr, context));
};

/**
 * gets the expression distinct expr1, expr2,..
 * @method sum
 * @param {Array.<sqlFun|object|null|undefined>} exprList
 * @param {Environment} context
 * @returns {string}
 */
$sqlf.distinct = function (exprList, context) {
    return 'distinct ' + _.map(exprList, function (expr) {
        return toSql(expr, context);
    }).join(',');
};

/**
 * gets the 'elements belongs to list' sql condition
 * @method isIn
 * @param  {sqlFun|object|null|undefined}expr
 * @param  {Array.<sqlFun|object|null|undefined>} list
 * @param {Environment} context
 * @returns {string}
 * @example isIn('el',[1,2,3,4]) would be compiled into 'el in (1,2,3,4)'
 */
$sqlf.isIn = function (expr, list, context) {
    return doPar(toSql(expr, context) + " in " + toSql(list, context));
};


/**
 * get the '(expr (bitwise and) testMask) equal to val ' sql condition
 * @method testMask
 * @public
 * @param {sqlFun|object|null|undefined} expr
 * @param {sqlFun|object|null|undefined} mask
 * @param {sqlFun|object|null|undefined} val
 * @param {Environment} context
 * @returns {string}
 * @example testMask('a',5,1) would give '(a &  5) = 1'
 */
$sqlf.testMask = function (expr, mask, val, context) {
    return doPar(doPar(toSql(expr, context) + ' & ' + toSql(mask, context)) + '=' + toSql(val, context));
};

/**
 * get the 'expr between min and max' sql condition
 * @method between
 * @public
 * @param {sqlFun|object|null|undefined} expr
 * @param {sqlFun|object|null|undefined} min
 * @param {sqlFun|object|null|undefined} max
 * @param {Environment} context
 * @returns {string}
 */
$sqlf.between = function (expr, min, max, context) {
    return doPar(toSql(expr, context) + ' between ' + toSql(min, context) + ' and ' + toSql(max, context));
};

/**
 * gets the 'expression like mask' sql condition
 * @method like
 * @param {sqlFun|object|null|undefined} expr
 * @param {sqlFun|object|null|undefined} mask
 * @param {Environment} context
 * @returns {string}
 */
$sqlf.like = function (expr, mask, context) {
    return doPar(toSql(expr, context) + ' like ' + toSql(mask, context));
};

$sqlf.toSql = toSql;
$sqlf.quote = quote;
$sqlf.conditionToSql = conditionToSql;
$sqlf.isEmptyCondition = isEmptyCondition;

const charTypes = {
    'text': true,
    'ntext': true,
    'varchar': true,
    'char': true,
    'nvarchar': true,
    'nchar': true,
    'sysname': true
};

const intTypes = {
    'tinyint': true,
    'smallint': true,
    'int': true,
    'bigint': true
};

const floatTypes = {
    'real': true,
    'money': true,
    'float': true,
    'decimal': true,
    'numeric': true,
    'smallmoney': true
};


/**
 * Get object from a string, assuming that the strings represents a given sql type
 * @method getObject
 * @param {string} s
 * @param {string} sqlType, one of the db specific allowed
 * @return {object}
 **/
function getObject(s, sqlType) {
    if (charTypes[sqlType]) {
        return s;
    }
    if (intTypes[sqlType]) {
        return parseInt(s, 10);
    }
    if (floatTypes[sqlType]) {
        return parseFloat(s);
    }
    //TODO : date time management
    return s;
}

$sqlf.getObject = getObject;


module.exports = {
    jsMySqlFormatter: $sqlf
};