ayd-hana-client-cloud
Version:
Official SAP HANA Node.js Driver
748 lines (671 loc) • 23.6 kB
JavaScript
'use strict';
module.exports =
{
// Create an readable stream which returns object
createObjectStream: function (resultset) {
return new HanaRSStream(resultset, false);
},
// Create an readable stream which returns array
createArrayStream: function (resultset) {
return new HanaRSStream(resultset, true);
},
// Create a LOB stream
createLobStream: function (resultset, columnIndex, options) {
return new HanaLobStream(resultset, columnIndex, options);
},
// Create a LOB stream for a LOB output parameter
createParameterLobStream: function (statement, paramIndex, options) {
return new HanaParameterLobStream(statement, paramIndex, options);
},
// Create a statement which allows user to pass readable streams for input parameters
createStatement: function (connection, sql, callback) {
return new HanaStatement(connection, sql, callback);
},
// Create a statement for procedure call. After calling exec, output parameters
// (err, scalarParams, table1, table2...) will be passed to the callback
createProcStatement: function (connection, sql, callback) {
return new HanaProcStatement(connection, sql, callback);
}
};
var util = require('util');
var streamModule = require('stream');
var Stream = streamModule.Stream;
var Readable = streamModule.Readable;
// HanaRSStream is used by both createObjectStream and createArrayStream
function HanaRSStream(resultset, asArray) {
checkParameter('resultset', resultset)
Readable.call(this, { objectMode: true });
resultset.setAsArrayDefault(asArray);
this.resultset = resultset;
};
util.inherits(HanaRSStream, Readable);
HanaRSStream.prototype._read = function () {
var stream = this;
// There are 3 combinations of sync vs async next and getValues.
// When nextCanBlock is false, the stream is about 2x faster
// than without this optimization.
// This implementation might be simpler if promises were used.
if(this.resultset.nextCanBlock()) {
// async next
this.resultset.next(function (err, ret) {
if (err === undefined) {
if (ret) {
if(stream.resultset.getValuesCanBlock()) {
// async getValues
stream.resultset.getValues(function (err, ret) {
if (err === undefined) {
stream.push(ret);
} else {
stream.emit('error', err);
}
});
} else {
// sync getValues
try {
stream.push(stream.resultset.getValues());
} catch(err) {
stream.emit('error', err);
}
}
} else {
stream.push(null);
}
} else {
stream.emit('error', err);
}
});
} else {
// nextCanBlock returned false, use sync next and getValues.
try {
var ret = this.resultset.next();
if (ret) {
stream.push(stream.resultset.getValues());
} else {
stream.push(null);
}
} catch(err) {
stream.emit('error', err);
}
}
};
HanaRSStream.prototype._destroy = function () {
this.push(null);
};
// Lob stream
function HanaLobStream(resultset, columnIndex, options) {
checkParameter('resultset', resultset);
var opts = getLobStreamOptions(options);
Readable.call(this, opts);
this.resultset = resultset;
this.columnInfo = this.resultset.getColumnInfo();
this.columnIndex = columnIndex;
checkColumnIndex(this.columnInfo, columnIndex);
checkLobType(this.columnInfo, columnIndex);
this.isString = this.columnInfo[this.columnIndex].typeName === 'STRING';
this.offset = 0;
this.noPause = opts.noPause;
this.valueAsString = false;
if (typeof opts.valueAsString === "boolean") {
this.valueAsString = opts.valueAsString;
}
};
util.inherits(HanaLobStream, Readable);
HanaLobStream.prototype._read = function (bufferSize) {
if (this.resultset.isNull(this.columnIndex)) {
this.push(null);
return;
}
var stream = this;
stream.bufferSize = bufferSize;
var buffer = createBuffer(bufferSize);
if (!this.noPause) {
this.pause();
}
this.resultset.getData(this.columnIndex, this.offset, buffer, 0, bufferSize, function (err, bytesRetrieved) {
if (err === undefined) {
if (stream.isString) {
if (bytesRetrieved > 0) {
let nullChar = buffer.indexOf(0);
if (nullChar === 0) {
stream.push(null);
} else {
let newBuffer = buffer;
if (nullChar > 0) {
newBuffer = buffer.slice(0, nullChar);
}
let str = newBuffer.toString('utf8');
stream.offset += str.length;
if (stream.valueAsString) {
stream.push(str);
} else {
stream.push(newBuffer);
}
// +3 because SQLDBC does not copy partial characters,
// and the largest partial UTF-8 character is 3 (since
// the largest UTF-8 character is 4 bytes)
if (bytesRetrieved + 3 < stream.bufferSize) {
stream.push(null);
}
}
} else {
stream.push(null);
}
} else { // LOB
stream.offset += bytesRetrieved;
if (bytesRetrieved > 0) {
if (bytesRetrieved < stream.bufferSize) {
stream.push(buffer.slice(0, bytesRetrieved));
stream.push(null);
} else {
stream.push(buffer);
}
} else {
stream.push(null);
}
}
} else {
if (err.code === 100) { // No more result
stream.push(null);
} else {
stream.emit('error', err);
}
}
if (!stream.noPause) {
stream.resume();
}
});
};
HanaLobStream.prototype._destroy = function () {
this.push(null);
};
// Parameter lob stream
function HanaParameterLobStream(statement, paramIndex, options) {
checkParameter('statement', statement);
var opts = getLobStreamOptions(options);
Readable.call(this, opts);
this.statement = statement;
this.paramInfo = this.statement.getParameterInfo();
this.paramIndex = paramIndex;
checkParameterIndex(this.paramInfo, paramIndex);
checkParameterLobType(this.paramInfo, paramIndex);
this.isString = this.paramInfo[this.paramIndex].typeName === 'STRING';
this.offset = 0;
this.noPause = opts.noPause;
this.valueAsString = false;
if (typeof opts.valueAsString === "boolean") {
this.valueAsString = opts.valueAsString;
}
};
util.inherits(HanaParameterLobStream, Readable);
HanaParameterLobStream.prototype._read = function (bufferSize) {
if (this.statement.isParameterNull(this.paramIndex)) {
this.push(null);
return;
}
var stream = this;
stream.bufferSize = bufferSize;
var buffer = createBuffer(bufferSize);
if (!this.noPause) {
this.pause();
}
this.statement.getData(this.paramIndex, this.offset, buffer, 0, bufferSize, function (err, bytesRetrieved) {
if (err === undefined) {
if (stream.isString) {
if (bytesRetrieved > 0) {
let nullChar = buffer.indexOf(0);
if (nullChar === 0) {
stream.push(null);
} else {
let newBuffer = buffer;
if (nullChar > 0) {
newBuffer = buffer.slice(0, nullChar);
}
let str = newBuffer.toString('utf8');
stream.offset += str.length;
if (stream.valueAsString) {
stream.push(str);
} else {
stream.push(newBuffer);
}
if (bytesRetrieved < stream.bufferSize) {
stream.push(null);
}
}
} else {
stream.push(null);
}
} else { // LOB
stream.offset += bytesRetrieved;
if (bytesRetrieved > 0) {
if (bytesRetrieved < stream.bufferSize) {
stream.push(buffer.slice(0, bytesRetrieved));
stream.push(null);
} else {
stream.push(buffer);
}
} else {
stream.push(null);
}
}
} else {
if (err.code === 100) { // No more result
stream.push(null);
} else {
stream.emit('error', err);
}
}
if (!stream.noPause) {
stream.resume();
}
});
};
HanaParameterLobStream.prototype._destroy = function () {
this.push(null);
};
// Statement which allows user to pass readable streams for input parameters
function HanaStatement(connection, sql, callback) {
if (connection === undefined || connection === null) {
handleError("Invalid parameter 'connection'.", callback);
return;
}
if (sql === undefined || sql === null) {
handleError("Invalid parameter 'sql'.", callback);
return;
}
this.connection = connection;
this.sql = sql;
this.stmt = null;
this.result = 0;
if (callback) {
var hanaStmt = this;
this.connection.prepare(sql, function (err, stmt) {
hanaStmt.stmt = stmt;
callback(err, hanaStmt);
});
} else {
this.stmt = this.connection.prepare(sql);
}
};
HanaStatement.prototype.getStatement = function () {
return this.stmt;
};
HanaStatement.prototype.drop = function (callback) {
if (this.stmt) {
var stmt = this.stmt;
this.stmt = null;
stmt.drop(callback);
}
};
HanaStatement.prototype.exec = function (params, callback) {
if (callback === undefined || callback === null || (callback instanceof Function) === false) {
throw new Error("Invalid parameter 'callback'.");
}
if (params === undefined || params === null || !(params instanceof Array)) {
handleError("Invalid parameter 'params'.", callback);
return;
}
if (!this.stmt) {
handleError("Invalid statement.", callback);
return;
}
var hasArrayParam = false;
var hasNonArrayParam = false;
for (var i = 0; i < params.length; i++) {
if (params[i] instanceof Array) {
hasArrayParam = true;
} else {
hasNonArrayParam = true;
}
if (hasArrayParam && hasNonArrayParam) {
handleError("Invalid parameter 'params': contains both array and non-array parameters.", callback);
return;
}
}
if (params.length === 0) {
callback(null, 0);
return;
}
this.callback = callback;
this.currentRow = 0;
if (hasArrayParam) {
this.paramsArray = params;
} else {
this.paramsArray = [];
this.paramsArray.push(params);
}
this._exec();
}
HanaStatement.prototype._exec = function () {
this.params = this.paramsArray[this.currentRow];
var isStream = [];
var paramsNew = [];
var streamCount = 0;
var hanaStmt = this;
for (var i = 0; i < this.params.length; i++) {
if (this.params[i] instanceof Stream) {
this.params[i].pause();
isStream.push(true);
paramsNew.push({sendParameterData : true});
streamCount++;
} else {
isStream.push(false);
paramsNew.push(this.params[i]);
}
}
if (streamCount <= 0) {
this.stmt.exec(paramsNew, function (err, result) {
if (err) {
hanaStmt.callback(err, result);
} else {
hanaStmt.currentRow++;
hanaStmt.result += result;
if (hanaStmt.currentRow === hanaStmt.paramsArray.length) {
hanaStmt.callback(null, hanaStmt.result);
} else {
hanaStmt._exec();
}
}
});
} else {
this.stmt.exec(paramsNew, function (err, result) {
if (err) {
hanaStmt.callback(err, result);
} else {
for (var paramIndex = 0; paramIndex < hanaStmt.params.length; paramIndex++) {
if (hanaStmt.params[paramIndex] instanceof Stream) {
break;
}
}
hanaStmt.sendParameterData(paramIndex);
}
});
}
};
HanaStatement.prototype.sendParameterData = function (paramIndex) {
var hanaStmt = this;
this.params[paramIndex].on('error', function (error) {
hanaStmt.callback(error);
});
this.params[paramIndex].on('end', function () {
hanaStmt.stmt.sendParameterData(paramIndex, null, function (err) {
if (err) {
hanaStmt.callback(err);
} else {
var nextStream = -1;
for (var i = paramIndex + 1; i < hanaStmt.params.length; i++) {
if (hanaStmt.params[i] instanceof Stream) {
nextStream = i;
break;
}
}
if (nextStream >= 0) {
hanaStmt.sendParameterData(nextStream);
} else {
hanaStmt.currentRow++;
hanaStmt.result += 1;
if (hanaStmt.currentRow === hanaStmt.paramsArray.length) {
hanaStmt.callback(null, hanaStmt.result);
} else {
hanaStmt._exec();
}
}
}
});
});
this.params[paramIndex].on('data', function (buffer) {
hanaStmt.params[paramIndex].pause();
hanaStmt.stmt.sendParameterData(paramIndex, buffer, function (err) {
if (err) {
hanaStmt.callback(err);
} else {
hanaStmt.params[paramIndex].resume();
}
});
});
this.params[paramIndex].resume();
};
// Statement for procedure call
function HanaProcStatement(connection, sql, callback) {
if (connection === undefined || connection === null) {
handleError("Invalid parameter 'connection'.", callback);
return;
}
if (sql === undefined || sql === null) {
handleError("Invalid parameter 'sql'.", callback);
return;
}
if (callback !== undefined && callback !== null && !(callback instanceof Function)) {
throw new Error("Invalid parameter 'callback'.");
}
this.connection = connection;
this.columnInfo = [];
if (callback) {
var hanaStmt = this;
connection.prepare(sql, function (err, stmt) {
hanaStmt.stmt = stmt;
callback(err, hanaStmt);
});
} else {
this.stmt = connection.prepare(sql);
return this;
}
};
HanaProcStatement.prototype.drop = function (callback) {
if (this.stmt) {
var stmt = this.stmt;
this.stmt = null;
stmt.drop(callback);
}
};
HanaProcStatement.prototype.exec = function (params, callback) {
if (params !== undefined && params !== null &&
(!(params instanceof Array) && !(params instanceof Object))) {
handleError("Invalid parameter 'params'.", callback);
return;
}
if (callback === undefined || callback === null || !(callback instanceof Function)) {
throw new Error("Invalid parameter 'callback'.");
}
if (!this.stmt) {
handleError("Invalid statement.", callback);
return;
}
this.callback = callback;
this.scalarParams = {};
this.hasScalarParams = false;
this.tableParams = [[]];
this.currentTable = 0;
this.columnInfo = [];
var hanaStmt = this;
this.stmt.execQuery(params, function (err, rs) {
if (err) {
hanaStmt.callback(err);
} else {
hanaStmt.rs = rs;
var paramInfo = hanaStmt.stmt.getParameterInfo();
var paramCount = 0;
for (var i = 0; i < paramInfo.length; i++) {
if (paramInfo[i].direction === 2 || paramInfo[i].direction === 3) {
hanaStmt.hasScalarParams = true;
hanaStmt.stmt.getParameterValue(i, (function() {
var index = i;
return function(err, val) {
if(err) {
hanaStmt.callback(err);
} else {
++paramCount;
hanaStmt.scalarParams[paramInfo[index].name] = val;
if (paramCount === paramInfo.length) {
return hanaStmt.fetchTableParam();
}
}
};
})() );
} else {
++paramCount;
}
}
if (paramCount === paramInfo.length) {
hanaStmt.fetchTableParam();
}
}
});
}
// Returns an array of arrays. Each array contains column info for each output table.
HanaProcStatement.prototype.getColumnInfo = function () {
return this.columnInfo;
};
HanaProcStatement.prototype.execute = function (params, callback) {
this.exec(params, callback);
}
HanaProcStatement.prototype.fetchTableParam = function () {
function fetchNextResult(hanaStmt) {
var columns = hanaStmt.rs.getColumnInfo();
if ((columns != null) && (columns != undefined) && (columns.length > 0)) {
hanaStmt.columnInfo.push(columns);
}
hanaStmt.rs.nextResult(function (err, hasResult) {
if (err) {
hanaStmt.callback(err);
} else if (hasResult) {
hanaStmt.currentTable++;
hanaStmt.tableParams.push([]);
hanaStmt.fetchTableParam();
} else {
hanaStmt.rs.close(function (err) {
if (err) {
hanaStmt.callback(err);
} else {
var args = [];
if (hanaStmt.hasScalarParams) {
args.push(hanaStmt.scalarParams);
} else {
args.push({});
}
for (var i = 0; i < hanaStmt.tableParams.length; i++) {
args.push(hanaStmt.tableParams[i]);
}
var evalStr = 'hanaStmt.callback(null';
for (var j = 0; j < args.length; j++) {
evalStr += ', args[' + j + ']';
}
evalStr += ')';
eval(evalStr);
}
});
}
});
}
var hanaStmt = this;
if (this.rs.nextCanBlock()) {
// async next
this.rs.next(function (err, hasData) {
if (err) {
hanaStmt.callback(err);
} else {
if (hasData) {
try {
hanaStmt.tableParams[hanaStmt.currentTable].push(hanaStmt.rs.getValues());
} catch (newerr) {
hanaStmt.callback(newerr);
return;
}
hanaStmt.fetchTableParam();
} else {
fetchNextResult(hanaStmt);
}
}
});
} else {
// nextCanBlock returned false, use sync next.
var hasDataSync = false;
try {
hasDataSync = this.rs.next();
if (hasDataSync) {
hanaStmt.tableParams[hanaStmt.currentTable].push(hanaStmt.rs.getValues());
}
} catch (newerr) {
hanaStmt.callback(newerr);
return;
}
if (hasDataSync) {
hanaStmt.fetchTableParam();
} else {
fetchNextResult(hanaStmt);
}
}
}
function handleError(err, callback) {
if (callback) {
callback(err);
} else {
throw err;
}
};
function isLob(type) {
if (type === 25 || // CLOB
type === 26 || // NCLOB
type === 27 || // BLOB
type === 13) { // VARBINARY
return true;
}
return false;
};
function checkParameter(name, param) {
if (param === undefined || param === null) {
throw new Error("Invalid parameter '" + name + "'.");
}
};
function checkColumnIndex(columnInfo, columnIndex) {
if (columnIndex === undefined || columnIndex === null ||
columnIndex < 0 || columnIndex >= columnInfo.length) {
throw new Error("Invalid parameter 'columnIndex'.");
}
};
function checkLobType(columnInfo, columnIndex) {
var type = columnInfo[columnIndex].nativeType;
if (!isLob(type)) {
throw new Error('Column is not LOB type.');
}
};
function checkParameterIndex(paramInfo, paramIndex) {
if (paramIndex === undefined || paramIndex === null ||
paramIndex < 0 || paramIndex >= paramInfo.length) {
throw new Error("Invalid parameter 'paramIndex'.");
}
};
function checkParameterLobType(paramInfo, paramIndex) {
var type = paramInfo[paramIndex].nativeType;
if (!isLob(type)) {
throw new Error('Parameter is not LOB type.');
}
};
function createBuffer(size) {
if (typeof Buffer.alloc === 'function') {
return Buffer.alloc(size);
} else {
return new Buffer(size);
}
};
function isInteger(value) {
if (typeof value === 'number' && value % 1 === 0) {
return true;
} else {
return false;
}
}
function getLobStreamOptions(options) {
var opts = {};
Object.assign(opts, options);
opts.objectMode = false;
if (isInteger(opts.readSize) && opts.readSize > 0) {
opts.highWaterMark = opts.readSize;
}
opts.noPause = false;
if (options && 'noPause' in options && (typeof options.noPause === 'boolean')) {
opts.noPause = options.noPause;
}
return opts;
}
var MAX_READ_SIZE = Math.pow(2, 18);
var DEFAULT_READ_SIZE = Math.pow(2, 11) * 100;