Source: src/edge-sql.js

/*globals require */
"use strict";
/*

 */

/**
 * @property defer
 * @type {function}
 */
const defer     = require("JQDeferred");
const _         = require('lodash');
const isolationLevels=['READ_UNCOMMITTED','READ_COMMITTED','REPEATABLE_READ','SNAPSHOT','SERIALIZABLE'];


module.exports = {
	isolationLevels: isolationLevels,
	objectify: simpleObjectify,
	EdgeConnection: EdgeConnection
};

/**
 * Simplified objectifier having an array of column names for first argument
 * @private
 * @param {Array} colNames
 * @param {Array} rows
 * @returns {Array}
 */
function simpleObjectify(colNames, rows) {
	var colLength = colNames.length,
		rowLength = rows.length,
		result = [],
		rowIndex = rowLength,
		colIndex,
		value,
		row;
	while (--rowIndex >= 0) {
		value = {};
		row = rows[rowIndex];
		colIndex = colLength;
		while (--colIndex >= 0) {
			value[colNames[colIndex]] = row[colIndex];
		}
		result[rowIndex] = value;
	}
	return result;
}

/**
 * Gets a  database connection
 * @param {string} connectionString
 * @param {string} driver  can be sqlServer or mySql
 * @constructor
 */
function EdgeConnection(connectionString, driver) {	
	this.sqlCompiler = 'db';
	this.edgeHandler = null;
	this.driver = driver||'mySql';
	this.connectionString = connectionString;
	this.defaultTimeout= 30;

	/*references:["MySqlData.dll",
					"System.Data.SqlClient.dll",
					"Microsoft.Extension.Configuration.dll",
					"System.Configuration.ConfigurationManager.dll"],*/
	//assemblyFile:"edge-db-fake.dll",
	//this.typeName="EdgeCompiler2";		//Startup
	//this.methodName="CompileFunc2";	//Invoke

}



/**
 * transforms row data into plain objects
 * @method simpleObjectifier
 * @private
 * @param {string[]} colNames
 * @param {object[]}  row
 * @returns {object}
 */
function simpleObjectifier(colNames, row) {
	let obj = {};
	_.each(colNames, function (value, index) {
		obj[value] = row[index];
	});
	return obj;
}



/**
 * Evaluates parameter to connect to database depending on the open/closed state of the connection.
 * If a connection is open, the handle of the open connection is used. Otherwise, a connectionString is provided.
 * @private
 * @returns {*}
 */
EdgeConnection.prototype.getDbConn = function () {
	if (this.edgeHandler !== null) {
		return {handler: this.edgeHandler, driver: this.driver};
	}
	return {connectionString: this.connectionString, driver: this.driver};
};


/**
 * Opens the physical connection
 * @public
 * @methodo open
 * @returns {promise}  Returns a promise that is resolved when connection is established
 */
EdgeConnection.prototype.open = function () {
	const def = defer(),
		that = this,
		edge      = require('edge-js'),
		/**
		 * Opens the phisical connection
		 * @method edgeOpenInternal
		 * @private
		 * @returns {promise}
		 */
		edgeOpenInternal = edge.func(this.sqlCompiler,
			{
				source: 'open',
				connectionString: this.connectionString,
				cmd: 'open',
				driver: this.driver,
				/*references:["MySqlData.dll",
					"System.Data.SqlClient.dll",
					"Microsoft.Extension.Configuration.dll",
					"System.Configuration.ConfigurationManager.dll"],*/
				//assemblyFile:"edge-db-fake.dll",
				//typeName:"EdgeCompiler",		//Startup
				//methodName:"CompileFunc"	//Invoke
			});
	edgeOpenInternal({}, function (error, result) {
		if (error) {
			//console.log(error);
			def.reject(error);
			return;
		}
		if (result) {
			//console.log("got handler "+result+" for "+that.connectionString);
			that.edgeHandler = result;
			def.resolve();
			return;
		}
		console.log("shouldn't reach here");
		def.reject("shouldn't reach here");
	});
	return def.promise();
};

/**
 * Closes the phisical connection
 * @method edgeClose
 * @returns {*}
 */
