Skip to content

Added support for FIXED types #261

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 10 commits into
base: master
Choose a base branch
from
5 changes: 4 additions & 1 deletion lib/protocol/ExecuteTask.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,10 @@ function ExecuteTask(connection, options, callback) {
this.scrollableCursor = options.scrollableCursor;
this.statementId = options.statementId;
this.functionCode = options.functionCode;
this.writer = new Writer(options.parameters.types, connection.useCesu8, connection.spatialTypes);
this.writer = new Writer(
{ types: options.parameters.types, fractions: options.parameters.fractions },
{ useCesu8: connection.useCesu8, spatialTypes: connection.spatialTypes }
);
var values = options.parameters.values;
if (values.length && Array.isArray(values[0])) {
this.parameterValues = values.slice();
Expand Down
6 changes: 4 additions & 2 deletions lib/protocol/Parser.js
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,8 @@ function createHonestParseFunction(metadata, options) {

function addReadFunction(column) {
var args = [];
if (column.dataType === TypeCode.DECIMAL) {
if (column.dataType === TypeCode.DECIMAL || column.dataType === TypeCode.FIXED8
|| column.dataType === TypeCode.FIXED12 || column.dataType === TypeCode.FIXED16) {
args.push(column.fraction);
}
column.f = {
Expand Down Expand Up @@ -154,7 +155,8 @@ function createFunctionBody(metadata, options) {

function getReadFunction(column) {
var fn = ReadFunction[column.dataType];
if (column.dataType === TypeCode.DECIMAL) {
if (column.dataType === TypeCode.DECIMAL || column.dataType === TypeCode.FIXED8
|| column.dataType === TypeCode.FIXED12 || column.dataType === TypeCode.FIXED16) {
fn += '(' + column.fraction + ')';
} else {
fn += '()';
Expand Down
39 changes: 39 additions & 0 deletions lib/protocol/Reader.js
Original file line number Diff line number Diff line change
Expand Up @@ -365,10 +365,49 @@ Reader.prototype.readDecimal = function readDecimal(fraction) {
return value;
};

Reader.prototype.readFixed8 = function readFixed8(fraction) {
if (this.buffer[this.offset++] === 0x00) {
return null;
}
var value = bignum.readFIXED(this.buffer, 8, this.offset, fraction);
this.offset += 8;
return value;
}

Reader.prototype.readFixed12 = function readFixed12(fraction) {
if (this.buffer[this.offset++] === 0x00) {
return null;
}
var value = bignum.readFIXED(this.buffer, 12, this.offset, fraction);
this.offset += 12;
return value;
}

Reader.prototype.readFixed16 = function readFixed16(fraction) {
if (this.buffer[this.offset++] === 0x00) {
return null;
}
var value = bignum.readFIXED(this.buffer, 16, this.offset, fraction);
this.offset += 16;
return value;
}

Reader.prototype.readAlphanum = function readAlphanum() {
return this.readBytes(false, true);
}

Reader.prototype.readBoolean = function readBoolean() {
var value = this.buffer[this.offset++];
switch (value) {
case 0x02:
return true;
case 0x01:
return null;
default:
return false;
}
}

Reader.LobDescriptor = LobDescriptor;

function LobDescriptor(type, options, charLength, byteLength, locatorId, chunk, defaultType) {
Expand Down
1 change: 1 addition & 0 deletions lib/protocol/Result.js
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,7 @@ function isLob(column) {
case TypeCode.NCLOB:
case TypeCode.NLOCATOR:
case TypeCode.TEXT:
case TypeCode.BINTEXT:
return true;
default:
return false;
Expand Down
1 change: 1 addition & 0 deletions lib/protocol/ResultSet.js
Original file line number Diff line number Diff line change
Expand Up @@ -397,6 +397,7 @@ function isLob(column) {
case TypeCode.NCLOB:
case TypeCode.NLOCATOR:
case TypeCode.TEXT:
case TypeCode.BINTEXT:
return true;
default:
return false;
Expand Down
7 changes: 6 additions & 1 deletion lib/protocol/Statement.js
Original file line number Diff line number Diff line change
Expand Up @@ -112,9 +112,14 @@ Statement.prototype._normalizeInputParameters = function _normalizeInputParamete
return values[metadata.name];
}

function getTypeFraction(metadata) {
return metadata.fraction;
}

var parameters = {
types: inputParameterMetadata.map(getDataType),
values: undefined
values: undefined,
fractions: inputParameterMetadata.map(getTypeFraction),
};

parameters.values = Array.isArray(values) ? values : inputParameterMetadata.filter(isDefined).map(getObjectValue);
Expand Down
184 changes: 164 additions & 20 deletions lib/protocol/Writer.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ var LobOptions = common.LobOptions;
var NormalizedTypeCode = common.NormalizedTypeCode;
var bignum = util.bignum;
var calendar = util.calendar;
var zeropad = require('../util/zeropad');
var isValidDay = calendar.isValidDay;
var isValidTime = calendar.isValidTime;
var isZeroDay = calendar.isZeroDay;
Expand All @@ -39,12 +40,14 @@ var REGEX = {
};

const maxDecimalMantissaLen = 34;
const maxFixedMantissaLen = 38;

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe add a comment here describing the structure of 'params'

function Writer(types, useCesu8, spatialTypes) {
this._types = types.map(normalizeType);
function Writer(params, options) {
this._types = params.types.map(normalizeType);
this._fractions = params.fractions;
this.reset();
this._useCesu8 = (useCesu8 === true);
this._spatialTypes = (spatialTypes === 1 ? 1 : 0);
this._useCesu8 = (options && options.useCesu8 === true);
this._spatialTypes = ((options && options.spatialTypes === 1) ? 1 : 0);
}

function normalizeType(type) {
Expand All @@ -66,13 +69,13 @@ Writer.prototype.reset = function reset() {
Writer.prototype.setValues = function setValues(values) {
this.reset();
for (var i = 0; i < values.length; i++) {
this.add(this._types[i], values[i]);
this.add(this._types[i], values[i], this._fractions ? this._fractions[i] : undefined);
}
this._params = true;
};

exports.create = function createWriter(params, useCesu8, spatialTypes) {
var writer = new Writer(params.types, useCesu8, spatialTypes);
exports.create = function createWriter(params, options) {
var writer = new Writer(params, options);
writer.setValues(params.values);
return writer;
};
Expand All @@ -95,9 +98,11 @@ Object.defineProperties(Writer.prototype, {
}
});

Writer.prototype.add = function add(type, value) {
Writer.prototype.add = function add(type, value, fraction) {
if (typeof value === 'undefined' || value === null) {
this.pushNull(type);
} else if (type === TypeCode.FIXED8 || type === TypeCode.FIXED12 || type === TypeCode.FIXED16) {
this[type](value, fraction);
} else {
this[type](value);
}
Expand Down Expand Up @@ -491,9 +496,9 @@ Writer.prototype[TypeCode.DOUBLE] = function writeDouble(value) {
Writer.prototype[TypeCode.DECIMAL] = function writeDecimal(value) {
var decimal;
if (util.isString(value)) {
decimal = stringToDecimal(value);
decimal = stringToDecimal(value, maxDecimalMantissaLen);
} else if (util.isNumber(value)) {
decimal = stringToDecimal(value.toExponential());
decimal = stringToDecimal(value.toExponential(), maxDecimalMantissaLen);
} else {
throw createInputError('DECIMAL');
}
Expand All @@ -503,6 +508,100 @@ Writer.prototype[TypeCode.DECIMAL] = function writeDecimal(value) {
this.push(buffer);
};

function fixedToDecimal(value, fraction, typeStr) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think toFixedDecimal would be a better name since we aren't converting a 'fixed' value to a decimal, but rather converting a general value to a decimal with a particular format.

var decimal;
// Convert to decimal object with maximum number of digits 38 and minimum exponent is
// -fraction, so there are at most 'fraction' digits after the decimal
if (util.isString(value)) {
decimal = stringToDecimal(value, maxFixedMantissaLen, -fraction, typeStr);
} else if (util.isNumber(value)) {
decimal = stringToDecimal(value.toExponential(), maxFixedMantissaLen, -fraction, typeStr);
} else {
throw createInputError(typeStr);
}
if (decimal.m.length + decimal.e + fraction > maxFixedMantissaLen) {
throw createInputError(typeStr); // Numeric overflow, greater than maximum precision
}

if ((-decimal.e) < fraction) {
decimal.m += zeropad.ZEROS[fraction + decimal.e];
}
return decimal;
}

function writeFixed16Buffer(decimal, buffer, offset) {
bignum.writeUInt128LE(buffer, decimal.m, offset);
if (decimal.s === -1) {
// Apply two's complement conversion
var extraOne = true;
for (var i = offset; i < offset + 16; i++) {
if (extraOne) {
if (buffer[i] !== 0) {
buffer[i] = 0xff - buffer[i] + 1;
extraOne = false;
} else {
buffer[i] = 0;
}
} else {
buffer[i] = 0xff - buffer[i];
}
}
}
}

function checkFixedOverflow(decimal, extBuffer, byteLimit, typeStr) {
if (decimal.s === -1) {
for (var i = byteLimit; i < 16; ++i) {
if (extBuffer[i] != 0xff) {
throw createInputError(typeStr);
}
}
if ((extBuffer[byteLimit - 1] & 0x80) == 0) {
throw createInputError(typeStr);
}
} else {
for (var i = byteLimit; i < 16; ++i) {
if (extBuffer[i] != 0) {
throw createInputError(typeStr);
}
}
if (extBuffer[byteLimit - 1] & 0x80) {
throw createInputError(typeStr);
}
}
}

Writer.prototype[TypeCode.FIXED8] = function writeFixed8(value, fraction) {
var extBuffer = new Buffer(16);
var decimal = fixedToDecimal(value, fraction, 'FIXED8');
writeFixed16Buffer(decimal, extBuffer, 0);
// Check that the representation does not exceed 8 bytes
checkFixedOverflow(decimal, extBuffer, 8, 'FIXED8');
var buffer = new Buffer(9);
buffer[0] = TypeCode.FIXED8;
extBuffer.copy(buffer, 1, 0, 8);
this.push(buffer);
}

Writer.prototype[TypeCode.FIXED12] = function writeFixed12(value, fraction) {
var extBuffer = new Buffer(16);
var decimal = fixedToDecimal(value, fraction, 'FIXED12');
writeFixed16Buffer(decimal, extBuffer, 0);
// Check that the representation does not exceed 12 bytes
checkFixedOverflow(decimal, extBuffer, 12, 'FIXED12');
var buffer = new Buffer(13);
buffer[0] = TypeCode.FIXED12;
extBuffer.copy(buffer, 1, 0, 12);
this.push(buffer);
}

Writer.prototype[TypeCode.FIXED16] = function writeFixed16(value, fraction) {
var buffer = new Buffer(17);
buffer[0] = TypeCode.FIXED16;
writeFixed16Buffer(fixedToDecimal(value, fraction, 'FIXED16'), buffer, 1);
this.push(buffer);
}

Writer.prototype[TypeCode.NSTRING] = function writeNString(value) {
this.writeCharacters(TypeCode.NSTRING, value);
};
Expand Down Expand Up @@ -531,6 +630,9 @@ Writer.prototype[TypeCode.BLOB] = function writeBLob(value) {
var buffer = new Buffer(10);
buffer.fill(0x00);
buffer[0] = TypeCode.BLOB;
if (util.isString(value)) {
value = util.convert.encode(value, this._useCesu8);
}
this.pushLob(buffer, value);
};

Expand Down Expand Up @@ -772,6 +874,35 @@ Writer.prototype[TypeCode.ST_GEOMETRY] = function writeST_GEOMETRY(value) {
}
}

Writer.prototype[TypeCode.BOOLEAN] = function writeBoolean(value) {
var buffer = new Buffer(2);
buffer[0] = TypeCode.BOOLEAN;
// 0x02 - True, 0x01 - Null, 0x00 - False
if (value === null) {
buffer[1] = 0x01;
} else if (util.isString(value)) {
if (value.toUpperCase() === 'TRUE' || value === '1') {
buffer[1] = 0x02;
} else if (value.toUpperCase() === 'FALSE' || value === '0') {
buffer[1] = 0x00;
} else if (value.toUpperCase() === 'UNKNOWN' || value.length === 0) {
buffer[1] = 0x01;
} else {
throw createInputError('BOOLEAN');
}
} else if (util.isNumber(value)) {
buffer[1] = value == 0 ? 0x00 : 0x02;
} else if (value === true) {
buffer[1] = 0x02;
} else if (value === false) {
buffer[1] = 0x00;
} else {
throw createInputError('BOOLEAN');
}

this.push(buffer);
}

function setChar(str, i, c) {
if(i >= str.length) return str;
return str.substring(0, i) + c + str.substring(i + 1);
Expand All @@ -793,12 +924,12 @@ function trimTrailingZeroes(str) {
return str.substring(0, i + 1);
}

function stringToDecimal(str) {
function stringToDecimal(str, maxMantissaLen, minExp, typeStr) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In my own testing I discovered a subtle behaviour change. Previously, inserting to a DECIMAL column would truncate decimal values beyond the column scale, whereas now with DFV 8 support these values are being rounded. For example, inserting "123.456" into a DECIMAL(10,2) gives "123.45" with DFV 7, but "123.46" with DFV 8. Both the hana-client driver and the server itself use truncation, and I found a reference to this in the server documentation, so we should treat that as the correct behaviour.

/* jshint bitwise:false */
var dec = str.match(REGEX.DECIMAL);
// REGEX.DECIMAL will match "." and "" despite these being invalid.
if (!dec || str === "." || str === "") {
throw createInputError('DECIMAL');
throw createInputError(typeStr === undefined ? 'DECIMAL' : typeStr);
}
var sign = dec[1] === '-' ? -1 : 1;
var mInt = dec[2] || '';
Expand All @@ -810,14 +941,27 @@ function stringToDecimal(str) {
if(mantissa.length === 0) mantissa = "0";
exp -= mFrac.length

// round to maxDecimalMantissaLen digits and increment exp appropriately
if(mantissa.length > maxDecimalMantissaLen) {
var followDigit = mantissa[maxDecimalMantissaLen];
exp += (mantissa.length - maxDecimalMantissaLen)
mantissa = mantissa.substring(0, maxDecimalMantissaLen);
var calcMaxMantissaLength = maxMantissaLen;
if (minExp !== undefined && exp < minExp) {
// Shift the maxMantissaLen such that the exponent is minExp
calcMaxMantissaLength = exp + mantissa.length - minExp;
if (calcMaxMantissaLength < 0) {
// All digits are rounded away
return {s: 1, m: "0", e: 0};
} else if (calcMaxMantissaLength == 0) {
// Only the first digit matters
return {s: sign, m: mantissa[0] > '4' ? "1" : "0", e: minExp};
}
}

// round to calcMaxMantissaLen digits and increment exp appropriately
if(mantissa.length > calcMaxMantissaLength) {
var followDigit = mantissa[calcMaxMantissaLength];
exp += (mantissa.length - calcMaxMantissaLength);
mantissa = mantissa.substring(0, calcMaxMantissaLength);
if(followDigit > '4') {
// round up
var i = maxDecimalMantissaLen - 1;
var i = calcMaxMantissaLength - 1;
while(i >= 0 && mantissa[i] === '9') {
i -= 1;
}
Expand All @@ -830,9 +974,9 @@ function stringToDecimal(str) {
mantissa = mantissa.substring(0, i + 1);
mantissa = setChar(mantissa, i, String.fromCharCode(mantissa.charCodeAt(i) + 1));
}
} else if(mantissa[maxDecimalMantissaLen - 1] === '0') {
} else if(mantissa[calcMaxMantissaLength - 1] === '0') {
var trimmed = trimTrailingZeroes(mantissa);
exp += (maxDecimalMantissaLen - trimmed.length);
exp += (calcMaxMantissaLength - trimmed.length);
mantissa = trimmed;
}
}
Expand Down
Loading