ayd-hana-client-cloud
Version:
Official SAP HANA Node.js Driver
783 lines (723 loc) • 28.9 kB
JavaScript
'use strict';
var fs = require('fs');
var path = require('path');
var os = require('os');
var net = require('net');
const debug = require('debug')('@sap/hana-client:index');
debug.log = console.info.bind(console);
const name = 'index.js';
var majorVersion = process.versions.node.split('.')[0];
debug('Starting %s', name);
var extensions = {
'darwin': '.dylib',
'linux': '.so',
'win32': '.dll'
};
var db = null;
function isMusl() {
const output = require('child_process').spawnSync('ldd',['--version']).stderr
if(output && output.toString('utf8').indexOf('musl') > -1) {
return true;
}
return false;
}
var readPipe = null;
function refPipe() {
readPipe.ref();
}
function unrefPipe() {
readPipe.unref();
}
function setupPipe(pipeInfo) {
if (!db) return;
var ldb = db;
if (process.platform === 'win32') {
readPipe = net.createConnection(pipeInfo["IPCEndpoint"]);
} else {
readPipe = net.Socket({fd : pipeInfo["fd"],
readable : true});
}
readPipe.unref();
readPipe.on('data', function(data) {
ldb.__runCallbacks();
});
ldb.__setRefFunctions(refPipe, unrefPipe);
}
/* Parse internal binary result set format created by N-API execute method.
* Done this way since the performance of creating N-API objects for the result set
* is very poor due to the N-API interaface itself.
* See InternalResultSetBuilder comment in internal code for description of
* the binary format.
*
* Usage:
* var rs_parser = new InternalResultSetParser(rs_bin);
* // at this point rs_parser.numberOfRows, rs_parser.numberOfColumns,
* // rs_parser.columnNames and rs_parser.columnTableNames are all available
* rows = [];
* for(var i = 0; i < rs_parser.numberOfRows; i++) {
* rows[i] = [];
* for(var j = 0; j < rs_parser.numberOfColumns; j++) {
* rows[i][j] = rs_parser.readValue();
* }
* }
* rs_parser.verifyAtEnd();
*/
class InternalResultSetParser {
// initialize and read header
constructor(rs_bin) {
if(!Buffer.isBuffer(rs_bin)) {
this.throwError("rs_bin is not a Buffer in InternalResultSetParser constructor");
} else if(rs_bin.length < 16) {
this.throwError("rs_bin length of "+rs_bin.length+ " is too small in InternalResultSetParser constructor");
}
this.m_buffer = rs_bin;
this.m_curr_offset = 0;
this.m_in_array_value = false;
var length_in_buffer = this.privateReadUInt64();
//debug("length_in_buffer: " + (Number(length_in_buffer)/1024/1024) + "MB");
if(this.m_buffer.length != length_in_buffer) {
this.throwError("rs_bin length of "+this.m_buffer.length+" does not match internal length of "+length_in_buffer+" in InternalResultSetParser constructor");
}
this.numberOfRows = this.privateReadUInt32();
this.numberOfColumns = this.privateReadUInt32();
if(this.numberOfColumns < 1 || this.numberOfColumns > 65535) {
this.throwError("Invalid numberOfColumns of "+this.numberOfColumns+" in InternalResultSetParser constructor");
}
// read column names
this.columnNames = [];
for(var i = 0; i < this.numberOfColumns; i++) {
var col_name = this.readValue();
if(typeof col_name !== 'string') {
this.throwError("Invalid column name type of " + typeof col_name + " in InternalResultSetParser constructor");
}
this.columnNames.push(col_name);
}
// read column table names
this.columnTableNames = [];
for(var i = 0; i < this.numberOfColumns; i++) {
var col_table_name = this.readValue();
if(col_table_name !== null && typeof col_table_name !== 'string') {
this.throwError("Invalid read column table name type of " + typeof col_table_name + " in InternalResultSetParser constructor");
}
this.columnTableNames.push(col_table_name);
}
};
throwError(msg) {
var err = new Error("Error making result set Object: " + msg);
err.code = -20009;
err.sqlState = 'HY000';
throw(err);
}
// read the next value
readValue() {
const I_RS_NULL = 1;
const I_RS_BOOLEAN = 2;
const I_RS_INT32 = 3;
const I_RS_DOUBLE = 4;
const I_RS_STRING = 5;
const I_RS_BINARY = 6;
const I_RS_ARRAY = 7;
if(this.m_curr_offset >= this.m_buffer_length) {
this.throwError("InternalResultSetParser.readValue already at end of buffer");
}
var type = this.m_buffer.readInt8(this.m_curr_offset++);
switch(type) {
case I_RS_NULL:
return null;
case I_RS_BOOLEAN:
var val = this.m_buffer.readInt8(this.m_curr_offset++);
return val ? true : false;
case I_RS_INT32:
var num = this.m_buffer.readInt32LE(this.m_curr_offset);
this.m_curr_offset += 4;
return num;
case I_RS_DOUBLE:
var num = this.m_buffer.readDoubleLE(this.m_curr_offset);
this.m_curr_offset += 8;
return num;
case I_RS_STRING:
var len = this.privateReadUInt32();
var str = this.m_buffer.toString('utf8', this.m_curr_offset, this.m_curr_offset + len);
this.m_curr_offset += len;
return str;
case I_RS_BINARY:
var len = this.privateReadUInt32();
var bin = this.m_buffer.subarray(this.m_curr_offset, this.m_curr_offset + len);
this.m_curr_offset += len;
return bin;
case I_RS_ARRAY:
// read array header
var num_elements = this.m_buffer.readInt32LE(this.m_curr_offset);
this.m_curr_offset += 4;
if(this.m_in_array_value) {
this.throwError("InternalResultSetParser.readValue nested array value");
}
this.m_in_array_value = true;
var array = [];
// read array elements
for(var i = 0; i < num_elements; i++) {
array.push(this.readValue());
}
this.m_in_array_value = false;
return array;
};
this.throwError("InternalResultSetParser.readValue unexpected value type code of "+type);
}
// confirm that all data in the binary buffer was read
verifyAtEnd() {
if(this.m_curr_offset != this.m_buffer.length) {
this.throwError("Verify at end failed. m_curr_offset:" + this.m_curr_offset + " m_buffer.length:" + this.m_buffer.length);
}
}
// internal method used to implement InternalResultSetParser itself
privateReadUInt32() {
var val = this.m_buffer.readInt32LE(this.m_curr_offset);
this.m_curr_offset += 4;
return val;
}
privateReadUInt64() {
var val;
if(majorVersion >= 12) {
val = this.m_buffer.readBigUInt64LE(this.m_curr_offset);
this.m_curr_offset += 8;
} else {
// node 11 and below don't support Buffer.readBigUInt64LE
// (although some versions of node 10 do support it).
var low_val = this.privateReadUInt32();
var high_val = this.privateReadUInt32();
val = low_val + high_val * 0x100000000;
}
return val;
}
/* member data:
* numberOfRows: number of rows in result set
* numberOfColumns: number of columns in result set
* columnNames: array of strings of column names
* columnTableNames: array of (strings or NULL) of column table names
* member data that should be considered private:
* m_buffer: the entire binary buffer we are parsing
* m_curr_offset: current offset of m_buffer for next read
* m_in_array_value: true when elements remain in I_RS_ARRAY
*/
};
// convert rs_bin in InternalResultSet binary format to row array object
// returned by Statement::execute and Connection::execute
function translateFromInternalResultSet(options, rs_bin) {
var rowsAsArray = false;
var nestTables = false;
if(options !== null) {
if(options.rowsAsArray) {
rowsAsArray = true;
}
if(options.nestTables) {
nestTables = true;
}
}
var rs_parser = new InternalResultSetParser(rs_bin);
var column_names = rs_parser.columnNames;
var column_table_names = rs_parser.columnTableNames;
var rows = []; // array of all rows
var number_of_rows = rs_parser.numberOfRows;
var number_of_columns = rs_parser.numberOfColumns;
for(var i = 0; i < number_of_rows; i++) {
var row;
if(rowsAsArray) {
row = []; // array of column values
for(var j = 0; j < number_of_columns; j++) {
row.push(rs_parser.readValue());
}
} else {
row = {};
for(var j = 0; j < number_of_columns; j++) {
if(nestTables && column_table_names[j]) {
// there is a key for each table name and the value is
// an object with keys column name and values column value
if(row[column_table_names[j]] === undefined) {
row[column_table_names[j]] = {};
}
row[column_table_names[j]][column_names[j]] = rs_parser.readValue();
} else {
// each column value has key column name and value column value
row[column_names[j]] = rs_parser.readValue();
}
}
}
rows.push(row);
}
rs_parser.verifyAtEnd();
return rows;
}
// helper for StatementExec and ConnectionExec.
class ArgParser {
constructor(argv) {
this.argv = argv;
this.next_idx = 0;
}
// Return the next arg, skipping args that are null or undefined,
// return null if there is no more args
getNext() {
while(this.next_idx < this.argv.length) {
var next_arg = this.argv[this.next_idx++];
if(next_arg !== null && next_arg !== undefined) {
return next_arg;
}
}
return null;
}
// return what the next arg will be, but do NOT advance next_idx
peekNext() {
var save_next_idx = this.next_idx;
var peek_arg = this.getNext();
this.next_idx = save_next_idx;
return peek_arg;
}
// Helper for parseExecuteArgs to resolve ambiguity of first parameter being an
// object, which could be named bind parameters or could be options (yuck!)
// Return true if arg is bind parameters, false otherwise
isBindParameter(arg) {
if(arg === null) return false;
if(Array.isArray(arg)) {
// array can only be bind parameter
return true;
}
if(typeof(arg) !== 'object') {
// not array or object, cannot be either bind parameter or options
// (could be a function if it is a valid arg)
return false;
}
// the first arg is of type object, and could be either named bind parameters
// or options
var peek_arg = this.peekNext();
if(peek_arg !== null && typeof(peek_arg) === 'object') {
// arg after this must be options, so this must be named bind parameters
return true;
}
// still ambiguous, check if it seems to be an options object
if(arg.nestTables !== undefined ||
arg.rowsAsArray !== undefined ||
arg.queryTimeout !== undefined ||
arg.communicationTimeout !== undefined ||
arg.fastInternalRows !== undefined ||
arg.vectorOutputType !== undefined) {
// has a valid option key, so assume options
return false;
}
// didn't have any valid option keys, assume named bind parameters
return true;
}
// parse params, options and cbfunc args common to
// Statement.execute and Connection.execute
parseExecuteArgs() {
this.params = null;
this.options = null;
this.cbfunc = null;
var arg = this.getNext();
// params arg must be first if specified
if(this.isBindParameter(arg)) {
this.params = arg;
arg = this.getNext();
}
// options arg must be next if specified
if(arg !== null && typeof(arg) === 'object' && !Array.isArray(arg)) {
this.options = arg;
arg = this.getNext();
}
// callback arg must be last if specified
if(arg !== null && typeof(arg) === 'function') {
this.cbfunc = arg;
arg = this.getNext();
}
if(arg !== null) {
// The user passed arguments in the wrong order or the wrong types of args
// Even on invalid args, it is important to set cbfunc if one is
// provided so that the cbfunc is called with the error.
// Otherwise what was intended to be an async method will throw
// an error that the app probably won't expect, crashing the app.
while(arg !== null) {
if(typeof(arg) === 'function') {
this.cbfunc = arg;
break;
}
arg = this.getNext();
}
throw(new Error());
}
}
}
// A pure JavaScript shim that the user calls instead of an N-API Statement.exec[ute]
// (see dynamicallyAddStmtExecMethod).
// This calls Statement.execInternal and then can use pure JavaScript
// conversion of an intermediate result set to the JavaScript objects returned.
// This eliminates the need to create the result set JavaScript objects inside
// N-API (which can be very expensive).
// see also ConnectionExec which has very similar code.
function StatementExec(stmt, p1, p2, p3, execType) {
var args = new ArgParser([p1, p2, p3]);
var parseError;
try {
args.parseExecuteArgs();
} catch(e) {
var methodName;
switch (execType) {
case 2:
methodName = "exec[ute]Batch";
break;
case 1:
methodName = "exec[ute]Query";
break;
default:
case 0:
methodName = "exec[ute]";
break;
}
parseError = new Error("Invalid parameter for function '" + methodName + "([params][, options][, callback])'");
parseError.code = -20002;
parseError.sqlState = 'HY000';
}
var func;
switch (execType) {
case 2:
func = stmt.execBatchInternal;
break;
case 1:
func = stmt.execQueryInternal;
break;
case 0:
default:
func = stmt.execInternal;
break;
}
if(args.cbfunc) {
// async call, do result set conversion work after Statement.execInternal
// but before calling the callback function passed in.
if(parseError) {
args.cbfunc(parseError);
return;
}
func.call(stmt, args.params,
args.options,
function StatementExecCallback(e, rows) {
if(!e) {
// convert rows from InternalResultSet format
// (Note rows can be a Number if stmt does not return a result set)
if(Buffer.isBuffer(rows)) {
try {
rows = translateFromInternalResultSet(args.options, rows);
} catch(err) {
// The intermediate format of the result set could not
// be converted to the JavaScript result set.
e = err;
rows = null;
}
}
}
// call callback function passed in
if(arguments.length == 2) {
args.cbfunc(e, rows);
} else {
// execInternal only provided one parameter to this callback.
// For example, when there are no results due to DDL statement.
args.cbfunc(e);
}
});
} else {
// sync call, just call directly...
if(parseError) {
throw parseError;
}
var rows = func.call(stmt, args.params, args.options, null);
// convert rows from InternalResultSet format
// (Note rows can be a Number if stmt does not return a result set)
if(Buffer.isBuffer(rows)) {
rows = translateFromInternalResultSet(args.options, rows);
}
return rows;
}
}
// Add the exec pure JavaScript method into the stmt object.
// This is called by the N-API portion of the driver
// when creating each Statement object.
function dynamicallyAddStmtExecMethod(stmt) {
if(stmt.exec == null) {
stmt.exec = function(p1 = null, p2 = null, p3 = null) {
return StatementExec(this, p1, p2, p3, 0);
}
} // else case should never happen
if(stmt.execute == null) {
stmt.execute = function(p1 = null, p2 = null, p3 = null) {
return StatementExec(this, p1, p2, p3, 0);
}
} // else case should never happen
if(stmt.execQuery == null) {
stmt.execQuery = function(p1 = null, p2 = null, p3 = null) {
return StatementExec(this, p1, p2, p3, 1);
}
} // else case should never happen
if(stmt.executeQuery == null) {
stmt.executeQuery = function(p1 = null, p2 = null, p3 = null) {
return StatementExec(this, p1, p2, p3, 1);
}
} // else case should never happen
if(stmt.execBatch == null) {
stmt.execBatch = function(p1 = null, p2 = null, p3 = null) {
return StatementExec(this, p1, p2, p3, 2);
}
} // else case should never happen
if(stmt.executeBatch == null) {
stmt.executeBatch = function(p1 = null, p2 = null, p3 = null) {
return StatementExec(this, p1, p2, p3, 2);
}
} // else case should never happen
}
// Similar to StatementExec except that this is for Connection.exec
// and there is an extra sql parameter
// When the user calls Connection.exec[ute], this is called.
function ConnectionExec(conn, sql, p2, p3, p4) {
var args = new ArgParser([p2, p3, p4]);
var parseError;
try {
args.parseExecuteArgs();
} catch(e) {
parseError = new Error("Invalid parameter for function 'exec[ute](sql[, params][, options][, callback])'");
parseError.code = -20002;
parseError.sqlState = 'HY000';
}
if(args.cbfunc) {
// async call, do result set conversion work after Statement.execInternal
// but before calling the callback function passed in.
if(parseError) {
args.cbfunc(parseError);
return;
}
conn.execInternal(sql,
args.params,
args.options,
function ConnectionExecCallback(e, rows) {
if(!e) {
// convert rows from InternalResultSet format
// (Note rows can be a Number if sql does not return a result set)
if(Buffer.isBuffer(rows)) {
try {
rows = translateFromInternalResultSet(args.options, rows);
} catch(err) {
// The intermediate format of the result set could not
// be converted to the JavaScript result set.
e = err;
rows = null;
}
}
}
// call callback function passed in
if(arguments.length == 2) {
args.cbfunc(e, rows);
} else {
// execInternal only provided one parameter to this callback.
// For example, when there are no results due to DDL statement.
args.cbfunc(e);
}
});
} else {
// sync call, just call directly...
if(parseError) {
throw parseError;
}
var rows = conn.execInternal(sql, args.params, args.options, null);
// convert rows from InternalResultSet format
// (Note rows can be a Number if sql does not return a result set)
if(Buffer.isBuffer(rows)) {
rows = translateFromInternalResultSet(args.options, rows);
}
return rows;
}
}
// Add the exec pure JavaScript method into the conn object.
// This is called by the N-API portion of the driver
// when creating each Connection object.
function dynamicallyAddConnExecMethod(conn) {
if(conn.exec == null) {
conn.exec = function(sql, p2 = null, p3 = null, p4 = null) {
return ConnectionExec(this, sql, p2, p3, p4);
}
} // else case should never happen
if(conn.execute == null) {
conn.execute = function(sql, p2 = null, p3 = null, p4 = null) {
return ConnectionExec(this, sql, p2, p3, p4);
}
} // else case should never happen
}
process.on('exit', function() {
db.__stopAllWork();
});
debug('Detected os.arch=' + os.arch() + ', '
+ 'process.platform=' + process.platform + ', '
+ 'process.arch=' + process.arch + ', '
+ 'process.version=' + process.version );
if (process.platform === 'win32' && process.arch != 'x64') {
debug('ERROR: On Windows, this driver only supports the x64 architecture. ' +
'Your node process is: ' + process.arch );
throw new Error('On Windows, this driver only supports the x64 architecture. ' +
'Your node process is: ' + process.arch );
}
if (majorVersion < 8) {
throw new Error("Node version " + process.version + " is unsupported by @sap/hana-client. Only versions >=8 are supported");
}
// Look for prebuilt binary and DBCAPI based on platform
var pb_subdir = null;
var dbcapi_name = 'libdbcapiHDB';
if (process.platform === 'linux') {
if ( isMusl() ) {
pb_subdir = 'linuxx86_64_alpine-gcc6';
} else if (process.arch === 'x64') {
pb_subdir = 'linuxx86_64-gcc6';
} else if (process.arch.toLowerCase().indexOf('ppc') != -1 && os.endianness() === 'LE') {
pb_subdir = 'linuxppc64le-gcc6';
} else if (process.arch === 'arm64') {
pb_subdir = 'linuxaarch64-gcc9';
} else {
pb_subdir = 'unknown';
}
} else if (process.platform === 'win32') {
pb_subdir = 'ntamd64-msvc2022';
} else if (process.platform === 'darwin') {
if (process.arch === 'arm64') {
pb_subdir = 'darwinarm64-xcode12';
} else {
pb_subdir = 'darwinintel64-xcode7';
}
}
var dbcapi_env = 'DBCAPI_API_DLL';
var modpath = process.cwd();
var pb_path = path.join(modpath, 'prebuilt', pb_subdir);
if (pb_path.includes('app.asar')) {
pb_path = pb_path.replace('app.asar', 'app.asar.unpacked');
}
var dll_path = pb_path;
var dbcapi = path.join(dll_path, dbcapi_name + extensions[process.platform]);
// Remove unrelated native platform libraries
const removeDirectory = function (dir) {
if (fs.existsSync(dir)) {
fs.readdirSync(dir).forEach((file, index) => {
const curPath = path.join(dir, file);
if (fs.lstatSync(curPath).isDirectory()) { // recurse
removeDirectory(curPath);
} else { // delete file
fs.unlinkSync(curPath);
}
});
fs.rmdirSync(dir);
}
};
const getSubDirs = function (dir) {
const isDirectory = source => fs.lstatSync(source).isDirectory();
const getDirectories = source =>
fs.readdirSync(source).map(name => path.join(source, name)).filter(isDirectory);
return getDirectories(dir);
};
if (process.env['HDB_NODE_PLATFORM_CLEAN'] === '1') {
var pb_path_all = getSubDirs(path.join(modpath, 'prebuilt'));
pb_path_all.forEach((pbPath) => {
if (pbPath !== pb_path) {
try {
removeDirectory(pbPath);
} catch (ex) {
debug(ex.message);
debug("Failed to remove unrelated native platform libraries in '" + pbPath + "'.");
}
}
});
} else if (process.env['HDB_NODEJS_INSTALL_PLATFORMS']) {
var keep = process.env['HDB_NODEJS_INSTALL_PLATFORMS'].split(',').map(name => path.join(modpath, 'prebuilt', name));
var pb_path_all = getSubDirs(path.join(modpath, 'prebuilt'))
var pb_count = pb_path_all.length;
pb_path_all.forEach((pbPath) => {
if (keep.indexOf(pbPath) < 0) {
try {
removeDirectory(pbPath);
pb_count -= 1;
} catch (ex) {
debug(ex.message);
debug("Failed to remove unrelated native platform libraries in '" + pbPath + "'.");
}
}
});
if (pb_count <= 0) {
debug("WARNING: Environment variable HDB_NODEJS_INSTALL_PLATFORMS did not name any valid platforms. All native platform libraries have been removed.");
}
}
// Found dbcapi
// Now find driver
var default_driver_file = 'hana-client';
var driver_file = default_driver_file;
if (process.env['HDB_NODE_NO_NAPI_DRIVER'] === '1') {
// Check if there is a node-version-specific driver
// Fall back on hana-client.node
var v = process.version;
var match = v.match(/v([0-9]+)\.([0-9]+)\.[0-9]+/);
driver_file += '_v' + match[1];
if (match[1] + 0 == 0) {
driver_file += '_' + match[2];
}
}
var driver_path = path.join(pb_path, driver_file + '.node');
try {
debug("Checking for existence of "+driver_path);
fs.statSync(driver_path);
} catch (ex) {
debug("Did not find "+driver_path);
driver_path = path.join(pb_path, default_driver_file + '.node');
try {
debug("Checking for existence of "+driver_path);
fs.statSync(driver_path);
} catch (ex) {
debug("No prebuilt node driver found for platform: '" +
process.platform + "', arch: '" + process.arch +
"', endianness: '" + os.endianness() + "' for Node version: '" +
process.version + "'");
}
}
// Try loading
// 1. User's build
// 2. Prebuilt
debug('Attempting to load Hana node-hdbcapi driver');
var userbld_driver_path = path.join(modpath, 'build', 'Release', 'hana-client.node');
debug('... Trying user-built copy...');
try {
debug('... Looking for user-built copy in ' + userbld_driver_path + ' ... ');
fs.statSync(userbld_driver_path);
debug('Found.');
try {
debug('... Attempting to load user-built copy... ');
db = __non_webpack_require__(userbld_driver_path);
debug('Loaded.');
} catch (ex) {
debug(ex.message);
debug('Could not load: User-built copy did not satisfy requirements.');
throw ex;
}
} catch (ex) {
debug('Not found.');
}
if (db === null) {
debug('... Trying prebuilt copy...');
try {
debug('... Looking for prebuilt copy in ' + driver_path + ' ... ');
db = __non_webpack_require__(driver_path);
debug('Loaded.');
} catch (ex) {
debug(ex.message);
debug('Could not load: Prebuilt copy did not satisfy requirements.');
debug("Could not load modules for Platform: '" +
process.platform + "', Process Arch: '" + process.arch +
"', and Version: '" + process.version + "'");
throw ex;
}
}
db.__loadDbcapi(dbcapi);
var pipeInfo = db.__getPipe();
setupPipe(pipeInfo);
db.__setAddFnsThatAddMethodsToObjects(dynamicallyAddConnExecMethod,
dynamicallyAddStmtExecMethod);
if (db !== null) {
debug('Success.');
}
module.exports = db;