EdgeConnection.prototype.close = function () {
	let def = defer(),
		that = this,
		edge      = require('edge-js');

	if (this.edgeHandler===null) {
		//console.log("an EdgeConnection was found without an handler");
		def.resolve();
		return def;
	}
	//console.log("closing  handler "+this.edgeHandler+" for "+this.connectionString);
	let	edgeClose = edge.func(this.sqlCompiler,
			{
				handler: this.edgeHandler,
				source: 'close',
				cmd: 'close',
				driver: this.driver
			});
	
	edgeClose({}, function (error) {
		if (error) {
			//console.log("error closing handler "+that.edgeHandler+" for "+that.connectionString+":"+error);
			def.reject(error);
			return;
		}
		//console.log("closed handler "+that.edgeHandler+" for "+that.connectionString);
		that.edgeHandler = null;
		def.resolve();
	});
	return def.promise();
};

let nCommand =0;
/**
 * Executes a sql command and returns all sets of results. Each Results is given via a notify or resolve
 * @method queryBatch
 * @param {string} query
 * @param {boolean} [raw] if true, data are left in raw state and will be objectified by the client
 * @param {int} [timeout=  this.defaultTimeout]
 * @returns {defer}  a sequence of {[array of plain objects]} or {meta:[column names],rows:[arrays of raw data]}
 */
EdgeConnection.prototype.queryBatch = function (query, raw, timeout) {
	var edge      = require('edge-js'),
		edgeQuery = edge.func(this.sqlCompiler, _.assign({source: query,timeout:timeout||this.defaultTimeout}, this.getDbConn())),
		def = defer();
	//nextTick is necessary in order to run the sql function asyncronously, otherwise, cause of edge-js
	//  and mysql blocking nature, it starts notifying tables before the promise is actually returned
	//   causing data to be loss

	var that=this;
	process.nextTick(function () {
		//console.log("queryBatch "+query);
		nCommand++;
		let cc = nCommand;
		// if (that.edgeHandler === null) {
		// 	console.log("executing command  " + cc + "(" + that.edgeHandler + "):" + query);
		// }
		edgeQuery({}, function (error, result) {
			// console.log("receiving result  "+cc+"("+that.edgeHandler+")");
			// console.log(result);
			if (error) {
				def.reject(error + ' running ' + query);
				return def.promise();
			}
			if (typeof result ==='object' && typeof  result.error === 'string'){
				def.reject(result.error + ' running ' + query);
				return def.promise();
			}
			let i;
			for (i = 0; i < result.length - 1; i++) {
				if (raw) {
					def.notify(result[i]);
				}
				else {
					def.notify(simpleObjectify(result[i].meta, result[i].rows));
				}
			}
			if (raw) {
				def.resolve(result[i]);
			}
			else {
				def.resolve(simpleObjectify(result[i].meta, result[i].rows));
			}
		});
	});

	return def.promise();
};



/**
 * Executes a series of sql update/insert/delete commands
 * @method updateBatch
 * @param {string} query
 * @param {int} [timeout=  this.defaultTimeout]
 * @returns {*}
 */
EdgeConnection.prototype.updateBatch = function (query,timeout) {
	let  edge      = require('edge-js'),
		edgeQuery = edge.func(this.sqlCompiler, _.assign({source: query, cmd: 'nonquery',timeout:timeout||this.defaultTimeout},
		this.getDbConn())),
		def = defer();
	edgeQuery({}, function (error, result) {
		if (error) {
			def.reject(error);
			return;
		}
		def.resolve(result);
	});
	return def.promise();
};




/**
 * Gets a table and returns each SINGLE row by notification. Could eventually return more than a table indeed
 * For each table read emits a {meta:[column descriptors]} notification, and for each row of data emits a
 *   if raw= false: {row:object read from db}
 *   if raw= true: {row: [array of values read from db]}

 * @method queryLines
 * @param {string} query
 * @param {boolean} [raw=false]
 * @param {number} [timeout=this.defaultTimeout]
 * @returns {*}
 */
