Source: client/components/metadata/jsDataSet.js

/**
 * Created by Gaetano Lazzo on 07/02/2015.
 * Thanks to lodash, ObjectObserve
 */
/* jslint nomen: true */
/* jslint bitwise: true */
/*globals Environment,jsDataAccess,Function,jsDataQuery,define,_ */


(function (_,  dataQuery) {
    'use strict';


    //noinspection JSUnresolvedVariable

    /** Detect free variable `global` from Node.js. */
    let freeGlobal = typeof global === 'object' && global && global.Object === Object && global;

    //const freeGlobal = freeExports && freeModule && typeof global === 'object' && global;


    /** Detect free variable `self`. */
    let freeSelf = typeof self === 'object' && self && self.Object === Object && self;

    /** Used as a reference to the global object. */
    let root = freeGlobal || freeSelf || Function('return this')();



    /** Detect free variable `exports`. */
    let freeExports = typeof exports == 'object' && exports && !exports.nodeType && exports;


    /** Detect free variable `module`. */
    let freeModule = freeExports && typeof module === 'object' && module && !module.nodeType && module;


    //noinspection JSUnresolvedVariable
    /** Detect free variable `global` from Node.js or Browserified code and use it as `root`. (thanks lodash)*/
    let moduleExports = freeModule && freeModule.exports === freeExports;



    /**
     * @property CType
     * @public
     * @enum CType
     */
    const CType = {
        'byteArray':'byteArray',
        'string': 'string',
        'int': 'int',
        'number': 'number',
        'date': 'date',
        'bool': 'bool',
        'unknown': 'unknown'
    };




    //noinspection JSValidateTypes
    /**
     * @public
     * @enum DataRowState
     */
    const DataRowState = {
            detached: "detached",
            deleted: "deleted",
            added: "added",
            unchanged: "unchanged",
            modified: "modified"
        },


        /**
         * Enumerates possible version of a DataRow field: original, current
         * @public
         * @enum DataRowVersion
         */
        DataRowVersion = {
            original: "original",
            current: "current"
        };

    function dataRowDefineProperty(r, target, property,value) {
        if (r.removed.hasOwnProperty(property)) {
            //adding a property that was previously removed
            if (r.removed[property] !== value) {
                //if the property had been removed with a different value, that values now goes into
                // old values
                r.old[property] = r.removed[property];
            }
            delete r.removed[property];
        }
        else {
            r.added[property] = value;
        }
    }

    const proxyObjectRow = {
        get: function(target, prop, receiver) {
            if (typeof prop === 'symbol'){
                return target[prop];
            }
            if (target.getRow  && prop.startsWith('$')) { //&&  typeof prop === 'string'
                if (prop === "$acceptChanges") {
                    return () => target.getRow().acceptChanges();
                }
                if (prop === "$rejectChanges") {
                    return () => target.getRow().rejectChanges();
                }
                if (prop === "$del") {
                    return () => target.getRow().del();
                }
                if (prop === "$DataRow") {
                    return target.getRow();
                }
            }
            return target[prop];
        },


        set: function(target, property, value, receiver) {
            if (!target.getRow) {
                return  false;
            }

            let r  = target.getRow();
            if (!r){
                return false;
            }
            if (!target.hasOwnProperty(property)){
                dataRowDefineProperty(r,target,property);
            }
            //if property is added, old values has not to be set
            if (!r.added.hasOwnProperty(property)) {
                if (!r.old.hasOwnProperty(property)) {//only original value has to be saved
                    r.old[property] = target[property];
                }
                else {
                    if (r.old[property] === value) {
                        delete r.old[property];
                    }
                }
            }
            target[property]=value;
            return true;
        },
        defineProperty: function(target, property, descriptor) {
            if (!target.getRow) {
                return  false;
            }
            let r  = target.getRow();
            if (!r){
                return false;
            }
            dataRowDefineProperty(r,target,property,target[property]);
            return Reflect.defineProperty(target, property, descriptor);
        },

        deleteProperty: function(target, property) {
            if (!target.getRow) {
                return  false;
            }
            let r  = target.getRow();
            if (!r){
                return false;
            }
//                property; // a property which has been been removed from obj
//                getOldValueFn(property); // its old value
            if (r.added.hasOwnProperty(property)) {
                delete r.added[property];
            }
            else {
                if (r.old.hasOwnProperty(property)) {
                    //removing a property that had been previously modified
                    r.removed[property] = r.old[property];
                }
                else {
                    r.removed[property] = target[property];
                }
            }
            delete target[property];
            return true;
        },

    };

    /**
     * @public
     * @class DataColumn
     * @param {string} columnName
     * @param {CType} ctype type of the column field
     **/
    function DataColumn(columnName, ctype) {

        /**
         * name of the column
         * @property {string} name
         **/
        this.name = columnName;

        /**
         * type of the column
         * @property {CType} ctype
         **/
        this.ctype = ctype;

        /**
         * Skips this column on insert copy
         * @type {boolean}
         */
        //this.skipInsertCopy = false;

        /**
         * column name for posting to db
         * @property {string} forPosting
         **/
        this.forPosting= undefined;
    }


    /**
     * DataRow shim, provides methods to manage objects as Ado.Net DataRows
     * @module DataSet
     * @submodule DataRow
     */


    /**
     * class type to host data
     * @public
     * @class ObjectRow
     */
    function ObjectRow() {
        return null;
    }

    ObjectRow.prototype = {
        constructor: ObjectRow,
        /**
         * Gets the DataRow linked to an ObjectRow
         * @public
         * @method getRow
         * @returns {DataRow}
         */
        getRow : function () {
            return null;
        }
    };

    /**
     * Provides methods to manage objects as Ado.Net DataRows
     * Creates a DataRow from a generic plain object
     * @class
     * @name DataRow
     * @param {object} o this is the main object managed by the application logic, it is attached to a getRow function
     */
    function DataRow(o) {
        if (o.constructor === DataRow) {
            throw 'Called  DataRow with a DataRow as input parameter';
        }

        if (o.getRow) {
            if (this && this.constructor === DataRow) {
                o = _.clone(o);
                //throw 'Called new DataRow with an object already attached to a DataRow';
            }
            else {
                return o.getRow();
            }
        }

        if (this === undefined || this.constructor !== DataRow) {
            return new DataRow(o);
        }

        if (!o || typeof o !== 'object') {
            throw ('DataRow(o) needs an object as parameter');
        }

        /**
         * previous values of the DataRow, only previous values of changed fields are stored
         * @internal
         * @property {object} old
         */
        this.old = {};

        /**
         * fields added to object (after last acceptChanges())
         * @internal
         * @property {object} added
         */
        this.added = {};

        /**
         * fields removed (with delete o.field) from object (after last acceptChanges())
         * @internal
         * @property {object} removed
         */
        this.removed = {};

        this.myState = DataRowState.unchanged;
        let that = this;

        /**
         * State of the DataRow, possible values are added unchanged modified deleted detached
         * @public
         * @property  state
         * @type DataRowState
         */
        Object.defineProperty(this, 'state', {
            get: function () {
                if (that.myState === DataRowState.modified || that.myState === DataRowState.unchanged) {
                    if (Object.keys(that.old).length === 0 &&
                        Object.keys(that.added).length === 0 &&
                        Object.keys(that.removed).length === 0) {
                        that.myState = DataRowState.unchanged;
                    }
                    else {
                        that.myState = DataRowState.modified;
                    }
                }
                return that.myState;
            },
            set: function (value) {
                that.myState = value;
            },
            enumerable: false
        });


        /**
         * Get the DataRow attached to an object. This method is attached to the object itself,
         *  so you can get the DataRow calling o.getRow() where o is the plain object
         * This transforms o into an ObjectRow
         */
        Object.defineProperty(o, 'getRow', {
            value: function () {
                return that;
            },
            enumerable: false,
            configurable: true   //allows a successive deletion of this property
        });



        /**
         * @public
         * @property {ObjectRow} current current value of the DataRow is the ObjectRow attached to it
         */
        this.current = new Proxy(o,proxyObjectRow);

        /**
         * @public
         * @property {DataTable} table
         */
        this.table=undefined;
    }

    /**
     * @type {DataRow}
     */
    DataRow.prototype = {
        constructor: DataRow,

        /**
         * get the value of a field of the object. If dataRowVer is omitted, it's equivalent to o.fieldName
         * @method getValue
         * @param {string} fieldName
         * @param {DataRowVersion} [dataRowVer='current'] possible values are 'original', 'current'
         * @returns {object}
         */
        getValue: function (fieldName, dataRowVer) {
            if (dataRowVer === DataRowVersion.original) {
                if (this.old.hasOwnProperty(fieldName)) {
                    return this.old[fieldName];
                }
                if (this.removed.hasOwnProperty(fieldName)) {
                    return this.removed[fieldName];
                }
                if (this.added.hasOwnProperty(fieldName)) {
                    return undefined;
                }
            }
            return this.current[fieldName];
        },

        /**
         * Gets the original row, before changes was made, undefined if current state is added
         * @method originalRow
         * @return {object}
         */
        originalRow: function () {
            if (this.state === DataRowState.unchanged || this.state === DataRowState.deleted) {
                return this.current;
            }
            if (this.state === DataRowState.added) {
                return undefined;
            }

            let o = {},
                that = this;
            _.forEach(_.keys(this.removed), function (k) {
                o[k] = that.removed[k];
            });
            _.forEach(_.keys(this.old), function (k) {
                o[k] = that.old[k];
            });
            _.forEach(_.keys(this.current), function (k) {
                if (that.added.hasOwnProperty(k)) {
                    return; //not part of original row
                }
                if (that.old.hasOwnProperty(k)) {
                    return; //not part of original row
                }
                o[k] = that.current[k];
            });
            return o;
        },

        /**
         * Make this row identical to another row (both in state, original and current value)
         * @param r {DataRow}
         * @return {DataRow}
         */
        makeSameAs: function (r) {
            if (this.state === DataRowState.deleted) {
                this.rejectChanges();
            }
            if (r.state === DataRowState.deleted) {
                return this.makeEqualTo(r.originalRow()).acceptChanges().del();
            }
            if (r.state === DataRowState.unchanged) {
                return this.makeEqualTo(r.current).acceptChanges();
            }
            if (r.state === DataRowState.modified) {
                return this.makeEqualTo(r.originalRow()).acceptChanges().makeEqualTo(r.current);
            }
            if (r.state === DataRowState.added) { //assumes this also is already in the state of "added"
                let res= this.makeEqualTo(r.current);
                res.state=DataRowState.added;
                return res;
            }
            return this;
        },


        /**
         * changes current row to make it's current values equal to another one. Deleted rows becomes modified
         * compared to patchTo, this also removes values that are not present in other row
         * @method makeEqualTo
         * @param {object} o
         * @return {DataRow}
         */
        makeEqualTo: function (o) {
            /**
             * @type {DataRow}
             */
            let that = this;
            if (this.state === DataRowState.deleted) {
                this.rejectChanges();
            }
            //removes properties in this that are not present in o
            _.forEach(_.keys(this.current), function (k) {
                if (!o.hasOwnProperty(k)) {
                    delete that.current[k];
                }
            });

            //get all properties from o
            _.forEach(_.keys(o), function (k) {
                that.current[k] = o[k];
            });

            return that;
        },

        /**
         * changes current row to make it's current values equal to another one. Deleted rows becomes modified
         * @method patchTo
         * @param {object} o
         * @return {DataRow}
         */
        patchTo: function (o) {
            let that = this;
            if (this.state === DataRowState.deleted) {
                this.rejectChanges();
            }

            //get all properties from o
            _.forEach(_.keys(o), function (k) {
                that.current[k] = o[k];
            });

            return this;
        },



        /**
         * Get the column name of all modified/added/removed fields
         * @return {*}
         */
        getModifiedFields: function () {
            return _.union(_.keys(this.old), _.keys(this.removed), _.keys(this.added));
        },

        /**
         * Makes changes permanents, discarding old values. state becomes unchanged, detached remains detached
         * @method acceptChanges
         * @return {DataRow}
         */
        acceptChanges: function () {
            if (this.state === DataRowState.detached) {
                return this;
            }
            if (this.state === DataRowState.deleted) {
                this.detach();
                return this;
            }
            this.reset();
            return this;
        },

        /**
         * Discard changes, restoring the original values of the object. state becomes unchanged,
         * detached remains detached
         * @method rejectChanges
         * @return {DataRow}
         */
        rejectChanges: function () {
            if (this.state === DataRowState.detached) {
                return this;
            }

            if (this.state === DataRowState.added) {
                this.detach();
                return this;
            }
            _.extend(this.current, this.old);
            let that = this;
            _.forEach(this.added, function (value, fieldToDel) {
                delete that.current[fieldToDel];
            });
            _.forEach(this.removed, function (value, fieldToAdd) {
                that.current[fieldToAdd] = that.removed[fieldToAdd];
            });

            this.reset();
            return this;
        },

        /**
         * resets all change and sets state to unchanged
         * @private
         * @method _reset
         * @return {DataRow}
         */
        reset: function () {
            this.old = {};
            this.added = {};
            this.removed = {};
            this.state = DataRowState.unchanged;
            return this;
        },

        /**
         * Detaches row, loosing all changes made. object is also removed from the underlying DataTable.
         * Proxy is disposed.
         * @method detach
         * @return {undefined}
         */
        detach: function () {
            this.state = DataRowState.detached;
            if (this.table) {
                //this calls row.detach
                this.table.detach(this.current);
                return undefined;
            }
            delete this.current.getRow;
            return undefined;
        },

        /**
         * Deletes the row. If it is in added state it becomes detached. Otherwise any changes are lost, and
         *  only rejectChanges can bring the row into life again
         *  @method del
         *  @returns {DataRow}
         */
        del: function () {
            if (this.state === DataRowState.deleted) {
                return this;
            }
            if (this.state === DataRowState.added) {
                this.detach();
                return this;
            }
            if (this.state === DataRowState.detached) {
                return this;
            }
            this.rejectChanges();
            this.state = DataRowState.deleted;
            return this;
        },

        /**
         * Debug - helper function
         * @method toString
         * @returns {string}
         */
        toString: function () {
            if (this.table) {
                return 'DataRow of table ' + this.table.name + ' (' + this.state + ')';
            }
            return 'DataRow' + ' (' + this.state + ')';
        },

        /**
         * Gets the parent(s) of this row in the dataSet it is contained, following the relation with the
         *  specified name
         * @method getParentRows
         * @param {string} relName
         * @returns {ObjectRow[]}
         */
        getParentRows: function (relName) {
            let rel = this.table.dataset.relations[relName];
            if (rel === undefined) {
                throw 'Relation ' + relName + ' does not exists in dataset ' + this.table.dataset.name;
            }
            return rel.getParents(this.current);
        },

        /**
         * Gets all parent rows of this one
         * @returns {ObjectRow[]}
         */
        getAllParentRows: function () {
            let that = this;
            return _(this.table.dataset.relationsByChild[this.table.name])
                .value()
                .reduce(function (list, rel) {
                    return list.concat(rel.getParents(that.current));
                }, [], this);
        },
        /**
         * Gets parents row of this row in a given table
         * @method getParentsInTable
         * @param {string} parentTableName
         * @returns {ObjectRow[]}
         */
        getParentsInTable: function (parentTableName) {
            let that = this;
            return _(this.table.dataset.relationsByChild[this.table.name])
                .filter({parentTable: parentTableName})
                .value()
                .reduce(function (list, rel) {
                    return list.concat(rel.getParents(that.current));
                }, [], this);
        },

        /**
         * Gets the child(s) of this row in the dataSet it is contained, following the relation with the
         *  specified name
         * @method getChildRows
         * @param {string} relName
         * @returns {ObjectRow[]}
         */
        getChildRows: function (relName) {
            let rel = this.table.dataset.relations[relName];
            if (rel === undefined) {
                throw 'Relation ' + relName + ' does not exists in dataset ' + this.table.dataset.name;
            }
            return rel.getChild(this.current);
        },

        /**
         * Gets all child rows of this one
         * @returns {ObjectRow[]}
         */
        getAllChildRows: function () {
            let that = this;
            return _(this.table.dataset.relationsByParent[this.table.name])
                .value()
                .reduce(function (list, rel) {
                    return list.concat(rel.getChild(that.current));
                }, []);
        },

        /**
         * Gets child rows of this row in a given table
         * @method getChildInTable
         * @param  {string} childTableName
         * @returns {ObjectRow[]}
         */
        getChildInTable: function (childTableName) {
            let that = this;
            return _(this.table.dataset.relationsByParent[this.table.name])
                .filter({childTable: childTableName})
                .value()
                .reduce(function (list, rel) {
                    return list.concat(rel.getChild(that.current));
                }, []);
        },

        /**
         * DataTable that contains this DataRow
         * @property table
         * @type DataTable
         */

        /**
         * Get an object with all key fields of this row
         * @method keySample
         * @returns {object}
         */
        keySample: function () {
            return _.pick(this.current, this.table.key);
        }

    };




    /**
     * Describe how to evaluate the value of a column before posting it
     * @constructor AutoIncrementColumn
     * @param {string} columnName
     * @param {object} options same options as AutoIncrement properties
     **/
    function AutoIncrementColumn(columnName, options) {
        /**
         * name of the column that has to be auto-incremented
         * @property {string} columnName
         */
        this.columnName = columnName;

        /**
         * Array of column names of selector fields. The max() is evaluating filtering the values of those fields
         * @property {string[]} [selector]
         */
        this.selector = options.selector || [];

        /**
         * Array of bit mask to use for comparing selector. If present, only corresponding bits will be compared,
         *  i.e. instead of sel=value it will be compared (sel & mask) = value
         * @property {number[]} [selectorMask]
         **/
        this.selectorMask = options.selectorMask || [];

        /**
         * A field to use as prefix for the evaluated field
         * @property {string} [prefixField]
         **/
        this.prefixField = options.prefixField;

        /**
         * String literal to be appended to the prefix before the evaluated max
         * @property {string} [middleConst]
         **/
        this.middleConst = options.middleConst;


        /**
         * for string id, the len of the evaluated max. It is not the overall size of the evaluated id, because a
         *  prefix and a middle const might be present
         * If idLen = 0 and there is no prefix, the field is assumed to be a number, otherwise a 0 prefixed string-number
         *  @property {number} [idLen=0]
         **/
        this.idLen = options.idLen || 0;

        /**
         * Indicates that numbering does NOT depend on prefix value, I.e. is linear in every section of the calculated field
         * @property {boolean} [linearField=false]
         **/
        this.linearField = options.linearField || false;

        /**
         * Minimum temporary value for in-memory rows
         * @property {number} [minimum=0]
         **/
        this.minimum = options.minimum || 0;

        /**
         * true if this field is a number
         * @property {number} [isNumber=false]
         **/
        if (options.isNumber === undefined) {
            this.isNumber = (this.idLen === 0) && (this.prefixField === undefined) &&
                (this.middleConst === undefined);
        }
        else {
            this.isNumber = options.isNumber;
        }

        if (this.isNumber === false && this.idLen === 0) {
            this.idLen = 12; //get a default for idLen
        }
    }

    AutoIncrementColumn.prototype = {
        constructor: AutoIncrementColumn
    };

    /**
     * Gets a function that filter selector fields eventually masking with selectorMask
     * @param row
     * @returns {sqlFun}
     */
    AutoIncrementColumn.prototype.getFieldSelectorMask = function (row) {
        let that = this;
        if (this.getInternalSelector === undefined) {
            this.getInternalSelector = function (r) {
                return dataQuery.and(
                    _.map(that.selector, function (field, index) {
                        if (that.selectorMask && that.selectorMask[index]) {
                            return dataQuery.testMask(field, that.selectorMask[index], r[field]);
                        }
                        else {
                            return dataQuery.eq(field, r[field]);
                        }
                    })
                );
            };
        }
        return this.getInternalSelector(row);
    };

    /**
     * evaluates the function to filter selector on a specified row and column
     * @method getSelector
     * @param {ObjectRow} r
     * @returns {sqlFun}
     */
    AutoIncrementColumn.prototype.getSelector = function (r) {
        let prefix = this.getPrefix(r),
            selector = this.getFieldSelectorMask(r);

        if (this.linearField === false && prefix !== '') {
            selector = dataQuery.and(selector, dataQuery.like(this.columnName, prefix + '%'));
        }
        return selector;
    };

    /**
     * Gets the prefix evaluated for a given row
     * @method getPrefix
     * @param r
     * @returns string
     */
    AutoIncrementColumn.prototype.getPrefix = function (r) {
        let prefix = '';
        if (this.prefixField) {
            if (r[this.prefixField] !== null && r[this.prefixField] !== undefined) {
                prefix += r[this.prefixField];
            }
        }
        if (this.middleConst) {
            prefix += this.middleConst;
        }
        return prefix;
    };

    /**
     * gets the expression to be used for retrieving the max
     * @method getExpression
     * @param {ObjectRow} r
     * @return {sqlFun}
     */
    AutoIncrementColumn.prototype.getExpression = function (r) {
        let fieldExpr = dataQuery.field(this.columnName),
            lenToExtract,
            startSearch;
        if (this.isNumber) {
            return dataQuery.max(fieldExpr);
        }
        startSearch = this.getPrefix(r).length;
        lenToExtract = this.idLen;
        return dataQuery.max(dataQuery.convertToInt(dataQuery.substring(fieldExpr, startSearch + 1, lenToExtract)));
    };


    /**
     * Optional custom function to be called to evaluate the maximum value
     * @method customFunction
     * @param {ObjectRow} r
     * @param {string} columnName
     * @param {jsDataAccess} conn
     * @return {object}
     **/
    AutoIncrementColumn.prototype.customFunction = null;






    /**
     * A DataTable is s collection of ObjectRow and provides information about the structure of logical table
     * @class
     * @name DataTable
     * @param {string} tableName
     * @constructor
     * @return {DataTable}
     */
    function DataTable(tableName) {
        /**
         * Name of the table
         * @property {string} name
         */
        this.name = tableName;

        /**
         * Collection of rows, each one hiddenly surrounded with a DataRow object
         * @property rows
         * @type ObjectRow[]
         */
        this.rows = [];

        /**
         * Array of key column names
         * @private
         * @property {string[]} myKey
         */
        this.myKey = [];

        /**
         * Set of properties to be assigned to new rows when they are created
         * @property {object} myDefaults
         * @private
         */
        this.myDefaults = {};

        /**
         * Dictionary of DataColumn
         * @property columns
         * @type {{DataColumn}}
         */
        this.columns = {};

        /**
         * @property autoIncrementColumns
         * @type {{AutoIncrementColumn}}
         */
        this.autoIncrementColumns = {};

        /**
         * DataSet to which this table belongs
         * @property {DataSet} dataset
         */
        this.dataset = undefined;
        /**
         * A ordering to use for posting of this table
         * @property postingOrder
         * @type string | string[] | function
         */
    }

    DataTable.prototype = {
        constructor: DataTable,

        /**
         * @private
         * @property maxCache
         * @type object
         */

        /**
         * Mark the table as optimized / not optimized
         * An optimized table has a cache for all autoincrement field
         * @method setOptimize
         * @param {boolean} value
         */
        setOptimized: function (value) {
            if (value === false) {
                delete this.maxCache;
                return;
            }
            if (this.maxCache === undefined) {
                this.maxCache = {};
            }
        },

        /**
         * Check if this table is optimized
         * @method isOptimized
         * @returns {boolean}
         */
        isOptimized: function () {
            return this.maxCache !== undefined;
        },

        /**
         * Clear evaluated max cache
         * @method clearMaxCache
         */
        clearMaxCache: function () {
            if (this.maxCache !== undefined) {
                this.maxCache = {};
            }
        },

        /**
         *
         * @param {string[]}colNames
         */
        getPostingColumnsNames: function (colNames){
            if (this.postingTable()===this.name){
                return  colNames;
            }
            return _.map(colNames, c=>{
                let col=this.columns[c];
                if (col===undefined) {
                    return  c;
                }
                return col.forPosting || c;
            });
        },

        /**
         * Set a value in the max cache
         * @method setMaxExpr
         * @param {string} field
         * @param {sqlFun} expr
         * @param {sqlFun} filter
         * @param {int} num
         */
        setMaxExpr: function (field, expr, filter, num) {
            if (this.maxCache === undefined) {
                return;
            }
            let hash = field + '§' + expr.toString() + '§' + filter.toString();
            this.maxCache[hash] = num;
        },

        /**
         *
         * @param {string} name
         * @param {CType} ctype
         * @return {DataColumn}
         */
        setDataColumn: function (name, ctype) {
            let c = this.columns[name];
            if (c){
                c.ctype= ctype;
            } else {
                c= new DataColumn(name, ctype);
            }
            this.columns[name] = c;
            return c;
        },


        /**
         * get/set the minimum temp value for a field, assuming 0 if undefined
         * @method minimumTempValue
         * @param  {string} field
         * @param {number} [value]
         */
        minimumTempValue: function (field, value) {
            let autoInfo = this.autoIncrementColumns[field];
            if (autoInfo === undefined) {
                if (value === undefined) {
                    return 0;
                }
                this.autoIncrementColumns[field] = new AutoIncrementColumn(field, {minimum: value});
            }
            else {
                if (value === undefined) {
                    return autoInfo.minimum || 0;
                }
                autoInfo.minimum = value;
            }
        },


        /**
         * gets the max in cache for a field and updates the cache
         * @method getMaxExpr
         *@param {string} field
         * @param {sqlFun|string}expr
         * @param {sqlFun} filter
         * @return {number}
         */
        getMaxExpr: function (field, expr, filter) {
            let hash = field + '§' + expr.toString() + '§' + filter.toString(),
                res = this.minimumTempValue(field);
            if (this.maxCache[hash] !== undefined) {
                res = this.maxCache[hash];
            }
            this.maxCache[hash] = res + 1;
            return res;
        },

        /**
         * Evaluates the max of an expression eventually using a cached value
         * @method cachedMaxSubstring
         * @param {string} field
         * @param {number} start
         * @param {number} len
         * @param {sqlFun} filter
         * @return {number}
         */
        cachedMaxSubstring: function (field, start, len, filter) {
            let expr;
            if (!this.isOptimized()) {
                return this.unCachedMaxSubstring(field, start, len, filter);
            }
            expr = field + '§' + start + '§' + len + '§' + filter.toString();
            return this.getMaxExpr(field, expr, filter);
        },

        /**
         *  Evaluates the max of an expression without using any cached value. If len = 0 the expression is managed
         *   as a number with max(field) otherwise it is performed max(convertToInt(substring(field,start,len)))
         * @param {string} field
         * @param {number} start
         * @param {number} len
         * @param {sqlFun} filter
         * @return {number}
         */
        unCachedMaxSubstring: function (field, start, len, filter) {
            let res,
                min = this.minimumTempValue(field),
                expr,
                rows;
            if (start === 0 && len === 0) {
                expr = dataQuery.max(field);
            }
            else {
                expr = dataQuery.max(dataQuery.convertToInt(dataQuery.substring(field, start, len)));
            }

            rows = this.selectAll(filter);
            if (rows.length === 0) {
                res = 0;
            }
            else {
                res = expr(rows);
            }

            if (res < min) {
                return min;
            }
            return res;
        },

        /**
         * Extract a set of rows matching a filter function - skipping deleted rows
         * @method select
         * @param {sqlFun} [filter]
         * @returns {ObjectRow[]}
         */
        select: function (filter) {
            if (filter === null || filter === undefined) {
                return _.filter(this.rows, function (r) {
                    return r.getRow().state !== DataRowState.deleted;
                });
            }
            if (filter) {
                if (filter.isTrue) {
                    //console.log("always true: returning this.rows");
                    //does not return deleted rows, coherently with other cases
                    return _.filter(this.rows, function (r) {
                        return r.getRow().state !== DataRowState.deleted;
                    });
                    //return this.rows;
                }
                if (filter.isFalse) {
                    //console.log("always false: returning []");
                    return [];
                }
            }
            return _.filter(this.rows, function (r) {
                //console.log('actually filtering by '+filter);
                if (r.getRow().state === DataRowState.deleted) {
                    //console.log("skipping a deleted row");
                    return false;
                }
                if (filter) {
                    //console.log('filter(r) is '+filter(r));
                    //noinspection JSValidateTypes   because a sqlFun is also a Function
                    return filter(r);
                }
                return true;
            });
        },

        /**
         * Extract a set of rows matching a filter function - including deleted rows
         * @method selectAll
         * @param {sqlFun} filter
         * @returns {ObjectRow[]}
         */
        selectAll: function (filter) {
            if (filter) {
                return _.filter(this.rows, filter);
            }
            return this.rows;
        },


        /**
         * Get the filter that compares key fields of a given row
         * @method keyFilter
         * @param {object} row
         * @returns {*|sqlFun}
         */
        keyFilter: function (row) {
            if (this.myKey.length === 0) {
                throw 'No primary key specified for table:' + this.name + ' and keyFilter was invoked.';
            }
            return dataQuery.mcmp(this.myKey, row);
        },

        /**
         * Compares the key of two objects
         * @param {object} a
         * @param {object} b
         * @returns {boolean}
         */
        sameKey: function (a, b) {
            return _.find(this.myKey, function (k) {
                return a[k] !== b[k];
            }) !== undefined;
        },

        /**
         * Get/Set the primary key in a Jquery fashioned style. If k is given, the key is set, otherwise the existing
         *  key is returned
         * @method key
         * @param {string[]} [k]
         * @returns {*|string[]}
         */
        key: function (k) {
            if (k === undefined) {
                return this.myKey;
            }
            if (_.isArray(k)) {
                this.myKey = _.clone(k);
            }
            else {
                this.myKey = Array.prototype.slice.call(arguments);
            }
            _.forEach(this.columns,function(c){
                delete c.isPrimaryKey;
            });
            let self=this;
            _.forEach(this.myKey,function(k){
                if (!self.columns[k]){
                    return true;
                }
                self.columns[k].isPrimaryKey=true;
            });
            return this;
        },

        /**
         * Check if a column is key
         * @param {string} k
         * @returns {boolean}
         */
        isKey: function(k){
            if (this.columns[k]){
                return this.columns[k].isPrimaryKey;
            }
            return this.myKey.indexOf(k)>=0;
        },
        /**
         * Clears the table detaching all rows.
         * @method clear
         */
        clear: function () {
            let dr;
            _.forEach(this.rows, function (row) {
                dr = row.getRow();
                dr.table = null;
                dr.detach();
            });
            this.rows.length = 0;
        },

        /**
         * Detaches a row from the table
         * @method detach
         * @param obj
         */
        detach: function (obj) {
            if (!obj.acceptChanges){//non è il proxy, ottiene il proxy dal DataRow associato
                obj = obj.getRow().current;
            }

            let i = this.rows.indexOf(obj),
                dr;
            if (i >= 0) {
                this.rows.splice(i, 1);
            }
            dr = obj.getRow();
            dr.table = null;
            dr.detach();
        },

        /**
         * Adds an object to the table setting the datarow in the state of "added"
         * @method add
         * @param obj plain object
         * @returns DataRow created
         */
        add: function (obj) {
            let dr = this.load(obj);
            if (dr.state === DataRowState.unchanged) {
                dr.state = DataRowState.added;
            }
            return dr;
        },

        /**
         * check if a row is present in the table. If there is  a key, it is used for finding the row,
         *  otherwise a ==== comparison is made
         * @method existingRow
         * @param {Object} obj
         * @return {DataRow | undefined}
         */
        existingRow: function (obj) {
            if (obj.getRow && !obj.acceptChanges){
                obj = obj.getRow().current;
            }
            if (this.myKey.length === 0) {
                let i = this.rows.indexOf(obj);
                if (i === -1) {
                    return undefined;
                }
                return this.rows[i];
            }
            let arr = _.filter(this.rows, this.keyFilter(obj));
            if (arr.length === 0) {
                return undefined;
            }
            return arr[0];
        },

        /**
         * Adds an object to the table setting the datarow in the state of "unchanged"
         * @method load
         * @param {object} obj plain object to load in the table
         * @param {boolean} [safe=true] if false doesn't verify existence of row
         * @returns {DataRow} created DataRow
         */
        load: function (obj, safe) {
            let dr, oldRow;
            if (safe || safe === undefined) {
                oldRow = this.existingRow(obj);
                if (oldRow) {
                    return oldRow.getRow();
                }
            }
            dr = new DataRow(obj);
            dr.table = this;
            this.rows.push(dr.current);
            return dr;
        },

        /**
         * Adds an object to the table setting the datarow in the state of 'unchanged'
         * @method loadArray
         * @param {object[]} arr array of plain objects
         * @param {boolean} safe if false doesn't verify existence of row
         * @return *
         */
        loadArray: function (arr, safe) {
            let that = this;
            _.forEach(arr, function (o) {
                that.load(o, safe);
            });
        },

        /**
         * Accept any changes setting all dataRows in the state of 'unchanged'.
         * Deleted rows become detached and are removed from the table
         * @method acceptChanges
         */
        acceptChanges: function () {
            //First detach all deleted rows
            let newRows = [];
            _.forEach(this.rows,
                /**
                 * @type {ObjectRow}
                 * @param o
                 */
                function (o) {
                    let dr = o.getRow();
                    if (dr.state === DataRowState.deleted) {
                        dr.table = null;
                        dr.detach();
                    }
                    else {
                        dr.acceptChanges();
                        newRows.push(o);
                    }
                });
            this.rows = newRows;
        },

        /**
         * Reject any changes putting all to 'unchanged' state.
         * Added rows become detached.
         * @method rejectChanges
         */
        rejectChanges: function () {
            //First detach all added rows
            let newRows = [];
            _(this.rows).forEach(
                /**
                 * @method
                 * @param {ObjectRow} o
                 */
                function (o) {
                    let dr = o.getRow();
                    if (dr.state === DataRowState.added) {
                        dr.table = null;
                        dr.detach();
                    }
                    else {
                        dr.rejectChanges();
                        newRows.push(o);
                    }
                });
            this.rows = newRows;
        },

        /**
         * Check if any DataRow in the table has changes
         * @method hasChanges
         * @returns {boolean}
         */
        hasChanges: function () {
            return _.some(this.rows, function (o) {
                return o.getRow().state !== DataRowState.unchanged;
            });

        },

        /**
         * gets an array of all modified/added/deleted rows
         * @method getChanges
         * @returns {Array}
         */
        getChanges: function () {
            return _.filter(this.rows, function (o) {
                return o.getRow().state !== DataRowState.unchanged;
            });
        },

        /**
         * Debug-helper function
         * @method toString
         * @returns {string}
         */
        toString: function () {
            return "DataTable " + this.name;
        },

        /**
         * import a row preserving it's state, the row should already have a DataRow attached
         * @method importRow
         * @param {object} row input
         * @returns {DataRow} created
         */
        importRow: function (row) {
            let dr = row.getRow(),
                newR,
                newDr;
            newR = {};
            _.forOwn(row, function (val, key) {
                newR[key] = val;
            });
            newDr = new DataRow(newR);  //this creates an observer on newR
            newDr.state = dr.state;
            newDr.old = _.clone(dr.old, true);
            newDr.added = _.clone(dr.added, true);
            newDr.removed = _.clone(dr.removed, true);
            this.rows.push(newR);
            return newDr;
        },

        /**
         * Get/set the object defaults in a JQuery fashioned style. When def is present, its fields and values are
         *  merged into existent defaults.
         * @method defaults
         * @param [def]
         * @returns {object|*}
         */
        defaults: function (def) {
            if (def === undefined) {
                return this.myDefaults;
            }
            _.assign(this.myDefaults, def);
            return this;
        },

        /**
         * Clears any stored default value for the table
         * @method clearDefaults
         */
        clearDefaults: function () {
            this.myDefaults = {};
        },

        /**
         * creates a DataRow and returns the created object. The created object has the default values merged to the
         *  values in the optional parameter obj.
         * @method newRow
         * @param {object} [obj] contains the initial value of the created objects.
         * @param {ObjectRow} [parentRow]
         * @returns {object}
         */
        newRow: function (obj, parentRow) {
            let n = {};
            _.assign(n, this.myDefaults);
            if (_.isObject(obj)) {
                _.assign(n, obj);
            }
            if (parentRow !== undefined) {
                this.makeChild(n, parentRow.getRow().table.name, parentRow);
            }
            this.calcTemporaryId(n);
            return this.add(n).current;
        },

        /**
         * Make childRow child of parentRow if a relation between the two is found
         * @method makeChild
         * @param {object} childRow
         * @param {string} parentTable
         * @param {ObjectRow} [parentRow]
         */
        makeChild: function (childRow, parentTable, parentRow) {
            let that = this,
                parentRel = _.find(this.dataset.relationsByParent[parentTable],
                    function (rel) {
                        return rel.childTable === that.name;
                    });
            if (parentRel === undefined) {
                return;
            }
            parentRel.makeChild(parentRow, childRow);

        },


        /**
         * Get/Set a flag indicating that this table is not subjected to security functions in a jQuery fashioned
         *  style
         * @method skipSecurity
         * @param {boolean} [arg]
         * @returns {*|boolean}
         */
        skipSecurity: function (arg) {
            if (arg === undefined) {
                if (this.hasOwnProperty('isSkipSecurity')) {
                    return this.isSkipSecurity;
                }
                return false;
            }
            this.isSkipSecurity = arg;
            return this;
        },

        /**
         * Get/Set a flag indicating that this table is not subjected to the Insert and Copy function
         * @method skipInsertCopy
         * @param {boolean} [arg]
         * @returns {*|boolean}
         */
        skipInsertCopy: function (arg) {
            if (arg === undefined) {
                if (this.hasOwnProperty('isSkipInsertCopy')) {
                    return this.isSkipInsertCopy;
                }
                return false;
            }
            this.isSkipInsertCopy = arg;
            return this;
        },

        /**
         * Get/Set DenyClear. === y avoid to clear table on backend reads
         * @method denyClear
         * @param {string} [arg]
         * @returns {*|string}
         */
        denyClear: function (arg) {
            if (arg === undefined) {
                if (this.hasOwnProperty('myDenyClear')) {
                    return this.myDenyClear;
                }
                return false;
            }
            this.myDenyClear = arg;
            return this;
        },

        /**
         * Get/Set a table name, that represents the view table associated to the table
         * @method viewTable
         * @param {string} [arg]
         * @returns {*|string}
         */
        viewTable: function (arg) {
            if (arg === undefined) {
                if (this.hasOwnProperty('myViewTable')) {
                    return this.myViewTable;
                }
                return false;
            }
            this.myViewTable = arg;
            return this;
        },

        /**
         * Get/Set a table name, that represents the real table associated to the table
         * @method realTable
         * @param {string} [arg]
         * @returns {null|string}
         */
        realTable: function (arg) {
            if (arg === undefined) {
                if (this.hasOwnProperty('myRealTable')) {
                    return this.myRealTable;
                }
                return null;
            }
            this.myRealTable = arg;
            return this;
        },

        /**
         * Returns the table that should be used for writing, using tableForReading as a default for tableForWriting,
         *  or this.name if none of them is set
         *  @method postingTable
         *  @public
         * @return {string}
         */
        postingTable: function(){
            return  this.myTableForWriting || this.myTableForReading || this.name;
        },

        /**
         * Get/Set the name of table  to be used to read data from database in a Jquery fashioned style
         * @method tableForReading
         * @param {string} [tableName]
         * @returns {*|DataTable.myTableForReading|DataTable.name}
         */
        tableForReading: function (tableName) {
            if (tableName === undefined) {
                return this.myTableForReading || this.name;
            }
            this.myTableForReading = tableName;
            return this;
        },

        /**
         * Get/Set the name of table  to be used to write data from database in a Jquery fashioned style
         * @method tableForWriting
         * @param {string} [tableName]
         * @returns {*|DataTable.myTableForWriting|DataTable.name}
         */
        tableForWriting: function (tableName) {
            if (tableName === undefined) {
                return this.myTableForWriting || this.name;
            }
            this.myTableForWriting = tableName;
            return this;
        },

        /**
         * Get/Set a static filter  to be used to read data from database in a Jquery fashioned style
         * @method staticFilter
         * @param {sqlFun} [filter]
         * @returns {sqlFun}
         */
        staticFilter: function (filter) {
            if (filter === undefined) {
                return this.myStaticFilter;
            }
            this.myStaticFilter = filter;
        },

        /**
         * Sort a given array of rows, does not change table.
         * @param {ObjectRow[]} rows
         * @param {string} sortOrder it's like field1  [ASC|DESC] [, field2 [ASC|DESC] ..]
         * @return {ObjectRow[]}
         */
        sortRows: function(rows,sortOrder){
            let parts = sortOrder.split(",");
            let result = parts.reduce((prevResult,field)=>{
                let couple= field.trim();
                let parts = couple.split(" ");
                let sortOrder="asc";
                if (parts.length>1 && parts[parts.length-1].toUpperCase()==="DESC"){
                    sortOrder="desc";
                }
                prevResult.fields.push(parts[0]);
                prevResult.sorting.push(sortOrder);
                return prevResult;
            },{fields:[],sorting:[]});
            return _.orderBy(rows,result.fields,result.sorting);
        },

        /**
         * Returns table rows in a specified order, does not change table. Skips deleted rows.
         * @param {string} sortOrder it's like field1  [ASC|DESC] [, field2 [ASC|DESC] ..]
         * @return {ObjectRow[]}
         */
        getSortedRows: function(sortOrder){
          sortOrder = sortOrder || this.myOrderBy;
          if (!sortOrder){
              return this.select();
          }
          return this.sortRows(this.select(), sortOrder);
        },
        /**
         * Get/set the ordering that have to be user reading from db
         * @param {string} [fieldList] it's like field1  [ASC|DESC] [, field2 [ASC|DESC] ..]
         * @returns {string}
         */
        orderBy: function (fieldList) {
            if (fieldList === undefined) {
                return this.myOrderBy;
            }
            this.myOrderBy = fieldList;
            return this;
        },

        /**
         * get the list of columns or * if there is no column set
         * @method columnList
         * @returns string
         */
        columnList: function () {
            let c = _.map(
                this.columns,
                function (c) {
                    return c.name;
                }
            );
            if (c.length > 0) {
                return c.join(",");
            }
            return '*';
        },

        /**
         * Gets all autoincrement column names of this table
         * @method getAutoIncrementColumns
         * @returns string[]
         */
        getAutoIncrementColumns: function () {
            return _.keys(this.autoIncrementColumns);
        },

        /**
         * Get/Set autoincrement properties of fields
         * @method autoIncrement
         * @param {string} fieldName
         * @param {object} [autoIncrementInfo] //see AutoIncrementColumn properties for details
         * @returns {*|AutoIncrementColumn}
         */
        autoIncrement: function (fieldName, autoIncrementInfo) {
            if (autoIncrementInfo !== undefined) {
                this.autoIncrementColumns[fieldName] = new AutoIncrementColumn(fieldName, autoIncrementInfo);
                return this;
            }
            else {
                return this.autoIncrementColumns[fieldName];
            }
        },

        /**
         * Get a serializable version of this table. If serializeStructure=true, also structure information is serialized
         * @param {boolean} [serializeStructure=false]
         * @param {function} [filterRow] optional function for filtering rows to serialize
         * @return {object} the serialization object derived from this DataTable
         */
        serialize: function (serializeStructure, filterRow) {
            let clean= function (r){
                return _.pickBy(r,function(o){return o!==null && o!==undefined;});
            };
            let t = {};
            if (serializeStructure) {
                t.key = this.key().join();
                t.tableForReading = this.tableForReading();
                t.tableForWriting = this.tableForWriting();
                t.isCached = this.isCached;
                t.isTemporaryTable = this.isTemporaryTable;
                t.orderBy = this.orderBy();

                //t.staticFilter(this.staticFilter());
                if (this.staticFilter()) {
                    t.staticFilter=dataQuery.toObject(this.staticFilter());
                }
                t.skipSecurity = this.skipSecurity();
                t.skipInsertCopy = this.skipInsertCopy();
                t.realTable = this.realTable();
                t.viewTable = this.viewTable();
                t.denyClear = this.denyClear();
                t.defaults = this.defaults();
                t.autoIncrementColumns = this.autoIncrementColumns;
                t.columns = {};

                let o = {};
                _.forOwn(this.columns, function (val, key) {
                    o = {};
                    _.forOwn(val, function (v, k) {
                        if ((k === 'expression') && (_.isFunction(v) || _.isArray(v))) {
                            o[k] = dataQuery.toObject(v);
                        } else {
                            o[k] = v;
                        }
                    });
                    t.columns[key] = o;
                });
            }
            t.name= this.name;
            t.rows = [];
            _.forEach(this.rows, function (r) {
                if (filterRow && filterRow(r) === false) {
                    return; //skip this row
                }
                let row = r.getRow(),
                    rowState = row.state,
                    newRow = {state: rowState};
                if (rowState === DataRowState.deleted || rowState === DataRowState.unchanged || rowState === DataRowState.modified) {
                    newRow.old = clean(row.originalRow());
                }
                if (rowState === DataRowState.modified || rowState === DataRowState.added) {
                    newRow.curr = clean(r); //_.clone(r)
                }

                t.rows.push(newRow);
            });
            return t;
        },

        /**
         * Get data from a serialized structure. If serializeStructure=true, also structure information is serialized
         * @param {object} t
         * @param {boolean} [deserializeStructure=false]
         * @return {*}
         */
        deSerialize: function (t, deserializeStructure) {
            let that = this;
            if (deserializeStructure) {

                this.tableForReading(t.tableForReading);
                this.tableForWriting(t.tableForWriting);
                this.isCached = t.isCached;
                this.isTemporaryTable = t.isTemporaryTable;

                this.skipSecurity(t.skipSecurity);
                this.skipInsertCopy(t.skipInsertCopy);
                this.realTable(t.realTable);
                this.viewTable(t.viewTable);
                this.denyClear(t.denyClear);
                this.defaults(t.defaults);
                this.orderBy(t.orderBy);
                if (t.staticFilter) {
                    this.staticFilter(dataQuery.fromObject(t.staticFilter));
                }
                _.forEach(t.autoIncrementColumns, function (aiObj) {
                    let columnName = aiObj.columnName;

                    let options  = _.pick(aiObj, ['prefixField', 'linearField', 'idLen', 'middleConst', 'selector', 'selectorMask', 'minimum']);

                    that.autoIncrementColumns[columnName] = new AutoIncrementColumn(columnName, options);
                });
                if (t.columns) {
                    let o = {};
                    that.columns = {};
                    _.forOwn(t.columns, function (val, key) {
                        o = {};
                        _.forOwn(val, function (v, k) {
                            if (k === 'expression' && _.isObject(v)) {
                                o.expression = dataQuery.fromObject(v);
                            } else {
                                o[k] = v;
                            }
                        });
                        that.columns[key] = o;
                    });
                }

                this.key(t.key.split(','));
            }

            that.name=t.name;
            _.forEach(t.rows, function (r) {
                let rowState = r.state;
                if (rowState === DataRowState.added) {
                    that.add(r.curr);
                    return;
                }
                let newRow = that.load(r.old); //newRow is unchanged
                if (rowState === DataRowState.deleted) {
                    newRow.del();
                    return;
                }
                if (rowState === DataRowState.modified) {
                    newRow.acceptChanges();
                    newRow.makeEqualTo(r.curr);
                }
            });
        },


        /**
         * Get all relation where THIS table is the child and another table is the parent
         * @method parentRelations
         * @returns DataRelation[]
         */
        parentRelations: function () {
            return this.dataset.relationsByChild[this.name];
        },

        /**
         * Get all relation where THIS table is the parent and another table is the child
         * @method childRelations
         * @returns DataRelation[]
         */
        childRelations: function () {
            return this.dataset.relationsByParent[this.name];
        },

        /**
         * adds an array of objects to collection, as unchanged, if they still are not present. Existence is verified
         *  basing on  key
         * @method mergeArray
         * @param {Object[]} arr
         * @param {boolean} overwrite
         * @return {*}
         */
        mergeArray: function (arr, overwrite) {
            let that = this;
            _.forEach(arr, function (r) {
                    let oldRow = that.existingRow(r);
                    if (oldRow) {
                        if (overwrite) {
                            oldRow.getRow().makeEqualTo(r);
                            oldRow.acceptChanges();
                        }
                    }
                    else {
                        that.load(r, false);
                    }
                }
            );
        },

        /**
         * clones table structure without copying any DataRow
         * @method clone
         * @return {DataTable}
         */
        clone: function () {
            let cloned = new DataTable(this.name);
            cloned.key(this.key());
            cloned.tableForReading(this.tableForReading());
            cloned.tableForWriting(this.tableForWriting());
            cloned.staticFilter(this.staticFilter());
            cloned.skipSecurity(this.skipSecurity());
            cloned.skipInsertCopy(this.skipInsertCopy());
            cloned.realTable(this.realTable());
            cloned.viewTable(this.viewTable());
            cloned.denyClear(this.denyClear());
            cloned.defaults(this.defaults());
            cloned.autoIncrementColumns = _.clone(this.autoIncrementColumns);
            cloned.columns = _.clone(this.columns);
            cloned.orderBy(this.orderBy());
            return cloned;
        },

        /**
         * Clones table structure and copies data
         * method @copy
         * @return {DataTable}
         */
        copy: function () {
            let cloned = this.clone();
            _.forEach(this.rows, function (row) {
                cloned.importRow(row);
            });
        },


        /**
         * Gets a filter of colliding rows supposing to change r[field]= value, on  a specified column
         * @method collisionFilter
         * @private
         * @param {ObjectRow} r
         * @param {string} field
         * @param {object} value
         * @param {AutoIncrementColumn} autoInfo
         * @return {sqlFun}
         */
        collisionFilter: function (r, field, value, autoInfo) {
            let fields = [autoInfo.columnName].concat(autoInfo.selector),
                values = _.map(fields, function (k) {
                    if (k !== field) {
                        return r[k];
                    }
                    return value;
                });
            return dataQuery.mcmp(fields, values);
        },

        /**
         * Assign a field assuring it will not cause duplicates on table's autoincrement fields
         * @method safeAssign
         * @param {ObjectRow} r
         * @param {string} field
         * @param {object} value
         * @return {*}
         */
        safeAssign: function (r, field, value) {
            this.avoidCollisions(r, field, value);
            this.assignField(r, field, value);
        },

        /**
         * check if changing a key field of a row it would collide with come autoincrement field. If it would,
         *  recalculates colliding rows/filter in accordance
         * @method avoidCollisions
         * @param {ObjectRow} r
         * @param {string} field
         * @param {object} value
         */
        avoidCollisions: function (r, field, value) {
            let that = this;
            let deps = this.fieldDependencies(field);
            if (this.autoIncrementColumns[field]) {
                deps.unshift(field);
            }
            _.forEach(deps, function (depField) {
                that.avoidCollisionsOnField(depField,
                    that.collisionFilter(r, field, value, that.autoIncrementColumns[depField]));
            });


        },

        /**
         * Recalculate a field to avoid collisions on some rows identified by a filter
         * @method avoidCollisionsOnField
         * @private
         * @param {string} field
         * @param {sqlFun} filter
         */
        avoidCollisionsOnField: function (field, filter) {
            let that = this;
            _.forEach(this.select(filter), function (rCollide) {
                that.calcTemporaryId(rCollide, field);
            });
        },

        /**
         * Assign a value to a field and update all dependencies
         * @method assignField
         * @param {ObjectRow} r
         * @param {string} field
         * @param {object} value
         */
        assignField: function (r, field, value) {
            this.cascadeAssignField(r, field, value); //change all child field
            r[field] = value;
            this.updateDependencies(r, field); //change all related field
        },

        /**
         * assign a value to a field in a row and all descending child rows
         * @method cascadeAssignField
         * @private
         * @param {ObjectRow} r
         * @param {string} parentField
         * @param {object} value
         */
        cascadeAssignField: function (r, parentField, value) {
            let ds = this.dataset;
            _.forEach(ds.relationsByParent[this.name], function (rel) {
                let pos = _.indexOf(rel.parentCols, parentField);
                if (pos >= 0) {
                    let childField = rel.childCols[pos];
                    _.forEach(rel.getChild(r), function (childRow) {
                        let childTable = ds.tables[rel.childTable];
                        childTable.cascadeAssignField(childRow, childField, value);

                        childRow[childField] = value;
                        childTable.updateDependencies(childRow, childField);
                    });
                }
            });
        },

        /**
         * Gets all autoincrement fields that depends on a given field, i.e. those having field as selector or prefixfield
         * @method fieldDependencies
         * @private
         * @param {string} field
         * @return {string[]}
         */
        fieldDependencies: function (field) {
            let res = [];
            _.forEach(_.values(this.autoIncrementColumns), function (autoInfo) {
                    if (autoInfo.prefixField === field) {
                        res.push(autoInfo.columnName);
                        return;
                    }
                    if (autoInfo.selector && _.indexOf(autoInfo.selector, field) >= 0) {
                        res.push(autoInfo.columnName);
                    }
                }
            );
            return res;
        },

        /**
         * Re calculate temporaryID affected by a field change. It should be done for every autoincrement field
         *  that has that field as a selector or as a prefix field
         * @method updateDependencies
         * @param {ObjectRow} row
         * @param {string} field
         * @returns {*}
         */
        updateDependencies: function (row, field) {
            let that = this;
            _.forEach(this.fieldDependencies(field), function (f) {
                that.calcTemporaryId(row, f);
            });
        },


        /**
         * Augment r[field] in order to avoid collision with another row that needs to take that value
         * if field is not specified, this is applied to all autoincrement field of the table
         * Precondition: r[[field] should be an autoincrement field
         * @method calcTemporaryId
         * @param {ObjectRow} r
         * @param {string} [field]
         */
        calcTemporaryId: function (r, field) {
            let that = this;
            if (field === undefined) {
                _.forEach(_.keys(this.autoIncrementColumns), function (field) {
                    that.calcTemporaryId(r, field);
                });
                return;
            }
            let prefix = '',
                newID,
                evaluatedMax,
                autoIncrementInfo = this.autoIncrementColumns[field],
                selector = autoIncrementInfo.getSelector(r),
                startSearch;
            if (autoIncrementInfo.isNumber) {
                evaluatedMax = this.cachedMaxSubstring(field, 0, 0, selector) + 1;
            }
            else {
                prefix = autoIncrementInfo.getPrefix(r);
                startSearch = prefix.length + 1;
                evaluatedMax = this.cachedMaxSubstring(field, startSearch, autoIncrementInfo.idLen, selector) + 1;
            }

            if (autoIncrementInfo.isNumber) {
                newID = evaluatedMax;
            }
            else {

                newID = evaluatedMax.toString();
                if (autoIncrementInfo.idLen > 0) {
                    while (newID.length < autoIncrementInfo.idLen) {
                        newID = '0' + newID;
                    }
                }
                newID = prefix + newID;
            }

            this.assignField(r, field, newID);

        },
        /**
         * merges changes from dataTable t assuming they are unchanged and they can be present in this or not.
         * If a row is not present, it is added. If it is present, it is updated.
         * It is assumed that "this" dataTable is unchanged at the beginning
         * @method mergeAsPut
         * @param {DataTable} t
         */
        mergeAsPut: function (t) {
            let that = this;
            _.forEach(t.rows, function (r) {
                let existingRow = that.select(that.keyFilter(r));
                if (existingRow.length === 0) {
                    that.add(_.clone(r.current));  // new row state is 'added'
                }
                else {
                    existingRow[0].getRow().makeEqualTo(r.current); //new row state is modified
                }
            });
        },

        /**
         * merges changes from dataTable t assuming they are unchanged and they are not present in this dataTable.
         * Rows are all added 'as is' to this, in the state of ADDED
         * It is assumed that "this" dataTable is unchanged at the beginning
         * @method mergeAsPost
         * @param {DataTable} t
         */
        mergeAsPost: function (t) {
            let that = this;
            _.forEach(t.rows, function (r) {
                that.add(_.clone(r.current)); //row is always simply  added
            });
        },

        /**
         * merges changes from dataTable t assuming they are unchanged and they are all present in this dataTable.
         * Rows are updated, but only  fields actually present in d are modified. Other field are left unchanged.
         * It is assumed that "this" dataTable is unchanged at the beginning
         * @method mergeAsPatch
         * @param {DataTable} t
         */
        mergeAsPatch: function (t) {
            let that = this;
            _.forEach(t.rows, function (r) {
                let existingRow = that.select(t.keyFilter(r));
                if (existingRow.length === 1) {
                    existingRow[0].getRow().patchTo(r); //row is now in the state of updated
                }
            });
        },

        /**
         * merge any row present in dataTable t. Rows are merged as unchanged if they are unchanged,
         *  otherwise their values are copied into existent dataTable
         *  DataSet must have same table structure
         * @param  {DataTable} t
         */
        merge: function (t) {
            let that = this;
            _.forEach(t.rows, function (r) {
                let existingRow = that.select(t.keyFilter(r));
                if (r.getRow().state === DataRowState.deleted) {
                    if (existingRow.length === 1) {
                        existingRow[0].makeSameAs(r.getRow());
                    }
                    else {
                        that.add(_.clone(r.getRow())).acceptChanges().del();
                    }
                }
                else {
                    if (existingRow.length === 1) {
                        existingRow[0].getRow().makeSameAs(r.getRow());
                    }
                    else {
                        that.add({}).makeSameAs(r.getRow());
                    }
                }
            });
        }

    };


    /**
     * Manages auto fill of locking purposed fields and evaluates filter for optimistic locking for update
     * In his basic implementation accept a list of fields to fill. Values for filling are taken from
     *  environment.
     * @class
     * @name OptimisticLocking
     * @param {string[]} updateFields Fields to fill and to check during update operations
     * @param {string[]} createFields Fields to fill and to check during insert operations
     */
    function OptimisticLocking(updateFields, createFields) {
        this.updateFields = updateFields || [];
        this.createFields = createFields || [];

    }

    OptimisticLocking.prototype = {
        constructor: OptimisticLocking,

        /**
         * This function is called before posting row into db for every insert/update
         * @method prepareForPosting
         * @param {ObjectRow} r
         * @param {Environment} env
         */
        prepareForPosting: function (r, env) {
            let row = r.getRow();
            if (row.state === DataRowState.added) {
                _.forEach(this.createFields, function (field) {
                    //noinspection JSUnresolvedFunction
                    r[field] = env.field(field);
                });
                return;
            }
            if (row.state === DataRowState.modified) {
                _.forEach(this.updateFields, function (field) {
                    //noinspection JSUnresolvedFunction
                    r[field] = env.field(field);
                });
            }
        },

        /**
         * Get the optimistic lock for updating or deleting a row
         * @method getOptimisticLock
         * @param {ObjectRow}r
         * @returns {sqlFun}
         */
        getOptimisticLock: function (r) {
            let row = r.getRow(),
                fields,
                key = row.table.key();
            if (key.length !== 0) {
                fields = key.concat(this.updateFields);
                return dataQuery.mcmp(fields,
                    _.map(fields, function (f) {
                        return row.getValue(f, DataRowVersion.original);
                    })
                );
            }
            return dataQuery.mcmp(_.keys(r), r);
        }
    };



    /**
     * Describe a relation between two DataTables of a DataSet.
     * @constructor DataRelation
     * @param {string} relationName
     * @param {string} parentTableName
     * @param {String|String[]} parentColsName array of string
     * @param {string} childTableName
     * @param {String|String[]} [childColsName=parentColsName] optional names of child columns
     * @return {DataRelation}
     */
    function DataRelation(relationName, parentTableName, parentColsName, childTableName, childColsName) {
        this.name = relationName;
        /**
         * Parent table name
         * @property parentTable
         * @type string
         */
        this.parentTable = parentTableName;

        /**
         * DataSet to which this DataRelation belongs to. It is used to retrieve parent and child table
         * @property dataSet
         * @type DataSet
         */
        this.dataset = null;

        /**
         * Array of parent column names or comma separated column names
         * @property parentCols
         * @type String|String[]
         */
        this.parentCols = _.isString(parentColsName) ?
            _.map(parentColsName.split(','), function (s) {
                return s.trim();
            })
            : _.clone(parentColsName);

        /**
         * Child table name
         * @property childTable
         * @type string
         */
        this.childTable = childTableName;

        /**
         * Array of child column names  or comma separated column names
         * @property childCols
         * @type string|string[]
         */
        if (childColsName) {
            this.childCols = _.isString(childColsName) ?
                _.map(childColsName.split(','), function (s) {
                    return s.trim();
                })
                : _.clone(childColsName);
        }
        else {
            this.childCols = this.parentCols;
        }
    }

    DataRelation.prototype = {
        /**
         * Gets a filter that will be applied to the child table and will find any child row of a given ObjectRow
         * @method getChildFilter
         * @param {ObjectRow} parentRow
         * @param {string} [alias] when present is used to attach an alias for the parent table in the composed filter
         */
        getChildFilter: function (parentRow, alias) {
            let that = this;
            return dataQuery.mcmp(that.childCols,
                _.map(that.parentCols, function (col) {
                    return parentRow[col];
                }),
                alias
            );
        },

        /**
         * Get any child of a given ObjectRow following this DataRelation
         * @method getChild
         * @param {ObjectRow} parentRow
         * @returns {ObjectRow[]}
         */
        getChild: function (parentRow) {
            let ds = this.dataset;
            if (ds === null) {
                ds = parentRow.getRow().table.dataset;
            }
            let childTable = ds.tables[this.childTable];
            return _.filter(childTable.rows, this.getChildFilter(parentRow));
        },

        /**
         * Gets a filter that will be applied to the parent table and will find any parent row of a given ObjectRow
         * @method getParentsFilter
         * @param {object} childRow
         * @param {string} [alias] when present is used to attach an alias for the parent table in the composed filter
         */
        getParentsFilter: function (childRow, alias) {
            let that = this;
            return dataQuery.mcmp(that.parentCols,
                _.map(that.childCols, function (col) {
                    return childRow[col];
                }),
                alias
            );
        },


        /**
         * Get any parent of a given ObjectRow following this DataRelation
         * @method getParents
         * @param {ObjectRow} childRow
         * @returns {ObjectRow[]}
         */
        getParents: function (childRow) {
            let ds = this.dataset;
            if (ds === null) {
                ds = childRow.getRow().table.dataset;
            }
            let actualParentTable = ds.tables[this.parentTable];
            return _.filter(actualParentTable.rows, this.getParentsFilter(childRow));
        },

        /**
         * Get a serialized version of this relation
         * @returns {{}}
         */
        serialize: function () {
            let rel = {};
            let sep = ",";
            //relation name is not serialized here, it is a key in the parent
            rel.parentTable = this.parentTable;
            //parent cols are serialized as a comma separated field list
            rel.parentCols = this.parentCols.join(sep);
            rel.childTable = this.childTable;
            //child cols are not serialized if are same as parent cols
            if (this.childCols !== this.parentCols) {
                rel.childCols = this.childCols.join(sep);
            }
            return rel;
        },

        deSerialize: function (rel) {
            let sep = ",";
            //relation name is not serialized here, it is a key in the parent
            this.parentTable = rel.parentTable;
            //parent cols are serialized as a comma separated field list
            this.parentCols = rel.parentCols.split(sep);
            this.childTable = rel.childTable;
            //child cols are not serialized if are same as parent cols
            if (rel.childCols) {
                this.childCols = rel.childCols.split(sep);
            }
            else {
                this.childCols = rel.parentCols.split(sep);
            }
        },

        /**
         * get/set the activation filter for the relation, i.e. a condition that must be satisfied in order to
         *  follow the relation when automatically filling dataset from database. The condition is meant to be applied
         *  to parent rows
         * @param {sqlFun} [filter]
         * @returns {*}
         */
        activationFilter: function (filter) {
            if (filter) {
                this.myActivationFilter = filter;
            }
            else {
                return this.myActivationFilter;
            }
        },

        /**
         * Establish if  a relation links the key of  a table into a subset of another table key
         * @method isEntityRelation
         * @returns {boolean}
         */
        isEntityRelation: function () {
            let parent = this.dataset.tables[this.parentTable],
                parentKey = parent.key(),
                child = this.dataset.tables[this.childTable];
            if (parentKey.length !== this.parentCols.length) {
                return false;
            }
            //parent columns must be the key for parent table
            if (_.difference(parentKey, this.parentCols).length !== 0) {
                return false;
            }
            //child columns must be a subset of the child table key
            if (_.difference(this.childCols, child.key()).length > 0) {
                return false;
            }
            return true;
        },

        /**
         * Modifies childRow in order to make it child of parentRow. Sets to null corresponding fields if
         *  parentRow is null or undefined
         * @method makeChild
         * @param {ObjectRow} parentRow
         * @param {ObjectRow} childRow
         * @return {*}
         */
        makeChild: function (parentRow, childRow) {
            _.each(_.map(
                _.zip(this.parentCols, this.childCols),
                _.curry(_.zipObject)(['parentCol', 'childCol'])
                ),
                function (pair) {
                    if (parentRow === undefined || parentRow === null) {
                        childRow[pair.childCol] = null;
                    }
                    else {
                        childRow[pair.childCol] = parentRow[pair.parentCol];
                    }
                });

            // _.forEach(_.zip(this.parentCols,this.childCols),function(colPair){
            //     if (parentRow === undefined || parentRow === null) {
            //         childRow[colPair[1]] = null;
            //     }
            //     else {
            //         childRow[colPair[1]] = parentRow[colPair[0]];
            //     }
            // });
        }
    };

    /**
     * Stores and manages a set of DataTables and DataRelations
     * @class
     * @name DataSet
     * @param {string} dataSetName
     * @returns {DataSet}
     * @constructor
     */
    function DataSet(dataSetName) {
        if (this.constructor !== DataSet) {
            return new DataSet(dataSetName);
        }
        /**
         * DataSet name
         * @property name
         */
        this.name = dataSetName;


        /**
         * Collection of DataTable where tables[tableName] is a DataTable named tableName
         * @public
         * @property {{DataTable}} tables
         */
        this.tables = {};


        /**
         * Collection of DataRelation  where relations[relName] is a DataRelation named relName
         * @property {{DataRelation}} relations
         */
        this.relations = {};

        /**
         * Gets all relations where the parent table is the key of the hash
         * relationsByParent['a'] is an array of all DataRelations where 'a' is the parent
         * @property {{DataRelation[]}} relationsByParent
         */
        this.relationsByParent = {};

        /**
         * Gets all relations where the child table is the key of the hash
         * relationsByChild['a'] is an array of all DataRelations where 'a' is the child
         *  @property {{DataRelation[]}}  relationsByChild
         */
        this.relationsByChild = {};

        /**
         * DataSet - level optimistic locking, this property is set in custom implementations
         * @property {OptimisticLocking} optimisticLocking
         */
        this.optimisticLocking=undefined;
    }


    DataSet.prototype = {
        constructor: DataSet,
        toString: function () {
            return "dataSet " + this.name;
        },

        getParentChildRelation: function (parentName, childName) {
            return _(this.relationsByChild[childName])
                .filter({parentTable: parentName})
                .value();
        },
        /**
         * Clones a DataSet replicating its structure but without copying any ObjectRow
         * @method clone
         * @returns {DataSet}
         */
        clone: function () {
            /**
             * newDs
             * @type {DataSet}
             */
            let newDs = new DataSet(this.name);
            newDs.optimisticLocking = this.optimisticLocking;
            _.forEach(this.tables, function (t) {
                /**
                 * newT
                 * @type {DataTable}
                 */
                let newT = t.clone();
                newT.dataset = newDs;
                newDs.tables[newT.name] = newT;
                newDs.relationsByChild[newT.name] = [];
                newDs.relationsByParent[newT.name] = [];
            });
            _.forEach(this.relations, function (r) {
                newDs.newRelation(r.name, r.parentTable, r.parentCols, r.childTable, r.childCols);
            });
            return newDs;
        },

        /**
         * Creates a new DataTable attaching it to the DataSet
         * @method newTable
         * @param {string} tableName
         * @returns {DataTable}
         */
        newTable: function (tableName) {
            if (this.tables[tableName]) {
                throw ("Table " + tableName + " is already present in dataset");
            }
            let t = new DataTable(tableName);
            t.dataset = this;
            this.tables[tableName] = t;
            this.relationsByChild[tableName] = [];
            this.relationsByParent[tableName] = [];
            return t;
        },

        /**
         * Adds a datatable to DataSet
         * @method addTable
         * @param {DataTable} table
         * @returns {DataTable}
         */
        addTable: function (table) {
            let tableName= table.name;
            if (this.tables[tableName]) {
                throw ("Table " + tableName + " is already present in dataset");
            }
            if (table.dataset) {
                throw ("Table " + tableName + " already belongs to a dataset");
            }

            table.dataset = this;
            this.tables[tableName] = table;
            this.relationsByChild[tableName] = [];
            this.relationsByParent[tableName] = [];
            return table;
        },

        /**
         * Creates a copy of the DataSet, including both structure and data.
         * @method copy
         * @returns {DataSet}
         */
        copy: function () {
            let newDS = this.clone();
            _.forEach(this.tables, function (t) {
                let newT = newDS.tables[t.name];
                _.forEach(t.rows, function (r) {
                    newT.importRow(r);
                });
            });
            return newDS;
        },

        /**
         * Calls acceptChanges to all contained DataTables
         * @method acceptChanges
         */
        acceptChanges: function () {
            _.forEach(this.tables, function (t) {
                t.acceptChanges();
            });
        },

        /**
         * Calls rejectChanges to all contained DataTables
         * @method rejectChanges
         */
        rejectChanges: function () {
            _.forEach(this.tables, function (t) {
                t.rejectChanges();
            });
        },

        /**
         * Check if any contained DataTable has any changes
         * @method hasChanges
         * @returns {boolean}
         */
        hasChanges: function () {
            return _.some(this.tables, function (t) {
                return t.hasChanges();
            });
        },

        /**
         * Creates a new DataRelation and attaches it to the DataSet
         * @method newRelation
         * @param {string} relationName
         * @param {string} parentTableName
         * @param {string[]} parentColsName array of string
         * @param {string} childTableName
         * @param {string[]} childColsName array of string
         * @return {DataRelation}
         */
        newRelation: function (relationName, parentTableName, parentColsName, childTableName, childColsName) {
            if (this.relations[relationName]) {
                throw ("Relation " + relationName + " is already present in dataset");
            }
            if (this.tables[parentTableName] === undefined) {
                throw ("Parent table:" + parentTableName + " of relation " + relationName + " is not a dataSet table");
            }
            if (this.tables[childTableName] === undefined) {
                throw ("Child table:" + childTableName + " of relation " + relationName + " is not a dataSet table");
            }

            let rel = new DataRelation(relationName, parentTableName, parentColsName, childTableName, childColsName);
            rel.dataset = this;
            this.relations[relationName] = rel;

            if (!this.relationsByParent[parentTableName]) {
                this.relationsByParent[parentTableName] = [];
            }
            this.relationsByParent[parentTableName].push(rel);


            if (!this.relationsByChild[childTableName]) {
                this.relationsByChild[childTableName] = [];
            }
            this.relationsByChild[childTableName].push(rel);

            return rel;
        },
        /**
         * Deletes a row with all subentity child
         * @method cascadeDelete
         * @param {ObjectRow} row
         * @return {*}
         */
        cascadeDelete: function (row) {
            let r = row.getRow(),
                table = r.table,
                that = this;
            _.forEach(this.relationsByParent[table.name], function (rel) {
                if (rel.isEntityRelation()) {
                    _.forEach(rel.getChild(row), function (toDel) {
                        if (toDel.getRow().state !== DataRowState.deleted) {
                            that.cascadeDelete(toDel);
                        }
                    });
                }
                else {
                    _.forEach(rel.getChild(row), function (toUnlink) {
                            rel.makeChild(null, toUnlink);
                        }
                    );
                }
            });
            r.del();
        },

        /**
         * Creates a serializable version of this DataSet
         * @method serialize
         * @param {boolean} [serializeStructure=false] when true serialized also structure, when false only row data
         * @param {function} [filterRow] function to select which rows have to be serialized
         * @returns {object}
         */
        serialize: function (serializeStructure, filterRow) {
            let d = {},
                that = this;
            if (serializeStructure) {
                d.name = this.name;
                d.relations = {};
                _.forEach(_.keys(this.relations), function (relationName) {
                    d.relations[relationName] = that.relations[relationName].serialize();
                });
            }
            d.tables = {};
            _.forEach(_.keys(this.tables), function (tableName) {
                d.tables[tableName] = that.tables[tableName].serialize(serializeStructure, filterRow);
            });
            return d;
        },

        /**
         * Restores data from an object obtained with serialize().
         * @method deSerialize
         * @param {object} d
         * @param {boolean} deSerializeStructure
         */
        deSerialize: function (d, deSerializeStructure) {
            let that = this;
            if (deSerializeStructure) {
                this.name = d.name;
            }
            _.forEach(_.keys(d.tables), function (tableName) {
                let t = that.tables[tableName];
                if (t === undefined) {
                    t = that.newTable(tableName);
                }
                t.deSerialize(d.tables[tableName],deSerializeStructure);
            });
            if (deSerializeStructure) {
                _.forEach(_.keys(d.relations), function (relationName) {
                    let rel = d.relations[relationName],
                        newRel = that.newRelation(relationName, rel.parentTable, rel.parentCols, rel.childTable, rel.childCols);
                    newRel.deSerialize(rel);
                });
            }
        },

        /**
         * merges changes from DataSet d assuming they are unchanged and they can be present in this or not.
         * If a row is not present, it is added. If it is present, it is updated.
         * It is assumed that "this" dataset is unchanged at the beginning
         * @method mergeAsPut
         * @param {DataSet} d
         */
        mergeAsPut: function (d) {
            let that = this;
            _.forEach(d.tables, function (t) {
                that.tables[t.name].mergeAsPut(t);
            });
        },

        /**
         * merges changes from DataSet d assuming they are unchanged and they are not present in this dataset.
         * Rows are all added 'as is' to this, in the state of ADDED
         * It is assumed that "this" dataset is unchanged at the beginning
         * @method mergeAsPost
         * @param {DataSet} d
         */
        mergeAsPost: function (d) {
            let that = this;
            _.forEach(d.tables, function (t) {
                that.tables[t.name].mergeAsPost(t);
            });
        },

        /**
         * merges changes from DataSet d assuming they are unchanged and they are all present in this dataset.
         * Rows are updated, but only  fields actually present in d are modified. Other field are left unchanged.
         * It is assumed that "this" dataset is unchanged at the beginning
         * @method mergeAsPatch
         * @param {DataSet} d
         */
        mergeAsPatch: function (d) {
            let that = this;
            _.forEach(d.tables, function (t) {
                that.tables[t.name].mergeAsPatch(t);
            });
        },

        /**
         * merge any row present in dataset d. Rows are merged as unchanged if they are unchanged,
         *  otherwise their values are copied into existent dataset
         *  DataSet must have same table structure
         * @param d
         */
        merge: function (d) {
            let that = this;
            _.forEach(d.tables, function (t) {
                that.tables[t.name].merge(t);
            });
        },

        /**
         * Import data from a given dataset
         * @method importData
         * @param {DataSet}  d
         */
        importData: function (d) {
            this.mergeAsPost(d);
            this.acceptChanges();
        }
    };


    let jsDataSet = {
        dataRowState: DataRowState,
        dataRowVersion: DataRowVersion,
        DataColumn: DataColumn,
        DataRow: DataRow,
        DataTable: DataTable,
        DataSet: DataSet,
        toString: function () {
            return "dataSet Namespace";
        },
        CType:CType,
        OptimisticLocking: OptimisticLocking,
        myLoDash: _ //for testing purposes
    };


    // Some AMD build optimizers like r.js check for condition patterns like the following:
    //noinspection JSUnresolvedVariable
    if (typeof define === 'function' && typeof define.amd === 'object' && define.amd) {
        // Expose lodash to the global object when an AMD loader is present to avoid
        // errors in cases where lodash is loaded by a script tag and not intended
        // as an AMD module. See http://requirejs.org/docs/errors.html#mismatch for
        // more details.
        root.jsDataSet = jsDataSet;

        // Define as an anonymous module so, through path mapping, it can be
        // referenced as the "underscore" module.
        //noinspection JSUnresolvedFunction
        define(function () {
            return jsDataSet;
        });
    }
    // Check for `exports` after `define` in case a build optimizer adds an `exports` object.
    else if (freeExports && freeModule) {
        // Export for Node.js or RingoJS.
        if (moduleExports) {
            (freeModule.exports = jsDataSet).jsDataSet = jsDataSet;
        }
        // Export for Narwhal or Rhino -require.
        else {
            freeExports.jsDataSet = jsDataSet;
        }
    }
    else {
        // Export for a browser or Rhino.
        root.jsDataSet = jsDataSet;
    }
}).call(this,
    (typeof _ === 'undefined') ? require('lodash') : _,
    (typeof jsDataQuery === 'undefined') ? require('./jsDataQuery').jsDataQuery : jsDataQuery
);