Skip to content

Commit

Permalink
[CONJS-198] new option addition checkNumberRange to works with inse…
Browse files Browse the repository at this point in the history
…rtIdAsNumber/decimalAsNumber/bigIntAsNumber #201
  • Loading branch information
rusher committed Jun 2, 2022
1 parent 268111d commit 6a4e879
Show file tree
Hide file tree
Showing 11 changed files with 347 additions and 83 deletions.
3 changes: 2 additions & 1 deletion documentation/callback-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,13 +52,14 @@ Since 3.x version, driver has reliable default, returning:
* DECIMAL => javascript String
* BIGINT => javascript [BigInt](https://mariadb.com/kb/en/bigint/) object

For compatibility with previous version or mysql/mysql driver, 3 options have been added to return BIGINT/DECIMAL as number, as previous defaults.
For compatibility with previous version or mysql/mysql driver, 4 options have been added to return BIGINT/DECIMAL as number, as previous defaults.

|option|description|type|default|
|---:|---|:---:|:---:|
| **insertIdAsNumber** | Whether the query should return last insert id from INSERT/UPDATE command as BigInt or Number. default return BigInt |*boolean* | false |
| **decimalAsNumber** | Whether the query should return decimal as Number. If enabled, this might return approximate values. |*boolean* | false |
| **bigIntAsNumber** | Whether the query should return BigInt data type as Number. If enabled, this might return approximate values. |*boolean* | false |
| **checkNumberRange** | when used in conjunction of decimalAsNumber, insertIdAsNumber or bigIntAsNumber, if conversion to number is not exact, connector will throw an error (since 3.0.1) |*function*| |

Previous options `supportBigNumbers` and `bigNumberStrings` still exist for compatibility, but are now deprecated.

Expand Down
1 change: 1 addition & 0 deletions documentation/connection-options.md
Original file line number Diff line number Diff line change
Expand Up @@ -394,6 +394,7 @@ mariadb.createConnection({
| **bigNumberStrings** | (deprecated) if set with `supportBigNumbers` DECIMAL/BIGINT data type will be returned as string |*boolean* | false |
| **stream** | permits to set a function with parameter to set stream (since 3.0)|*function*| |
| **bitOneIsBoolean** | return BIT(1) values as boolean |*boolean* | true |
| **checkNumberRange** | when used in conjunction of decimalAsNumber, insertIdAsNumber or bigIntAsNumber, if BigInt conversion to number is not exact, connector will throw an error (since 3.0.1)|*function*| |


### SSH tunnel
Expand Down
3 changes: 2 additions & 1 deletion documentation/promise-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,13 +62,14 @@ Since 3.x version, driver has reliable default, returning:
* DECIMAL => javascript String
* BIGINT => javascript [BigInt](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/BigInt) object

For compatibility with previous version or mysql/mysql driver, 3 options have been added to return BIGINT/DECIMAL as number, as previous defaults.
For compatibility with previous version or mysql/mysql driver, 4 options have been added to return BIGINT/DECIMAL as number, as previous defaults.

|option|description|type|default|
|---:|---|:---:|:---:|
| **insertIdAsNumber** | Whether the query should return last insert id from INSERT/UPDATE command as BigInt or Number. default return BigInt |*boolean* | false |
| **decimalAsNumber** | Whether the query should return decimal as Number. If enabled, this might return approximate values. |*boolean* | false |
| **bigIntAsNumber** | Whether the query should return BigInt data type as Number. If enabled, this might return approximate values. |*boolean* | false |
| **checkNumberRange** | when used in conjunction of decimalAsNumber, insertIdAsNumber or bigIntAsNumber, if conversion to number is not exact, connector will throw an error (since 3.0.1) |*function*| |

Previous options `supportBigNumbers` and `bigNumberStrings` still exist for compatibility, but are now deprecated.

Expand Down
149 changes: 112 additions & 37 deletions lib/cmd/column-definition.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ const Collations = require('../const/collations.js');
const FieldType = require('../const/field-type');
const FieldDetails = require('../const/field-detail');
const Capabilities = require('../const/capabilities');
const Errors = require('../misc/errors');

// noinspection JSBitwiseOperatorUsage
/**
Expand Down Expand Up @@ -52,22 +53,26 @@ class ColumnDef {
switch (this.columnType) {
case FieldType.TINY:
if (this.signed()) {
return (packet, index, nullBitmap, opts) => (isNullBitmap(index, nullBitmap) ? null : packet.readInt8());
return (packet, index, nullBitmap, opts, throwUnexpectedError) =>
isNullBitmap(index, nullBitmap) ? null : packet.readInt8();
} else {
return (packet, index, nullBitmap, opts) => (isNullBitmap(index, nullBitmap) ? null : packet.readUInt8());
return (packet, index, nullBitmap, opts, throwUnexpectedError) =>
isNullBitmap(index, nullBitmap) ? null : packet.readUInt8();
}

case FieldType.YEAR:
case FieldType.SHORT:
if (this.signed()) {
return (packet, index, nullBitmap, opts) => (isNullBitmap(index, nullBitmap) ? null : packet.readInt16());
return (packet, index, nullBitmap, opts, throwUnexpectedError) =>
isNullBitmap(index, nullBitmap) ? null : packet.readInt16();
} else {
return (packet, index, nullBitmap, opts) => (isNullBitmap(index, nullBitmap) ? null : packet.readUInt16());
return (packet, index, nullBitmap, opts, throwUnexpectedError) =>
isNullBitmap(index, nullBitmap) ? null : packet.readUInt16();
}

case FieldType.INT24:
if (this.signed()) {
return (packet, index, nullBitmap, opts) => {
return (packet, index, nullBitmap, opts, throwUnexpectedError) => {
if (isNullBitmap(index, nullBitmap)) {
return null;
}
Expand All @@ -76,7 +81,7 @@ class ColumnDef {
return result;
};
} else {
return (packet, index, nullBitmap, opts) => {
return (packet, index, nullBitmap, opts, throwUnexpectedError) => {
if (isNullBitmap(index, nullBitmap)) {
return null;
}
Expand All @@ -88,22 +93,35 @@ class ColumnDef {

case FieldType.INT:
if (this.signed()) {
return (packet, index, nullBitmap, opts) => (isNullBitmap(index, nullBitmap) ? null : packet.readInt32());
return (packet, index, nullBitmap, opts, throwUnexpectedError) =>
isNullBitmap(index, nullBitmap) ? null : packet.readInt32();
} else {
return (packet, index, nullBitmap, opts) => (isNullBitmap(index, nullBitmap) ? null : packet.readUInt32());
return (packet, index, nullBitmap, opts, throwUnexpectedError) =>
isNullBitmap(index, nullBitmap) ? null : packet.readUInt32();
}

case FieldType.FLOAT:
return (packet, index, nullBitmap, opts) => (isNullBitmap(index, nullBitmap) ? null : packet.readFloat());
return (packet, index, nullBitmap, opts, throwUnexpectedError) =>
isNullBitmap(index, nullBitmap) ? null : packet.readFloat();

case FieldType.DOUBLE:
return (packet, index, nullBitmap, opts) => (isNullBitmap(index, nullBitmap) ? null : packet.readDouble());
return (packet, index, nullBitmap, opts, throwUnexpectedError) =>
isNullBitmap(index, nullBitmap) ? null : packet.readDouble();

case FieldType.BIGINT:
return (packet, index, nullBitmap, opts) => {
return (packet, index, nullBitmap, opts, throwUnexpectedError) => {
if (isNullBitmap(index, nullBitmap)) return null;
const val = this.signed() ? packet.readBigInt64() : packet.readBigUInt64();
if (val != null && (opts.bigIntAsNumber || opts.supportBigNumbers)) {
if (opts.bigIntAsNumber && opts.checkNumberRange && !Number.isSafeInteger(Number(val))) {
return throwUnexpectedError(
`value ${val} can't safely be converted to number`,
false,
null,
'42000',
Errors.ER_PARSING_PRECISION
);
}
if (opts.supportBigNumbers && (opts.bigNumberStrings || !Number.isSafeInteger(Number(val)))) {
return val.toString();
}
Expand All @@ -113,21 +131,45 @@ class ColumnDef {
};

case FieldType.DATE:
return (packet, index, nullBitmap, opts) =>
return (packet, index, nullBitmap, opts, throwUnexpectedError) =>
isNullBitmap(index, nullBitmap) ? null : packet.readBinaryDate(opts);

case FieldType.DATETIME:
case FieldType.TIMESTAMP:
return (packet, index, nullBitmap, opts) =>
return (packet, index, nullBitmap, opts, throwUnexpectedError) =>
isNullBitmap(index, nullBitmap) ? null : packet.readBinaryDateTime(opts, this);

case FieldType.TIME:
return (packet, index, nullBitmap, opts) =>
return (packet, index, nullBitmap, opts, throwUnexpectedError) =>
isNullBitmap(index, nullBitmap) ? null : packet.readBinaryTime();

case FieldType.DECIMAL:
case FieldType.NEWDECIMAL:
return (packet, index, nullBitmap, opts) => {
if (this.scale == 0) {
//checkNumberRange additional check is only done when
// resulting value is an integer
return (packet, index, nullBitmap, opts, throwUnexpectedError) => {
if (isNullBitmap(index, nullBitmap)) return null;
const valDec = packet.readDecimalLengthEncoded();
if (valDec != null && (opts.decimalAsNumber || opts.supportBigNumbers)) {
if (opts.decimalAsNumber && opts.checkNumberRange && !Number.isSafeInteger(Number(valDec))) {
return throwUnexpectedError(
`value ${valDec} can't safely be converted to number`,
false,
null,
'42000',
Errors.ER_PARSING_PRECISION
);
}
if (opts.supportBigNumbers && (opts.bigNumberStrings || !Number.isSafeInteger(Number(valDec)))) {
return valDec.toString();
}
return Number(valDec);
}
return valDec;
};
}
return (packet, index, nullBitmap, opts, throwUnexpectedError) => {
if (isNullBitmap(index, nullBitmap)) return null;
const valDec = packet.readDecimalLengthEncoded();
if (valDec != null && (opts.decimalAsNumber || opts.supportBigNumbers)) {
Expand All @@ -141,7 +183,7 @@ class ColumnDef {

case FieldType.GEOMETRY:
let defaultVal = this.__getDefaultGeomVal();
return (packet, index, nullBitmap, opts) => {
return (packet, index, nullBitmap, opts, throwUnexpectedError) => {
if (isNullBitmap(index, nullBitmap)) {
return defaultVal;
}
Expand All @@ -150,36 +192,36 @@ class ColumnDef {

case FieldType.JSON:
//for mysql only => parse string as JSON object
return (packet, index, nullBitmap, opts) =>
return (packet, index, nullBitmap, opts, throwUnexpectedError) =>
isNullBitmap(index, nullBitmap) ? null : JSON.parse(packet.readStringLengthEncoded());

case FieldType.BIT:
if (this.columnLength === 1 && opts.bitOneIsBoolean) {
return (packet, index, nullBitmap, opts) =>
return (packet, index, nullBitmap, opts, throwUnexpectedError) =>
isNullBitmap(index, nullBitmap) ? null : packet.readBufferLengthEncoded()[0] === 1;
}
return (packet, index, nullBitmap, opts) =>
return (packet, index, nullBitmap, opts, throwUnexpectedError) =>
isNullBitmap(index, nullBitmap) ? null : packet.readBufferLengthEncoded();

default:
if (this.dataTypeFormat && this.dataTypeFormat === 'json' && opts.autoJsonMap) {
return (packet, index, nullBitmap, opts) =>
return (packet, index, nullBitmap, opts, throwUnexpectedError) =>
isNullBitmap(index, nullBitmap) ? null : JSON.parse(packet.readStringLengthEncoded());
}

if (this.collation.index === 63) {
return (packet, index, nullBitmap, opts) =>
return (packet, index, nullBitmap, opts, throwUnexpectedError) =>
isNullBitmap(index, nullBitmap) ? null : packet.readBufferLengthEncoded();
}

if (this.isSet()) {
return (packet, index, nullBitmap, opts) => {
return (packet, index, nullBitmap, opts, throwUnexpectedError) => {
if (isNullBitmap(index, nullBitmap)) return null;
const string = packet.readStringLengthEncoded();
return string == null ? null : string === '' ? [] : string.split(',');
};
}
return (packet, index, nullBitmap, opts) =>
return (packet, index, nullBitmap, opts, throwUnexpectedError) =>
isNullBitmap(index, nullBitmap) ? null : packet.readStringLengthEncoded();
}
} else {
Expand All @@ -189,16 +231,25 @@ class ColumnDef {
case FieldType.INT:
case FieldType.INT24:
case FieldType.YEAR:
return (packet, index, nullBitmap, opts) => packet.readIntLengthEncoded();
return (packet, index, nullBitmap, opts, throwUnexpectedError) => packet.readIntLengthEncoded();

case FieldType.FLOAT:
case FieldType.DOUBLE:
return (packet, index, nullBitmap, opts) => packet.readFloatLengthCoded();
return (packet, index, nullBitmap, opts, throwUnexpectedError) => packet.readFloatLengthCoded();

case FieldType.BIGINT:
return (packet, index, nullBitmap, opts) => {
return (packet, index, nullBitmap, opts, throwUnexpectedError) => {
const val = packet.readBigIntLengthEncoded();
if (val != null && (opts.bigIntAsNumber || opts.supportBigNumbers)) {
if (opts.bigIntAsNumber && opts.checkNumberRange && !Number.isSafeInteger(Number(val))) {
return throwUnexpectedError(
`value ${val} can't safely be converted to number`,
false,
null,
'42000',
Errors.ER_PARSING_PRECISION
);
}
if (opts.supportBigNumbers && (opts.bigNumberStrings || !Number.isSafeInteger(Number(val)))) {
return val.toString();
}
Expand All @@ -209,7 +260,29 @@ class ColumnDef {

case FieldType.DECIMAL:
case FieldType.NEWDECIMAL:
return (packet, index, nullBitmap, opts) => {
if (this.scale == 0) {
// this is an exact integer
return (packet, index, nullBitmap, opts, throwUnexpectedError) => {
const valDec = packet.readDecimalLengthEncoded();
if (valDec != null && (opts.decimalAsNumber || opts.supportBigNumbers)) {
if (opts.decimalAsNumber && opts.checkNumberRange && !Number.isSafeInteger(Number(valDec))) {
return throwUnexpectedError(
`value ${valDec} can't safely be converted to number`,
false,
null,
'42000',
Errors.ER_PARSING_PRECISION
);
}
if (opts.supportBigNumbers && (opts.bigNumberStrings || !Number.isSafeInteger(Number(valDec)))) {
return valDec.toString();
}
return Number(valDec);
}
return valDec;
};
}
return (packet, index, nullBitmap, opts, throwUnexpectedError) => {
const valDec = packet.readDecimalLengthEncoded();
if (valDec != null && (opts.decimalAsNumber || opts.supportBigNumbers)) {
if (opts.supportBigNumbers && (opts.bigNumberStrings || !Number.isSafeInteger(Number(valDec)))) {
Expand All @@ -221,7 +294,7 @@ class ColumnDef {
};

case FieldType.DATE:
return (packet, index, nullBitmap, opts) => {
return (packet, index, nullBitmap, opts, throwUnexpectedError) => {
if (opts.dateStrings) {
return packet.readAsciiStringLengthEncoded();
}
Expand All @@ -230,23 +303,24 @@ class ColumnDef {

case FieldType.DATETIME:
case FieldType.TIMESTAMP:
return (packet, index, nullBitmap, opts) => {
return (packet, index, nullBitmap, opts, throwUnexpectedError) => {
if (opts.dateStrings) {
return packet.readAsciiStringLengthEncoded();
}
return packet.readDateTime(opts);
};

case FieldType.TIME:
return (packet, index, nullBitmap, opts) => packet.readAsciiStringLengthEncoded();
return (packet, index, nullBitmap, opts, throwUnexpectedError) => packet.readAsciiStringLengthEncoded();

case FieldType.GEOMETRY:
let defaultVal = this.__getDefaultGeomVal();
return (packet, index, nullBitmap, opts) => packet.readGeometry(defaultVal);
return (packet, index, nullBitmap, opts, throwUnexpectedError) => packet.readGeometry(defaultVal);

case FieldType.JSON:
//for mysql only => parse string as JSON object
return (packet, index, nullBitmap, opts) => JSON.parse(packet.readStringLengthEncoded());
return (packet, index, nullBitmap, opts, throwUnexpectedError) =>
JSON.parse(packet.readStringLengthEncoded());

case FieldType.BIT:
if (this.columnLength === 1 && opts.bitOneIsBoolean) {
Expand All @@ -255,24 +329,25 @@ class ColumnDef {
return val == null ? null : val[0] === 1;
};
}
return (packet, index, nullBitmap, opts) => packet.readBufferLengthEncoded();
return (packet, index, nullBitmap, opts, throwUnexpectedError) => packet.readBufferLengthEncoded();

default:
if (this.dataTypeFormat && this.dataTypeFormat === 'json' && opts.autoJsonMap) {
return (packet, index, nullBitmap, opts) => JSON.parse(packet.readStringLengthEncoded());
return (packet, index, nullBitmap, opts, throwUnexpectedError) =>
JSON.parse(packet.readStringLengthEncoded());
}

if (this.collation.index === 63) {
return (packet, index, nullBitmap, opts) => packet.readBufferLengthEncoded();
return (packet, index, nullBitmap, opts, throwUnexpectedError) => packet.readBufferLengthEncoded();
}

if (this.isSet()) {
return (packet, index, nullBitmap, opts) => {
return (packet, index, nullBitmap, opts, throwUnexpectedError) => {
const string = packet.readStringLengthEncoded();
return string == null ? null : string === '' ? [] : string.split(',');
};
}
return (packet, index, nullBitmap, opts) => packet.readStringLengthEncoded();
return (packet, index, nullBitmap, opts, throwUnexpectedError) => packet.readStringLengthEncoded();
}
}
}
Expand Down
6 changes: 5 additions & 1 deletion lib/cmd/command.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ class Command extends EventEmitter {
this.resolve = resolve;
this.reject = reject;
this.sending = false;
this.unexpectedError = this.throwUnexpectedError.bind(this);
}

displaySql() {
Expand Down Expand Up @@ -111,12 +112,15 @@ class Command extends EventEmitter {

const affectedRows = packet.readUnsignedLength();
let insertId = packet.readSignedLengthBigInt();
info.status = packet.readUInt16();
if (insertId != null && (opts.supportBigNumbers || opts.insertIdAsNumber)) {
if (opts.insertIdAsNumber && opts.checkNumberRange && !Number.isSafeInteger(Number(insertId))) {
throw new Error(`last insert id value ${insertId} can't safely be converted to number`);
}
if (opts.supportBigNumbers && (opts.bigNumberStrings || !Number.isSafeInteger(Number(insertId)))) {
insertId = insertId.toString();
} else insertId = Number(insertId);
}
info.status = packet.readUInt16();

const okPacket = new OkPacket(affectedRows, insertId, packet.readUInt16());

Expand Down
Loading

0 comments on commit 6a4e879

Please sign in to comment.