EdgeConnection.prototype.queryLines = function (query, raw,timeout) {
	let def = defer(),
		lastMeta,
		callback = function (data, extCallback) {
			if (data.resolve) {
				def.resolve();
				return {};
			}
			if (data.meta){
				lastMeta = data.meta;
				if (!data.rows) def.notify(data);
			}
			if (data.rows) {
				if (raw) {
					def.notify({row: data.rows[0]});
				} else {
					def.notify({row: simpleObjectifier(lastMeta, data.rows[0])});
				}
			}

			if (extCallback)extCallback();
			return {};
		},
		edge      = require('edge-js'),
		edgeQuery = edge.func(this.sqlCompiler,
			_.assign({source: query, callback: callback, packetSize: 1,timeout:timeout||this.defaultTimeout},
				this.getDbConn()));
	process.nextTick(function() {
		//console.log("queryLines "+query);
		edgeQuery({}, function (error, result) {
			if (error) {
				def.reject(error + ' running ' + query);
				return;
			}
			if (result.length === 0) {
				//def.resolve();
				return {};
			}
			return {};
		});
	});
	return def.promise();
};



/**
 * the "edgeQuery" function is written in c#, and executes a series of select.
 * If a callback is specified, data is returned separately as {meta} - {rows} - {meta} - {rows} .. notifications
 * in this case has sense the parameter packetSize to limit the number of rows returned in each {rows} packet
 * If a callback is not specified, data is returned as a series of {meta, rows} notifications
 * A field "set" is also attached to any packet in order to identify the result set
 * if raw==false and a table (array of plain objects) is returned, the "set" field is attached to that array
 */


/**
 * Gets data packets row at a time
 * @public
 * @method queryPackets
 * @param {string} query
 * @param {boolean} [raw=false]
 * @param {number} [packSize=0]
 * @param {number} [timeout=this.defaultTimeout]
 * @returns {*}
 */
EdgeConnection.prototype.queryPackets = function (query, raw, packSize, timeout) {
	let def = defer(),
		packetSize = packSize || 0,
		lastMeta,
		currentSet = -1,
		callback = function (data, extCallback) {
			if (data.error) {
				//console.log("got error "+data.error+" running "+query);
				def.reject(new Error(data.error+" running "+query));
				return {};
			}
			if (data.resolve) {
				def.resolve();
				return {};
			}
			//meta is received for any new table, eventually with data
			if (data.meta) {
				currentSet += 1;
			}
			data.set = currentSet;
			if (raw) {
				def.notify(data);
			}
			else {
				if (data.meta) { //meta when present is a list of column names
					lastMeta = data.meta;
				}
				if (data.rows) {
					def.notify({rows: simpleObjectify(lastMeta, data.rows), set: currentSet});
				}

			}
			if (extCallback) extCallback();
			return {};
		};
	let that = this;
	//nextTick is necessary in order to run the sql function asyncronously, otherwise, cause of edge-js
	//  and mysql blocking nature, it starts notifying tables before the promise is actually returned
	//   causing data to be loss
	process.nextTick(function () {
		//console.log("queryPackets "+query);
		let edge      = require('edge-js'),
			edgeQuery = edge.func(that.sqlCompiler, _.assign({source: query,
						callback: callback,
						timeout:timeout||that.defaultTimeout,
						packetSize: packetSize},
			that.getDbConn()));

		edgeQuery({}, function (error) {
			if (error) {
				//console.log("error "+error+" running "+query);
				def.reject(error + ' running ' + query);
				return def.promise();
			}
			//def.resolve();
		});
	});
	return def.promise();
};



/**
 * Runs a sql script, eventually composed of multiple blocks separed by GO lines
 * @public
 * @method run
 * @param {string} script
 * @param {number} [timeout=this.defaultTimeout]
 * @returns {*}
 */
EdgeConnection.prototype.run = function (script,timeout) {
	let os = require('os'),
		//noinspection JSUnresolvedVariable
		lines = script.split(os.EOL),
		blocks = [],
		curr = '',
		first = true,
		that = this,
		i,
		s;
	for (i = 0; i < lines.length; i++) {
		s = lines[i];
		if (s.trim().toUpperCase() === 'GO') {
			blocks.push(curr);
			curr = '';
			first = true;
			continue;
		}
		if (!first) {
			//noinspection JSUnresolvedVariable
			curr += os.EOL;
		}
		curr += s;
		first = false;
	}
	if (curr.trim() !== '') {
		blocks.push(curr);
	}


	let def = defer(),
		index = 0;


	function loopScript() {
		if (index === blocks.length) {
			def.resolve();
		} else {
			that.updateBatch(blocks[index],timeout||that.defaultTimeout)
			.done(function () {
				index += 1;
				loopScript();
			})
			.fail(function (err) {
				def.reject(err);
			});
		}
	}


	loopScript();

	return def.promise();
};