Skip to content
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

Add .value() method to the typeCast field wrapper #2607

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 79 additions & 0 deletions benchmarks/benchmark-typecast.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
'use strict';

const createConnection = require('../test/common.test.cjs').createConnection;
const connection = createConnection();
const NUM_SAMPLES = 10000;

function typeCastRaw(field, next) {
if (field.type === 'VARCHAR') {
return field.string();
}

if (field.type === 'BINARY') {
return field.buffer().toString('ascii');
}

return next();
}

function typeCastValue(field, next) {
if (field.type === 'VARCHAR') {
return field.value();
}

if (field.type === 'BINARY') {
return field.value().toString('ascii');
}

return next();
}

async function benchmark(iterations, executor, typeCast) {
await new Promise((resolve, reject) => {
connection.query(
'TRUNCATE benchmark_typecast',
(err) => {
if (err) reject(err);
resolve();
},
);
});

await new Promise((resolve, reject) => {
connection.query(
'INSERT INTO benchmark_typecast VALUES ("hello", 0x1234)',
(err) => {
if (err) reject(err);
resolve();
},
);
});

const samples = [];
for (let i = 0; i < iterations; i++) {
const start = Date.now();
await new Promise((resolve, reject) => {
connection[executor]({ sql: `SELECT * FROM benchmark_typecast`, typeCast }, (err) => {
if (err) reject(err);
resolve();
});
});
samples.push(Date.now() - start);
}

console.log(
`${executor} ${typeCast ? typeCast : 'raw'}: AVG ${samples.reduce((a, b) => a + b, 0) / samples.length}ms`,
);
}

connection.query(
'CREATE TEMPORARY TABLE benchmark_typecast (v1 VARCHAR(16), v2 BINARY(4))',
async (err) => {
if (err) throw err;
await benchmark(NUM_SAMPLES, 'query');
await benchmark(NUM_SAMPLES, 'query', typeCastRaw);
await benchmark(NUM_SAMPLES, 'query', typeCastValue);

connection.end();
},
);
74 changes: 74 additions & 0 deletions lib/parsers/binary_parser.js
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,76 @@ function readCodeFor(field, config, options, fieldNum) {
}
}

function readValueFor(field, config, options, packet) {
const supportBigNumbers = Boolean(
options.supportBigNumbers || config.supportBigNumbers,
);
const bigNumberStrings = Boolean(
options.bigNumberStrings || config.bigNumberStrings,
);
const timezone = options.timezone || config.timezone;
const dateStrings = options.dateStrings || config.dateStrings;
const unsigned = field.flags & FieldFlags.UNSIGNED;
switch (field.columnType) {
case Types.TINY:
return unsigned ? packet.readInt8() : packet.readSInt8();
case Types.SHORT:
return unsigned ? packet.readInt16() : packet.readSInt16();
case Types.LONG:
case Types.INT24: // in binary protocol int24 is encoded in 4 bytes int32
return unsigned ? packet.readInt32() : packet.readSInt32();
case Types.YEAR:
return packet.readInt16();
case Types.FLOAT:
return packet.readFloat();
case Types.DOUBLE:
return packet.readDouble();
case Types.NULL:
return null;
case Types.DATE:
case Types.DATETIME:
case Types.TIMESTAMP:
case Types.NEWDATE:
if (helpers.typeMatch(field.columnType, dateStrings, Types)) {
return packet.readDateTimeString(field.decimals);
}
return packet.readDateTime(timezone);
case Types.TIME:
return packet.readTimeString();
case Types.DECIMAL:
case Types.NEWDECIMAL:
if (config.decimalNumbers) {
return packet.parseLengthCodedFloat();
}
return packet.readLengthCodedString("ascii");
case Types.GEOMETRY:
return packet.parseGeometryValue();
case Types.JSON:
// Since for JSON columns mysql always returns charset 63 (BINARY),
// we have to handle it according to JSON specs and use "utf8",
// see https://github.com/sidorares/node-mysql2/issues/409
return JSON.parse(packet.readLengthCodedString("utf8"));
case Types.LONGLONG:
if (!supportBigNumbers) {
return unsigned
? packet.readInt64JSNumber()
: packet.readSInt64JSNumber();
}
if (bigNumberStrings) {
return unsigned
? packet.readInt64String()
: packet.readSInt64String();
}
return unsigned ? packet.readInt64() : packet.readSInt64();

default:
if (field.characterSet === Charsets.BINARY) {
return packet.readLengthCodedBuffer();
}
return packet.readLengthCodedString(field.encoding)
}
}

function compile(fields, options, config) {
const parserFn = genFunc();
const nullBitmapLength = Math.floor((fields.length + 7 + 2) / 8);
Expand Down Expand Up @@ -110,6 +180,9 @@ function compile(fields, options, config) {
geometry: function () {
return packet.parseGeometryValue();
},
value: function () {
return readValueFor(field, config, options, packet);
},
};
}

Expand Down Expand Up @@ -208,6 +281,7 @@ function compile(fields, options, config) {
parserFn.toString(),
);
}

return parserFn.toFunction({ wrap });
}

Expand Down
64 changes: 64 additions & 0 deletions lib/parsers/text_parser.js
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,67 @@ function readCodeFor(type, charset, encodingExpr, config, options) {
}
}

