hdb
Version:
SAP HANA Database Client for Node
830 lines (748 loc) • 25.1 kB
JavaScript
// Copyright 2013 SAP AG.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http: //www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
// either express or implied. See the License for the specific
// language governing permissions and limitations under the License.
'use strict';
var EventEmitter = require('events').EventEmitter;
var tcp = require('./tcp');
var auth = require('./auth');
var util = require('../util');
var ClientInfo = require('./ClientInfo');
var Transaction = require('./Transaction');
var MessageBuffer = require('./MessageBuffer');
var common = require('./common');
var request = require('./request');
var reply = require('./reply');
const compressor = require('./Compressor');
var createExecuteTask = require('./ExecuteTask').create;
var ReplySegment = reply.Segment;
var part = require('./part');
const DataFormatVersion = require('./common/DataFormatVersion');
var MessageType = common.MessageType;
var MessageTypeName = common.MessageTypeName;
var SegmentKind = common.SegmentKind;
var ErrorLevel = common.ErrorLevel;
var PartKind = common.PartKind;
var bignum = util.bignum;
var debug = util.debuglog('hdb');
var trace = util.tracelog();
var EMPTY_BUFFER = common.EMPTY_BUFFER;
const DEFAULT_PACKET_SIZE = common.DEFAULT_PACKET_SIZE;
const MINIMUM_PACKET_SIZE = common.MINIMUM_PACKET_SIZE;
const MAXIMUM_PACKET_SIZE = common.MAXIMUM_PACKET_SIZE;
const PACKET_HEADER_LENGTH = common.PACKET_HEADER_LENGTH;
const SEGMENT_HEADER_LENGTH = common.SEGMENT_HEADER_LENGTH;
const PART_HEADER_LENGTH = common.PART_HEADER_LENGTH;
const DEFAULT_SPATIAL_TYPES = common.DEFAULT_SPATIAL_TYPES;
const MIN_COMPRESS_PKT_LEN = common.MIN_COMPRESS_PKT_LEN;
module.exports = Connection;
util.inherits(Connection, EventEmitter);
function Connection(settings) {
EventEmitter.call(this);
var self = this;
// public
this.connectOptions = new part.ConnectOptions();
this.clientContextOptions = new part.ClientContextOptions();
this.protocolVersion = undefined;
// private
this._clientInfo = new ClientInfo();
for(var key in settings) {
if(key.toUpperCase().startsWith("SESSIONVARIABLE:")) {
var sv_key = key.substring(key.indexOf(":") + 1);
var sv_value = settings[key];
if(sv_key && sv_key.length > 0 && sv_value && sv_value.length > 0) {
this._clientInfo.setProperty(sv_key, sv_value);
}
delete settings[key];
}
}
this._settings = settings || {};
this._socket = undefined;
this._queue = new util.Queue().pause();
this._state = new ConnectionState();
this._statementContext = undefined;
this._transaction = new Transaction();
this._transaction.once('error', function onerror(err) {
self.destroy(err);
});
this._initialHost = undefined;
this._initialPort = undefined;
this._redirectHost = undefined;
this._redirectPort = undefined;
this.host = this._settings['host'];
this.port = this._settings['port'];
this._redirectType = common.RedirectType.REDIRECTION_NONE;
this._compressionEnabled = false;
}
Connection.create = function createConnection(settings) {
return new Connection(settings);
};
Object.defineProperties(Connection.prototype, {
clientId: {
get: function getClientId() {
return this._settings.clientId || util.cid;
}
},
autoCommit: {
get: function getAutoCommit() {
return this._transaction.autoCommit;
},
set: function setAutoCommit(autoCommit) {
this._transaction.setAutoCommit(autoCommit);
}
},
holdCursorsOverCommit: {
get: function getHoldCursorsOverCommit() {
return !!this._settings.holdCursorsOverCommit;
},
set: function setHoldCursorsOverCommit(holdCursorsOverCommit) {
this._settings.holdCursorsOverCommit = holdCursorsOverCommit;
}
},
scrollableCursor: {
get: function getScrollableCursor() {
return !!this._settings.scrollableCursor;
},
set: function setScrollableCursor(scrollableCursor) {
this._settings.scrollableCursor = scrollableCursor;
}
},
initializationTimeout: {
get: function getInitializationTimeout() {
return this._settings.initializationTimeout || 5000;
}
},
readyState: {
get: function getReadyState() {
// no state ==> socket must be closed
if (!this._state) {
return 'closed';
}
// no socket ==> connectListener callback in open has not been called
if (!this._socket) {
return 'new';
}
// socket is closed ==> socket ended but not closed
if (this._socket.readyState !== 'open') {
return 'closed';
}
// no protocol version ==> open not yet finished
if (!this.protocolVersion) {
return 'opening';
}
switch (this._state.messageType) {
case MessageType.AUTHENTICATE:
case MessageType.CONNECT:
return 'connecting';
case MessageType.DISCONNECT:
return 'disconnecting';
default:
// do nothing
}
if (this._state.sessionId === -1) {
return 'disconnected';
}
return 'connected';
}
},
useCesu8: {
get: function shouldUseCesu8() {
return (this._settings.useCesu8 === true);
}
},
packetSize: {
get: function getPacketSize() {
let size = this._settings.packetSize || DEFAULT_PACKET_SIZE;
size = Math.min(size, MAXIMUM_PACKET_SIZE);
size = Math.max(size, MINIMUM_PACKET_SIZE);
return size;
}
},
packetSizeLimit: {
get: function getPacketSizeLimit() {
let limit = this._settings.packetSizeLimit || this.packetSize;
limit = Math.min(limit, MAXIMUM_PACKET_SIZE);
limit = Math.max(limit, this.packetSize);
return limit;
}
},
spatialTypes: {
get: function getSpatialTypes() {
return this._settings.spatialTypes || DEFAULT_SPATIAL_TYPES;
}
}
});
Connection.prototype.open = function open(options, cb) {
var self = this;
var timeoutObject = null;
function invalidInitializationReply() {
var err = new Error('Invalid initialization reply');
err.code = 'EHDBINIT';
return err;
}
function initializationTimeoutError() {
var seconds = Math.round(self.initializationTimeout / 1000);
var err = new Error('No initialization reply received within ' + seconds + ' sec');
err.code = 'EHDBTIMEOUT';
return err;
}
function cleanup() {
clearTimeout(timeoutObject);
self._socket.removeListener('error', onerror);
self._socket.removeListener('data', ondata);
}
function onerror(err) {
cleanup();
self._socket.destroy();
cb(err);
}
function ondata(chunk) {
cleanup();
if (!chunk || chunk.length < InitializationReply.LENGTH) {
return cb(invalidInitializationReply());
}
var reply = InitializationReply.read(chunk, 0);
self.protocolVersion = reply.protocolVersion;
self._addListeners(self._socket);
self.emit('open');
cb();
}
self._connect(options, function connectListener(err, socket){
if (err) { //if there is an error, there will not be a socket
cb(err);
return;
}
self.host = options['host'];
self.port = options['port'];
self._socket = socket;
self._socket.once('error', onerror);
self._socket.on('data', ondata);
timeoutObject = setTimeout(onerror, self.initializationTimeout, initializationTimeoutError());
self._socket.write(initializationRequestBuffer);
});
};
Connection.prototype._connect = tcp.connect;
Connection.prototype._addListeners = function _addListeners(socket) {
var self = this;
var packet = new MessageBuffer();
function cleanup() {
socket.removeListener('error', onerror);
socket.removeListener('data', ondata);
socket.removeListener('close', onclose);
}
// register listerners on socket
function ondata(chunk) {
packet.push(chunk);
if (packet.isReady()) {
if (self._state.sessionId !== packet.header.sessionId) {
self._state.sessionId = packet.header.sessionId;
self._state.packetCount = -1;
}
var buffer = packet.getData();
if(self._compressionEnabled && compressor.isPacketCompressed(packet.header.packetOptions)) {
buffer = compressor.decompress(buffer, packet.header.compressionVarpartLength);
}
packet.clear();
var cb = self._state.receive;
self._state.receive = undefined;
self._state.messageType = undefined;
self.receive(buffer, cb);
}
}
socket.on('data', ondata);
function onerror(err) {
var cb = self._state && self._state.receive;
if (cb) {
self._state.receive = null; // a callback should be called only once
cb(err);
} else if (self.listeners('error').length) {
self.emit('error', err);
} else {
debug('onerror', err);
}
}
socket.on('error', onerror);
function onclose(hadError) {
cleanup();
self._cleanup();
self.emit('close', hadError);
}
socket.on('close', onclose);
function onend() {
var err = new Error('Connection closed by server');
err.code = 'EHDBCLOSE';
self._clearQueue(err);
onerror(err);
}
socket.on('end', onend);
};
Connection.prototype._cleanup = function _cleanup() {
this._socket = undefined;
this._state = undefined;
// Connection is closed, outstanding tasks must fail
var err = new Error('Connection closed');
err.code = 'EHDBCLOSE';
this._clearQueue(err);
};
Connection.prototype._clearQueue = function _clearQueue(err) {
if (this._queue) {
this._queue.abort(err);
this._queue = undefined;
}
};
Connection.prototype.send = function send(message, receive) {
if (this._statementContext) {
message.unshift(PartKind.STATEMENT_CONTEXT, this._statementContext.getOptions());
}
if (this._clientInfo.shouldSend(message.type)) {
message.add(PartKind.CLIENT_INFO, this._clientInfo.getUpdatedProperties());
}
debug('send', message);
trace('REQUEST', message);
let size = this.packetSizeLimit - PACKET_HEADER_LENGTH;
let buffer = message.toBuffer(size);
if(buffer.length > size) {
return receive(new Error('Packet size limit exceeded'));
}
let packet = Buffer.alloc(PACKET_HEADER_LENGTH + buffer.length);
buffer.copy(packet, PACKET_HEADER_LENGTH);
let state = this._state;
state.messageType = message.type;
state.receive = receive;
// Increase packet count
state.packetCount++;
// Session identifier
bignum.writeUInt64LE(packet, state.sessionId, 0);
// Packet sequence number in this session
// Packets with the same sequence number belong to one request / reply pair
packet.writeUInt32LE(state.packetCount, 8);
// Used space in this packet
packet.writeUInt32LE(buffer.length, 12);
// Total space in this packet
packet.writeUInt32LE(size, 16);
// Number of segments in this packet
packet.writeUInt16LE(1, 20);
// Filler
packet.fill(0x00, 22, PACKET_HEADER_LENGTH);
if (this._socket) {
if (message.type != common.MessageType.AUTHENTICATE &&
message.type != common.MessageType.CONNECT &&
this._compressionEnabled &&
packet.length > MIN_COMPRESS_PKT_LEN) {
// Only use compressed packet if it actually was compressed
const compressedPacket = compressor.compress(packet);
if (compressedPacket.length < packet.length) {
this._socket.write(compressedPacket);
} else {
this._socket.write(packet);
}
} else {
this._socket.write(packet);
}
}
};
Connection.prototype.getClientInfo = function getClientInfo() {
return this._clientInfo;
};
Connection.prototype.setStatementContext = function setStatementContext(options) {
if (options && options.length) {
if (!this._statementContext) {
this._statementContext = new part.StatementContext();
}
this._statementContext.setOptions(options);
} else {
this._statementContext = undefined;
}
};
Connection.prototype.getAvailableSize = function getAvailableSize(forLobs = false) {
var totalSize = forLobs ? this.packetSize : this.packetSizeLimit;
var availableSize = totalSize - PACKET_HEADER_LENGTH - SEGMENT_HEADER_LENGTH - PART_HEADER_LENGTH;
if (this._statementContext) {
availableSize -= this._statementContext.size;
}
return availableSize;
};
Connection.prototype.setTransactionFlags = function setTransactionFlags(flags) {
if (flags) {
this._transaction.setFlags(flags);
}
};
Connection.prototype._parseReplySegment = function _parseReplySegment(buffer) {
var segment = ReplySegment.create(buffer, 0);
trace(segment.kind === SegmentKind.ERROR ? 'ERROR' : 'REPLY', segment);
return segment.getReply();
};
Connection.prototype.receive = function receive(buffer, cb) {
var error, reply;
try {
reply = this._parseReplySegment(buffer);
this.setStatementContext(reply.statementContext);
this.setTransactionFlags(reply.transactionFlags);
if (reply.kind === SegmentKind.ERROR && util.isObject(reply.error)) {
if (reply.error.level === ErrorLevel.WARNING) {
this.emit('warning', reply.error);
} else {
error = reply.error;
}
} else if (this._transaction.error) {
error = this._transaction.error;
}
debug('receive', reply);
} catch (err) {
error = err;
error.code = 'EHDBMSGPARSE';
debug('receive', error);
}
if (error && error.fatal) {
this.destroy(error);
}
if(cb) {
cb(error, reply);
}
};
Connection.prototype.enqueue = function enqueue(task, cb) {
var queueable;
if (!this._socket || !this._queue || this.readyState === 'closed') {
var err = new Error('Connection closed');
err.code = 'EHDBCLOSE';
if (cb) {
return cb(err);
} else if (util.isFunction(task.callback)) {
return task.callback(err);
}
}
if (util.isFunction(task)) {
queueable = this._queue.createTask(task, cb);
queueable.name = task.name;
} else if (util.isObject(task)) {
if (task instanceof request.Segment) {
queueable = this._queue.createTask(this.send.bind(this, task), cb);
queueable.name = MessageTypeName[task.type];
} else if (util.isFunction(task.run)) {
queueable = task;
}
}
if (queueable) {
this._queue.push(queueable);
}
};
Connection.prototype._createAuthenticationManager = auth.createManager;
Connection.prototype.connect = function connect(options, cb) {
var self = this;
var manager;
for(var key in options) {
if(key.toUpperCase().startsWith("SESSIONVARIABLE:")) {
var sv_key = key.substring(key.indexOf(":") + 1);
var sv_value = options[key];
if(sv_key && sv_key.length > 0 && sv_value && sv_value.length > 0) {
this._clientInfo.setProperty(sv_key, sv_value);
}
delete options[key];
}
if(key.toUpperCase() === "DATAFORMATSUPPORT") {
if (options[key] && options[key] >= 1 && options[key] <= DataFormatVersion.MAX_VERSION) {
this.connectOptions.setOptions([
{name : common.ConnectOption.DATA_FORMAT_VERSION,
value : options[key]},
{name : common.ConnectOption.DATA_FORMAT_VERSION2,
value : options[key]}
]);
} else {
// Raise an error for the invalid data format version
if (options[key] > DataFormatVersion.MAX_VERSION) {
return cb(new Error(util.format("Maximum driver supported data format %d is less than client requested %d",
DataFormatVersion.MAX_VERSION, options[key])));
} else {
return cb(new Error(util.format("Data format %s is invalid. Supported values are 1 to %d",
options[key], DataFormatVersion.MAX_VERSION)));
}
}
} else if (key.toUpperCase() === 'SPATIALTYPES') {
this._settings['spatialTypes'] = util.getBooleanProperty(options[key]) ? 1 : 0;
} else if (key.toUpperCase() === 'VECTOROUTPUTTYPE') {
this._settings['vectorOutputType'] = options[key].toUpperCase() === 'ARRAY' ? 'Array' : 'Buffer';
}
}
this.connectOptions.setOptions([
{name : common.ConnectOption.OS_USER,
value : this._clientInfo.getUser()}
]);
this.clientContextOptions.setOptions([
{name : common.ClientContextOption.CLIENT_APPLICATION_PROGRAM,
value : this._clientInfo.getApplication()}
]);
const compressionFlags = compressor.determineCompressionFlags(options['compress']);
if(compressionFlags) {
this.connectOptions.setOptions([{name: common.ConnectOption.COMPRESSION_LEVEL_AND_FLAGS, value: compressionFlags}]);
}
if(options["disableCloudRedirect"] == true) {
this._redirectType = common.RedirectType.REDIRECTION_DISABLED;
}
try {
manager = this._createAuthenticationManager(options);
} catch (err) {
return util.setImmediate(function () {
cb(err);
});
}
function connReceive(err, reply) {
if (err) {
return cb(err);
}
if (Array.isArray(reply.connectOptions)) {
self.connectOptions.setOptions(reply.connectOptions);
if(compressor.lz4Available && compressor.isLZ4CompressionNegotiated(self.connectOptions.compressionLevelAndFlags)) {
self._compressionEnabled = true;
}
}
manager.finalize(reply.authentication);
self._settings.user = manager.userFromServer;
if (manager.sessionCookie) {
self._settings.sessionCookie = manager.sessionCookie;
}
self._queue.resume();
cb(null, reply);
}
function authReceive(err, reply) {
if (err) {
return cb(err, reply);
}
manager.initialize(reply.authentication, function(err) {
if (err) return cb(err);
var redirectOptions = []
if (typeof self._initialHost === 'undefined') {
self._initialHost = self.host;
}
if (typeof self._initialPort === 'undefined') {
self._initialPort = self.port;
}
redirectOptions.push({
name: common.ConnectOption.ENDPOINT_HOST,
value: self._initialHost
});
redirectOptions.push({
name: common.ConnectOption.ENDPOINT_PORT,
value: self._initialPort
});
var endpointList = undefined;
if (typeof self._initialHost !== 'undefined' && typeof self._initialPort !== 'undefined') {
endpointList = self._initialHost + ":" + self._initialPort.toString();
}
redirectOptions.push({
name: common.ConnectOption.ENDPOINT_LIST,
value: endpointList
});
redirectOptions.push({
name: common.ConnectOption.REDIRECTION_TYPE,
value: self._redirectType
});
if (typeof self._redirectHost === 'undefined') {
self._redirectHost = self._initialHost;
}
redirectOptions.push({
name: common.ConnectOption.REDIRECTED_HOST,
value: self._redirectHost
});
if (typeof self._redirectPort === 'undefined') {
self._redirectPort = self._initialPort;
}
redirectOptions.push({
name: common.ConnectOption.REDIRECTED_PORT,
value: self._redirectPort
});
self.connectOptions.setOptions(redirectOptions);
self.send(request.connect({
authentication: manager.finalData(),
clientId: self.clientId,
connectOptions: self.connectOptions.getOptions(),
useCesu8: self.useCesu8
}), connReceive);
});
}
var authOptions = {
clientContext: this.clientContextOptions.getOptions(),
authentication: manager.initialData(),
useCesu8: self.useCesu8
}
if(this._redirectType == common.RedirectType.REDIRECTION_NONE) {
authOptions.dbConnectInfo = true;
}
this.send(request.authenticate(authOptions), authReceive);
};
Connection.prototype.disconnect = function disconnect(cb) {
var self = this;
function done(err, reply) {
self.destroy();
cb(err, reply);
}
function enqueueDisconnect() {
self.enqueue(request.disconnect(), done);
}
if (this.isIdle()) {
return enqueueDisconnect();
}
this._queue.once('drain', enqueueDisconnect);
};
Connection.prototype.executeDirect = function executeDirect(options, cb) {
options = util.extend({
autoCommit: this.autoCommit,
holdCursorsOverCommit: this.holdCursorsOverCommit,
scrollableCursor: this.scrollableCursor,
useCesu8: this.useCesu8
}, options);
this.enqueue(request.executeDirect(options), cb);
};
Connection.prototype.prepare = function prepare(options, cb) {
options = util.extend({
holdCursorsOverCommit: this.holdCursorsOverCommit,
scrollableCursor: this.scrollableCursor,
useCesu8: this.useCesu8
}, options);
this.enqueue(request.prepare(options), cb);
};
Connection.prototype.readLob = function readLob(options, cb) {
if (options.locatorId) {
options = {
readLobRequest: options
};
}
options.autoCommit = this.autoCommit;
this.enqueue(request.readLob(options), cb);
};
Connection.prototype.execute = function execute(options, cb) {
options = util.extend({
autoCommit: this.autoCommit,
holdCursorsOverCommit: this.holdCursorsOverCommit,
scrollableCursor: this.scrollableCursor,
parameters: EMPTY_BUFFER
}, options);
if (options.parameters === EMPTY_BUFFER) {
return this.enqueue(request.execute(options), cb);
}
this.enqueue(createExecuteTask(this, options, cb));
};
Connection.prototype.fetchNext = function fetchNext(options, cb) {
options.autoCommit = this.autoCommit;
options.useCesu8 = this.useCesu8;
this.enqueue(request.fetchNext(options), cb);
};
Connection.prototype.closeResultSet = function closeResultSet(options, cb) {
this.enqueue(request.closeResultSet(options), cb);
};
Connection.prototype.dropStatement = function dropStatement(options, cb) {
options.useCesu8 = this.useCesu8;
this.enqueue(request.dropStatementId(options), cb);
};
Connection.prototype.commit = function commit(options, cb) {
if (util.isFunction(options)) {
cb = options;
options = {};
}
this.enqueue(request.commit(options), cb);
};
Connection.prototype.rollback = function rollback(options, cb) {
if (util.isFunction(options)) {
cb = options;
options = {};
}
this.enqueue(request.rollback(options), cb);
};
// The function doesn't use the queue. It's used before the queue starts running
Connection.prototype.fetchDbConnectInfo = function (options, cb) {
if (this.readyState == 'closed') {
var err = new Error('Connection unexpectedly closed');
err.code = 'EHDBCLOSE';
return cb(err)
}
this.send(request.dbConnectInfo(options), function(err, reply) {
if (err) {
return cb(err);
}
var info = new part.DbConnectInfoOptions();
info.setOptions(reply.dbConnectInfo);
cb(null, info);
});
};
Connection.prototype._closeSilently = function _closeSilently() {
if (this._socket) {
this._socket.removeAllListeners('close');
this.close();
}
};
Connection.prototype.close = function close() {
var self = this;
function closeConnection() {
debug('close');
self.destroy();
}
if (this.readyState === 'closed') {
return;
}
if (this.isIdle()) {
return closeConnection();
}
this._queue.once('drain', closeConnection);
};
Connection.prototype.destroy = function destroy(err) {
if (this._socket) {
this._socket.destroy(err);
}
};
Connection.prototype.isIdle = function isIdle() {
return this._queue.empty && !this._queue.busy;
};
Connection.prototype.setAutoCommit = function setAutoCommit(autoCommit) {
this._transaction.autoCommit = autoCommit;
};
Connection.prototype.setInitialHostAndPort = function setInitialHostAndPort(host, port) {
this._initialHost = host;
this._initialPort = port;
};
Connection.prototype.setRedirectHostAndPort = function setRedirectHostAndPort(host, port) {
this._redirectHost = host;
this._redirectPort = port;
};
Connection.prototype.setRedirectType = function setRedirectType(type) {
this._redirectType = type;
};
Connection.prototype._setClientInfo = function _setClientInfo(key, val) {
this._clientInfo.setProperty(key, val);
};
function ConnectionState() {
this.sessionId = -1;
this.packetCount = -1;
this.messageType = undefined;
this.receive = undefined;
}
function Version(major, minor) {
this.major = major;
this.minor = minor;
}
Version.read = function readVersion(buffer, offset) {
return new Version(
buffer.readInt8(offset),
buffer.readInt16LE(offset + 1)
);
};
function InitializationReply(productVersion, protocolVersion) {
this.productVersion = productVersion;
this.protocolVersion = protocolVersion;
}
InitializationReply.LENGTH = 8;
InitializationReply.read = function readInitializationReply(buffer) {
var productVersion = Version.read(buffer, 0);
var protocolVersion = Version.read(buffer, 3);
return new InitializationReply(productVersion, protocolVersion);
};
var initializationRequestBuffer = new Buffer([
0xff, 0xff, 0xff, 0xff, 4, 20, 0, 4, 1, 0, 0, 1, 1, 1
]);