Skip to content

Commit 4bd3cdf

Browse files
authored
Added support for FIXED types (SAP#261)
- Added functions to read and write FIXED8, FIXED12, and FIXED16 types - Modified Statement.js and Writer.js to pass the fraction metadata to the Writer for use in writing FIXED types - Changed the behaviour when writing fixed DECIMAL types with inputs larger than the max mantissa length to truncate rather than rounding - The behaviour for writing floating DECIMAL types larger than the max mantissa length remains to round (consistent with HANA's TO_DECIMAL) - Added a zero to the zeropad.js to fix a case where large values inserted from FIXED types could yield '1undefined' when returning as a DECIMAL - Added a description for data format version 8 and its changes Test Changes - Added unit tests for the new FIXED type functions in bignum.js, Reader.js and Writer.js - Added integration tests for the new FIXED types - The tests for FIXED types also run on DFV 7 to ensure the same behaviour for DECIMAL types
1 parent bd535ce commit 4bd3cdf

19 files changed

Lines changed: 1132 additions & 77 deletions

lib/protocol/ExecuteTask.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,10 @@ function ExecuteTask(connection, options, callback) {
3030
this.scrollableCursor = options.scrollableCursor;
3131
this.statementId = options.statementId;
3232
this.functionCode = options.functionCode;
33-
this.writer = new Writer(options.parameters.types, connection.useCesu8, connection.spatialTypes);
33+
this.writer = new Writer(
34+
{ types: options.parameters.types, fractions: options.parameters.fractions },
35+
{ useCesu8: connection.useCesu8, spatialTypes: connection.spatialTypes }
36+
);
3437
var values = options.parameters.values;
3538
if (values.length && Array.isArray(values[0])) {
3639
this.parameterValues = values.slice();

lib/protocol/Parser.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,8 @@ function createHonestParseFunction(metadata, options) {
109109

110110
function addReadFunction(column) {
111111
var args = [];
112-
if (column.dataType === TypeCode.DECIMAL) {
112+
if (column.dataType === TypeCode.DECIMAL || column.dataType === TypeCode.FIXED8
113+
|| column.dataType === TypeCode.FIXED12 || column.dataType === TypeCode.FIXED16) {
113114
args.push(column.fraction);
114115
}
115116
column.f = {
@@ -154,7 +155,8 @@ function createFunctionBody(metadata, options) {
154155

155156
function getReadFunction(column) {
156157
var fn = ReadFunction[column.dataType];
157-
if (column.dataType === TypeCode.DECIMAL) {
158+
if (column.dataType === TypeCode.DECIMAL || column.dataType === TypeCode.FIXED8
159+
|| column.dataType === TypeCode.FIXED12 || column.dataType === TypeCode.FIXED16) {
158160
fn += '(' + column.fraction + ')';
159161
} else {
160162
fn += '()';

lib/protocol/Reader.js

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -365,6 +365,33 @@ Reader.prototype.readDecimal = function readDecimal(fraction) {
365365
return value;
366366
};
367367

368+
Reader.prototype.readFixed8 = function readFixed8(fraction) {
369+
if (this.buffer[this.offset++] === 0x00) {
370+
return null;
371+
}
372+
var value = bignum.readFIXED(this.buffer, 8, this.offset, fraction);
373+
this.offset += 8;
374+
return value;
375+
}
376+
377+
Reader.prototype.readFixed12 = function readFixed12(fraction) {
378+
if (this.buffer[this.offset++] === 0x00) {
379+
return null;
380+
}
381+
var value = bignum.readFIXED(this.buffer, 12, this.offset, fraction);
382+
this.offset += 12;
383+
return value;
384+
}
385+
386+
Reader.prototype.readFixed16 = function readFixed16(fraction) {
387+
if (this.buffer[this.offset++] === 0x00) {
388+
return null;
389+
}
390+
var value = bignum.readFIXED(this.buffer, 16, this.offset, fraction);
391+
this.offset += 16;
392+
return value;
393+
}
394+
368395
Reader.prototype.readAlphanum = function readAlphanum() {
369396
return this.readBytes(false, true);
370397
}

lib/protocol/Statement.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,9 +112,14 @@ Statement.prototype._normalizeInputParameters = function _normalizeInputParamete
112112
return values[metadata.name];
113113
}
114114

115+
function getTypeFraction(metadata) {
116+
return metadata.fraction;
117+
}
118+
115119
var parameters = {
116120
types: inputParameterMetadata.map(getDataType),
117-
values: undefined
121+
values: undefined,
122+
fractions: inputParameterMetadata.map(getTypeFraction),
118123
};
119124

120125
parameters.values = Array.isArray(values) ? values : inputParameterMetadata.filter(isDefined).map(getObjectValue);

lib/protocol/Writer.js

Lines changed: 165 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ var LobOptions = common.LobOptions;
2222
var NormalizedTypeCode = common.NormalizedTypeCode;
2323
var bignum = util.bignum;
2424
var calendar = util.calendar;
25+
var zeropad = require('../util/zeropad');
2526
var isValidDay = calendar.isValidDay;
2627
var isValidTime = calendar.isValidTime;
2728
var isZeroDay = calendar.isZeroDay;
@@ -40,12 +41,22 @@ var REGEX = {
4041
};
4142

4243
const maxDecimalMantissaLen = 34;
43-
44-
function Writer(types, useCesu8, spatialTypes) {
45-
this._types = types.map(normalizeType);
44+
const maxFixedMantissaLen = 38;
45+
46+
/**
47+
* Constructs a Writer to write input parameters into server readable representation
48+
* @param {Object} params - Metadata for input parameters to be written to the server
49+
* @param {number[]} params.types - Array of type codes for each parameter
50+
* @param {number[]} params.fractions - Array of the fraction / scale of each parameter
51+
* (fraction metadata is only necessary for FIXED / DECIMAL types)
52+
* @param {Object} options - Stores options to modify the way data types are written
53+
*/
54+
function Writer(params, options) {
55+
this._types = params.types.map(normalizeType);
56+
this._fractions = params.fractions;
4657
this.reset();
47-
this._useCesu8 = (useCesu8 === true);
48-
this._spatialTypes = (spatialTypes === 1 ? 1 : 0);
58+
this._useCesu8 = (options && options.useCesu8 === true);
59+
this._spatialTypes = ((options && options.spatialTypes === 1) ? 1 : 0);
4960
}
5061

5162
function normalizeType(type) {
@@ -67,13 +78,13 @@ Writer.prototype.reset = function reset() {
6778
Writer.prototype.setValues = function setValues(values) {
6879
this.reset();
6980
for (var i = 0; i < values.length; i++) {
70-
this.add(this._types[i], values[i]);
81+
this.add(this._types[i], values[i], this._fractions ? this._fractions[i] : undefined);
7182
}
7283
this._params = true;
7384
};
7485

75-
exports.create = function createWriter(params, useCesu8, spatialTypes) {
76-
var writer = new Writer(params.types, useCesu8, spatialTypes);
86+
exports.create = function createWriter(params, options) {
87+
var writer = new Writer(params, options);
7788
writer.setValues(params.values);
7889
return writer;
7990
};
@@ -96,9 +107,12 @@ Object.defineProperties(Writer.prototype, {
96107
}
97108
});
98109

99-
Writer.prototype.add = function add(type, value) {
110+
Writer.prototype.add = function add(type, value, fraction) {
100111
if (typeof value === 'undefined' || value === null) {
101112
this.pushNull(type);
113+
} else if (type === TypeCode.DECIMAL || type === TypeCode.FIXED8
114+
|| type === TypeCode.FIXED12 || type === TypeCode.FIXED16) {
115+
this[type](value, fraction);
102116
} else {
103117
this[type](value);
104118
}
@@ -533,12 +547,12 @@ Writer.prototype[TypeCode.DOUBLE] = function writeDouble(value) {
533547
this.push(buffer);
534548
};
535549

536-
Writer.prototype[TypeCode.DECIMAL] = function writeDecimal(value) {
550+
Writer.prototype[TypeCode.DECIMAL] = function writeDecimal(value, fraction) {
537551
var decimal;
538552
if (util.isString(value)) {
539-
decimal = stringToDecimal(value);
553+
decimal = stringToDecimal(value, maxDecimalMantissaLen, fraction);
540554
} else if (util.isNumber(value)) {
541-
decimal = stringToDecimal(value.toExponential());
555+
decimal = stringToDecimal(value.toExponential(), maxDecimalMantissaLen, fraction);
542556
} else {
543557
throw createInputError('DECIMAL');
544558
}
@@ -548,6 +562,103 @@ Writer.prototype[TypeCode.DECIMAL] = function writeDecimal(value) {
548562
this.push(buffer);
549563
};
550564

565+
function toFixedDecimal(value, fraction, typeStr) {
566+
var decimal;
567+
// Convert to decimal object with maximum number of digits 38
568+
if (util.isString(value)) {
569+
decimal = stringToDecimal(value, maxFixedMantissaLen, fraction, typeStr);
570+
} else if (util.isNumber(value)) {
571+
decimal = stringToDecimal(value.toExponential(), maxFixedMantissaLen, fraction, typeStr);
572+
} else {
573+
throw createInputError(typeStr);
574+
}
575+
// Truncate decimal with the minimum exponent being -fraction, so there are at most
576+
// 'fraction' digits after the decimal
577+
decimal = truncateDecimalToExp(decimal, -fraction);
578+
579+
if (decimal.m.length + decimal.e + fraction > maxFixedMantissaLen) {
580+
throw createInputError(typeStr); // Numeric overflow, greater than maximum precision
581+
}
582+
583+
if ((-decimal.e) < fraction) {
584+
decimal.m += zeropad.ZEROS[fraction + decimal.e];
585+
}
586+
return decimal;
587+
}
588+
589+
function writeFixed16Buffer(decimal, buffer, offset) {
590+
bignum.writeUInt128LE(buffer, decimal.m, offset);
591+
if (decimal.s === -1) {
592+
// Apply two's complement conversion
593+
var extraOne = true;
594+
for (var i = offset; i < offset + 16; i++) {
595+
if (extraOne) {
596+
if (buffer[i] !== 0) {
597+
buffer[i] = 0xff - buffer[i] + 1;
598+
extraOne = false;
599+
} else {
600+
buffer[i] = 0;
601+
}
602+
} else {
603+
buffer[i] = 0xff - buffer[i];
604+
}
605+
}
606+
}
607+
}
608+
609+
function checkFixedOverflow(decimal, extBuffer, byteLimit, typeStr) {
610+
if (decimal.s === -1) {
611+
for (var i = byteLimit; i < 16; ++i) {
612+
if (extBuffer[i] != 0xff) {
613+
throw createInputError(typeStr);
614+
}
615+
}
616+
if ((extBuffer[byteLimit - 1] & 0x80) == 0) {
617+
throw createInputError(typeStr);
618+
}
619+
} else {
620+
for (var i = byteLimit; i < 16; ++i) {
621+
if (extBuffer[i] != 0) {
622+
throw createInputError(typeStr);
623+
}
624+
}
625+
if (extBuffer[byteLimit - 1] & 0x80) {
626+
throw createInputError(typeStr);
627+
}
628+
}
629+
}
630+
631+
Writer.prototype[TypeCode.FIXED8] = function writeFixed8(value, fraction) {
632+
var extBuffer = new Buffer(16);
633+
var decimal = toFixedDecimal(value, fraction, 'FIXED8');
634+
writeFixed16Buffer(decimal, extBuffer, 0);
635+
// Check that the representation does not exceed 8 bytes
636+
checkFixedOverflow(decimal, extBuffer, 8, 'FIXED8');
637+
var buffer = new Buffer(9);
638+
buffer[0] = TypeCode.FIXED8;
639+
extBuffer.copy(buffer, 1, 0, 8);
640+
this.push(buffer);
641+
}
642+
643+
Writer.prototype[TypeCode.FIXED12] = function writeFixed12(value, fraction) {
644+
var extBuffer = new Buffer(16);
645+
var decimal = toFixedDecimal(value, fraction, 'FIXED12');
646+
writeFixed16Buffer(decimal, extBuffer, 0);
647+
// Check that the representation does not exceed 12 bytes
648+
checkFixedOverflow(decimal, extBuffer, 12, 'FIXED12');
649+
var buffer = new Buffer(13);
650+
buffer[0] = TypeCode.FIXED12;
651+
extBuffer.copy(buffer, 1, 0, 12);
652+
this.push(buffer);
653+
}
654+
655+
Writer.prototype[TypeCode.FIXED16] = function writeFixed16(value, fraction) {
656+
var buffer = new Buffer(17);
657+
buffer[0] = TypeCode.FIXED16;
658+
writeFixed16Buffer(toFixedDecimal(value, fraction, 'FIXED16'), buffer, 1);
659+
this.push(buffer);
660+
}
661+
551662
Writer.prototype[TypeCode.NSTRING] = function writeNString(value) {
552663
this.writeCharacters(TypeCode.NSTRING, value);
553664
};
@@ -870,12 +981,12 @@ function trimTrailingZeroes(str) {
870981
return str.substring(0, i + 1);
871982
}
872983

873-
function stringToDecimal(str) {
984+
function stringToDecimal(str, maxMantissaLen, fraction, typeStr) {
874985
/* jshint bitwise:false */
875986
var dec = str.match(REGEX.DECIMAL);
876987
// REGEX.DECIMAL will match "." and "" despite these being invalid.
877988
if (!dec || str === "." || str === "") {
878-
throw createInputError('DECIMAL');
989+
throw createInputError(typeStr === undefined ? 'DECIMAL' : typeStr);
879990
}
880991
var sign = dec[1] === '-' ? -1 : 1;
881992
var mInt = dec[2] || '';
@@ -887,14 +998,17 @@ function stringToDecimal(str) {
887998
if(mantissa.length === 0) mantissa = "0";
888999
exp -= mFrac.length
8891000

890-
// round to maxDecimalMantissaLen digits and increment exp appropriately
891-
if(mantissa.length > maxDecimalMantissaLen) {
892-
var followDigit = mantissa[maxDecimalMantissaLen];
893-
exp += (mantissa.length - maxDecimalMantissaLen)
894-
mantissa = mantissa.substring(0, maxDecimalMantissaLen);
895-
if(followDigit > '4') {
1001+
// Fit to maxMantissaLen digits and increment exp appropriately
1002+
if(mantissa.length > maxMantissaLen) {
1003+
var followDigit = mantissa[maxMantissaLen];
1004+
exp += (mantissa.length - maxMantissaLen)
1005+
mantissa = mantissa.substring(0, maxMantissaLen);
1006+
// When writing a floating point decimal (fraction > maxMantissaLen or
1007+
// fraction === undefined), we round to the max mantissa size, but with
1008+
// fixed we truncate to the max mantissa size
1009+
if((fraction === undefined || fraction > maxMantissaLen) && followDigit > '4') {
8961010
// round up
897-
var i = maxDecimalMantissaLen - 1;
1011+
var i = maxMantissaLen - 1;
8981012
while(i >= 0 && mantissa[i] === '9') {
8991013
i -= 1;
9001014
}
@@ -907,9 +1021,9 @@ function stringToDecimal(str) {
9071021
mantissa = mantissa.substring(0, i + 1);
9081022
mantissa = setChar(mantissa, i, String.fromCharCode(mantissa.charCodeAt(i) + 1));
9091023
}
910-
} else if(mantissa[maxDecimalMantissaLen - 1] === '0') {
1024+
} else if(mantissa[maxMantissaLen - 1] === '0') {
9111025
var trimmed = trimTrailingZeroes(mantissa);
912-
exp += (maxDecimalMantissaLen - trimmed.length);
1026+
exp += (maxMantissaLen - trimmed.length);
9131027
mantissa = trimmed;
9141028
}
9151029
}
@@ -921,6 +1035,34 @@ function stringToDecimal(str) {
9211035
};
9221036
}
9231037

1038+
function truncateDecimalToExp(decimal, minExp) {
1039+
var mantissa = decimal.m;
1040+
var exp = decimal.e;
1041+
var calcMaxMantissaLength;
1042+
if (exp < minExp) {
1043+
// Shift the max mantissa length such that the exponent is minExp
1044+
calcMaxMantissaLength = exp + mantissa.length - minExp;
1045+
if (calcMaxMantissaLength <= 0) {
1046+
// All digits are truncated away
1047+
return {s: 1, m: "0", e: minExp};
1048+
}
1049+
}
1050+
1051+
// truncate to calcMaxMantissaLen digits and increment exp appropriately
1052+
if(calcMaxMantissaLength && mantissa.length > calcMaxMantissaLength) {
1053+
exp += (mantissa.length - calcMaxMantissaLength);
1054+
mantissa = mantissa.substring(0, calcMaxMantissaLength);
1055+
// No need to trim trailing zeros since FIXED types will add zeros to
1056+
// match minExp
1057+
}
1058+
1059+
return {
1060+
s: decimal.s,
1061+
m: mantissa,
1062+
e: exp
1063+
};
1064+
}
1065+
9241066
function createInputError(type) {
9251067
return new Error(util.format('Wrong input for %s type', type));
9261068
}

lib/protocol/common/DataFormatVersion.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ module.exports = {
2222
LEVEL5: 5,
2323
LEVEL6: 6,
2424
LEVEL7: 7,
25+
LEVEL8: 8,
2526
// Maximum data format version supported by this driver
26-
MAX_VERSION: 7,
27+
MAX_VERSION: 8,
2728
};

lib/protocol/common/NormalizedTypeCode.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,10 @@ NormalizedTypeCode[TypeCode.DOUBLE] = TypeCode.DOUBLE;
3030
NormalizedTypeCode[TypeCode.REAL] = TypeCode.REAL;
3131
// Decimal
3232
NormalizedTypeCode[TypeCode.DECIMAL] = TypeCode.DECIMAL;
33+
// Fixed
34+
NormalizedTypeCode[TypeCode.FIXED8] = TypeCode.FIXED8;
35+
NormalizedTypeCode[TypeCode.FIXED12] = TypeCode.FIXED12;
36+
NormalizedTypeCode[TypeCode.FIXED16] = TypeCode.FIXED16;
3337
// String
3438
NormalizedTypeCode[TypeCode.STRING] = TypeCode.STRING;
3539
NormalizedTypeCode[TypeCode.VARCHAR1] = TypeCode.STRING;

lib/protocol/common/ReadFunction.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,9 @@ var READ_FLOAT = 'readFloat';
3838
var READ_DECIMAL = 'readDecimal';
3939
var READ_ALPHANUM = 'readAlphanum';
4040
var READ_BOOLEAN = 'readBoolean';
41+
var READ_FIXED8 = 'readFixed8';
42+
var READ_FIXED12 = 'readFixed12';
43+
var READ_FIXED16 = 'readFixed16';
4144

4245
ReadFunction[TypeCode.TINYINT] = READ_TINYINT;
4346
ReadFunction[TypeCode.SMALLINT] = READ_SMALLINT;
@@ -75,3 +78,6 @@ ReadFunction[TypeCode.DECIMAL] = READ_DECIMAL;
7578
ReadFunction[TypeCode.ST_GEOMETRY] = READ_BINARY;
7679
ReadFunction[TypeCode.ST_POINT] = READ_BINARY;
7780
ReadFunction[TypeCode.BOOLEAN] = READ_BOOLEAN;
81+
ReadFunction[TypeCode.FIXED8] = READ_FIXED8;
82+
ReadFunction[TypeCode.FIXED12] = READ_FIXED12;
83+
ReadFunction[TypeCode.FIXED16] = READ_FIXED16;

0 commit comments

Comments
 (0)