function readValueFor(field, config, options, packet) {
const supportBigNumbers = Boolean(
options.supportBigNumbers || config.supportBigNumbers,
);
const bigNumberStrings = Boolean(
options.bigNumberStrings || config.bigNumberStrings,
);
const timezone = options.timezone || config.timezone;
const dateStrings = options.dateStrings || config.dateStrings;

switch (field.columnType) {
case Types.TINY:
case Types.SHORT:
case Types.LONG:
case Types.INT24:
case Types.YEAR:
return packet.parseLengthCodedIntNoBigCheck();
case Types.LONGLONG:
if (supportBigNumbers && bigNumberStrings) {
return packet.parseLengthCodedIntString();
}
return packet.parseLengthCodedInt(supportBigNumbers);
case Types.FLOAT:
case Types.DOUBLE:
return packet.parseLengthCodedFloat();
case Types.NULL:
return packet.readLengthCodedNumber();
case Types.DECIMAL:
case Types.NEWDECIMAL:
if (config.decimalNumbers) {
return packet.parseLengthCodedFloat();
}
return packet.readLengthCodedString('ascii');
case Types.DATE:
if (helpers.typeMatch(field.columnType, dateStrings, Types)) {
return packet.readLengthCodedString('ascii');
}
return packet.parseDate(timezone);
case Types.DATETIME:
case Types.TIMESTAMP:
if (helpers.typeMatch(field.columnType, dateStrings, Types)) {
return packet.readLengthCodedString('ascii');
}
return packet.parseDateTime(timezone);
case Types.TIME:
return packet.readLengthCodedString('ascii');
case Types.GEOMETRY:
return packet.parseGeometryValue();
case Types.JSON:
// Since for JSON columns mysql always returns charset 63 (BINARY),
// we have to handle it according to JSON specs and use "utf8",
// see https://github.com/sidorares/node-mysql2/issues/409
return JSON.parse(packet.readLengthCodedString('utf8'));
default:
if (field.characterSet === Charsets.BINARY) {
return packet.readLengthCodedBuffer();
}
return packet.readLengthCodedString(field.encoding);
}
}

function compile(fields, options, config) {
// use global typeCast if current query doesn't specify one
if (
Expand Down Expand Up @@ -106,6 +167,9 @@ function compile(fields, options, config) {
geometry: function () {
return _this.packet.parseGeometryValue();
},
value: function () {
return readValueFor(field, config, options, _this.packet);
},
};
}

Expand Down
49 changes: 49 additions & 0 deletions test/integration/connection/test-typecast-values-execute.test.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
'use strict';

const { assert } = require('poku');

const v1 = 'variable len';
const v2 = true;
const v3 = '2024-04-18 15:48:14';
const v4 = '1.23';

function typeCast(field, next) {
if (field.type === 'TINY') {
return field.value() === 1;
}
if (field.type === 'DATETIME') {
return new Date(field.value());
}
return next();
}

function executeTests(res) {
const [{ v1: v1Actual, v2: v2Actual, v3: v3Actual, v4: v4Actual }] = res;
assert.equal(v1Actual, v1);
assert.equal(v2Actual, v2);
assert.equal(v3Actual.getTime(), new Date(v3).getTime());
assert.equal(v4Actual, v4);
}

const common = require('../../common.test.cjs');
const connection = common.createConnection({
typeCast: false,
});

connection.query(
`CREATE TEMPORARY TABLE typecast (v1 VARCHAR(16), v2 TINYINT(1), v3 DATETIME, v4 DECIMAL(10, 2))`,
(err) => {
if (err) throw err;
},
);
connection.query(
`INSERT INTO typecast VALUES ('${v1}', ${v2},'${v3}', ${v4})`,
(err) => {
if (err) throw err;
},
);
connection.execute({ sql: 'SELECT * FROM typecast', typeCast }, (err, res) => {
if (err) throw err;
executeTests(res);
connection.end();
});
49 changes: 49 additions & 0 deletions test/integration/connection/test-typecast-values.test.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
'use strict';

const { assert } = require('poku');

const v1 = 'variable len';
const v2 = true;
const v3 = '2024-04-18 15:48:14';
const v4 = '1.23';

function typeCast(field, next) {
if (field.type === 'TINY') {
return field.value() === 1;
}
if (field.type === 'DATETIME') {
return new Date(field.value());
}
return next();
}

function executeTests(res) {
const [{ v1: v1Actual, v2: v2Actual, v3: v3Actual, v4: v4Actual }] = res;
assert.equal(v1Actual, v1);
assert.equal(v2Actual, v2);
assert.equal(v3Actual.getTime(), new Date(v3).getTime());
assert.equal(v4Actual, v4);
}

const common = require('../../common.test.cjs');
const connection = common.createConnection({
typeCast: false,
});

connection.query(
`CREATE TEMPORARY TABLE typecast (v1 VARCHAR(16), v2 TINYINT(1), v3 DATETIME, v4 DECIMAL(10, 2))`,
(err) => {
if (err) throw err;
},
);
connection.query(
`INSERT INTO typecast VALUES ('${v1}', ${v2},'${v3}', ${v4})`,
(err) => {
if (err) throw err;
},
);
connection.query({ sql: 'SELECT * FROM typecast', typeCast }, (err, res) => {
if (err) throw err;
executeTests(res);
connection.end();
});
Loading