Skip to content
Merged
Show file tree
Hide file tree
Changes from 16 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
14 changes: 8 additions & 6 deletions .github/workflows/ci-coverage.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ name: CI - Coverage
on:
pull_request:
push:
branches: [ master ]
branches: [master]

workflow_dispatch:

Expand All @@ -20,14 +20,16 @@ jobs:
fail-fast: false
matrix:
node-version: [20.x]
mysql-version: ["mysql:5.7", "mysql:8.0.33"]
mysql-version: ['mysql:5.7', 'mysql:8.0.33']
use-compression: [0, 1]
use-tls: [0, 1]
mysql_connection_url_key: [""]
static-parser: [0, 1]
mysql_connection_url_key: ['']
env:
MYSQL_CONNECTION_URL: ${{ secrets[matrix.mysql_connection_url_key] }}
STATIC_PARSER: ${{ matrix.static-parser }}

name: Coverage ${{ matrix.node-version }} - DB ${{ matrix.mysql-version }}${{ matrix.mysql_connection_url_key }} - SSL=${{matrix.use-tls}} Compression=${{matrix.use-compression}}
name: Coverage ${{ matrix.node-version }} - DB ${{ matrix.mysql-version }}${{ matrix.mysql_connection_url_key }} - SSL=${{matrix.use-tls}} Compression=${{matrix.use-compression}} Static Parser=${{matrix.static-parser}}

steps:
- uses: actions/checkout@v4
Expand Down Expand Up @@ -62,5 +64,5 @@ jobs:
uses: codecov/codecov-action@v4
with:
token: ${{ secrets.CODECOV_TOKEN }}
flags: compression-${{ matrix.use-compression }},tls-${{ matrix.use-tls }}
name: codecov-umbrella-${{ matrix.node-version }}-${{ matrix.mysql-version }}-compression-${{ matrix.use-compression }}-tls-${{ matrix.use-tls }}
flags: compression-${{ matrix.use-compression }},tls-${{ matrix.use-tls }},static-parser-${{ matrix.static-parser }}
name: codecov-umbrella-${{ matrix.node-version }}-${{ matrix.mysql-version }}-compression-${{ matrix.use-compression }}-tls-${{ matrix.use-tls }}-static-parser-${{ matrix.static-parser }}
31 changes: 23 additions & 8 deletions .github/workflows/ci-linux.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,15 @@ jobs:
mysql-version: ['mysql:8.0.33']
use-compression: [0, 1]
use-tls: [0, 1]
static-parser: [0, 1]
mysql_connection_url_key: ['']
# TODO - add mariadb to the matrix. currently few tests are broken due to mariadb incompatibilities

env:
MYSQL_CONNECTION_URL: ${{ secrets[matrix.mysql_connection_url_key] }}
STATIC_PARSER: ${{ matrix.static-parser }}

name: Node.js ${{ matrix.node-version }} - DB ${{ matrix.mysql-version }}${{ matrix.mysql_connection_url_key }} - SSL=${{matrix.use-tls}} Compression=${{matrix.use-compression}}
name: Node.js ${{ matrix.node-version }} - DB ${{ matrix.mysql-version }}${{ matrix.mysql_connection_url_key }} - SSL=${{matrix.use-tls}} Compression=${{matrix.use-compression}} Static Parser=${{matrix.static-parser}}

steps:
- uses: actions/checkout@v4
Expand Down Expand Up @@ -57,7 +60,7 @@ jobs:

- name: Run tests
run: FILTER=${{matrix.filter}} MYSQL_USE_TLS=${{ matrix.use-tls }} MYSQL_USE_COMPRESSION=${{ matrix.use-compression }} npm run test
timeout-minutes: 5
timeout-minutes: 10

tests-linux-bun:
runs-on: ubuntu-latest
Expand All @@ -68,8 +71,12 @@ jobs:
mysql-version: ['mysql:8.0.33']
use-compression: [0, 1]
use-tls: [0, 1]
static-parser: [0, 1]

env:
STATIC_PARSER: ${{ matrix.static-parser }}

name: Bun ${{ matrix.bun-version }} - DB ${{ matrix.mysql-version }} - SSL=${{matrix.use-tls}} Compression=${{matrix.use-compression}}
name: Bun ${{ matrix.bun-version }} - DB ${{ matrix.mysql-version }} - SSL=${{matrix.use-tls}} Compression=${{matrix.use-compression}} Static Parser=${{matrix.static-parser}}

steps:
- uses: actions/checkout@v4
Expand Down Expand Up @@ -108,7 +115,7 @@ jobs:
MYSQL_USE_TLS: ${{ matrix.use-tls }}
FILTER: test-select-1|test-select-ssl
run: bun run test:bun
timeout-minutes: 1
timeout-minutes: 10

tests-linux-deno-v1:
runs-on: ubuntu-latest
Expand All @@ -118,14 +125,18 @@ jobs:
deno-version: [v1.x]
mysql-version: ['mysql:8.0.33']
use-compression: [0, 1]
static-parser: [0, 1]
# TODO: investigate error when using SSL (1)
#
# errno: -4094
# code: "UNKNOWN"
# syscall: "read"
use-tls: [0]

name: Deno ${{ matrix.deno-version }} - DB ${{ matrix.mysql-version }} - SSL=${{matrix.use-tls}} Compression=${{matrix.use-compression}}
env:
STATIC_PARSER: ${{ matrix.static-parser }}

name: Deno ${{ matrix.deno-version }} - DB ${{ matrix.mysql-version }} - SSL=${{matrix.use-tls}} Compression=${{matrix.use-compression}} Static Parser=${{matrix.static-parser}}

steps:
- uses: actions/checkout@v4
Expand Down Expand Up @@ -162,7 +173,7 @@ jobs:
MYSQL_USE_COMPRESSION: ${{ matrix.use-compression }}
MYSQL_USE_TLS: ${{ matrix.use-tls }}
run: deno task test:deno -- --denoCjs='.js,.cjs'
timeout-minutes: 5
timeout-minutes: 10

tests-linux-deno-v2:
runs-on: ubuntu-latest
Expand All @@ -172,14 +183,18 @@ jobs:
deno-version: [v2.x, canary]
mysql-version: ['mysql:8.0.33']
use-compression: [0, 1]
static-parser: [0, 1]
# TODO: investigate error when using SSL (1)
#
# errno: -4094
# code: "UNKNOWN"
# syscall: "read"
use-tls: [0]

name: Deno ${{ matrix.deno-version }} - DB ${{ matrix.mysql-version }} - SSL=${{matrix.use-tls}} Compression=${{matrix.use-compression}}
env:
STATIC_PARSER: ${{ matrix.static-parser }}

name: Deno ${{ matrix.deno-version }} - DB ${{ matrix.mysql-version }} - SSL=${{matrix.use-tls}} Compression=${{matrix.use-compression}} Static Parser=${{matrix.static-parser}}

steps:
- uses: actions/checkout@v4
Expand Down Expand Up @@ -216,4 +231,4 @@ jobs:
MYSQL_USE_COMPRESSION: ${{ matrix.use-compression }}
MYSQL_USE_TLS: ${{ matrix.use-tls }}
run: deno task test:deno
timeout-minutes: 5
timeout-minutes: 10
7 changes: 6 additions & 1 deletion lib/commands/query.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ const Readable = require('stream').Readable;
const Command = require('./command.js');
const Packets = require('../packets/index.js');
const getTextParser = require('../parsers/text_parser.js');
const staticParser = require('../parsers/static_text_parser.js');
const ServerStatus = require('../constants/server_status.js');

const EmptyPacket = new Packets.Packet(0, Buffer.allocUnsafe(4), 0, 4);
Expand Down Expand Up @@ -212,7 +213,11 @@ class Query extends Command {
if (this._receivedFieldsCount === this._fieldCount) {
const fields = this._fields[this._resultIndex];
this.emit('fields', fields);
this._rowParser = new (getTextParser(fields, this.options, connection.config))(fields);
if (this.options.useStaticParser) {
this._rowParser = staticParser(fields, this.options, connection.config);
} else {
this._rowParser = new (getTextParser(fields, this.options, connection.config))(fields);
}
return Query.prototype.fieldsEOF;
}
return Query.prototype.readField;
Expand Down
2 changes: 2 additions & 0 deletions lib/connection_config.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ const validOptions = {
typeCast: 1,
uri: 1,
user: 1,
useStaticParser: 1,
// These options are used for Pool
connectionLimit: 1,
maxIdle: 1,
Expand Down Expand Up @@ -147,6 +148,7 @@ class ConnectionConfig {
this.nestTables =
options.nestTables === undefined ? undefined : options.nestTables;
this.typeCast = options.typeCast === undefined ? true : options.typeCast;
this.useStaticParser = Boolean(options.useStaticParser);
if (this.timezone[0] === ' ') {
// "+" is a url encoded char for space so it
// gets translated to space when giving a
Expand Down
5 changes: 2 additions & 3 deletions lib/helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -74,14 +74,13 @@ const privateObjectProps = new Set([

exports.privateObjectProps = privateObjectProps;

const fieldEscape = (field) => {
const fieldEscape = (field, isEval = true) => {
if (privateObjectProps.has(field)) {
throw new Error(
`The field name (${field}) can't be the same as an object's private property.`,
);
}

return srcEscape(field);
return isEval ? srcEscape(field) : field;
};

exports.fieldEscape = fieldEscape;
150 changes: 150 additions & 0 deletions lib/parsers/static_text_parser.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
'use strict';

const Types = require('../constants/types.js');
const Charsets = require('../constants/charsets.js');
const helpers = require('../helpers');

const typeNames = [];
for (const t in Types) {
typeNames[Types[t]] = t;
}

function readField({ packet, type, charset, encoding, config, options }) {
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 (type) {
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:
case Types.DECIMAL:
case Types.NEWDECIMAL:
if (config.decimalNumbers) {
return packet.parseLengthCodedFloat();
}
return packet.readLengthCodedString('ascii');
case Types.DATE:
if (helpers.typeMatch(type, dateStrings, Types)) {
return packet.readLengthCodedString('ascii');
}
return packet.parseDate(timezone);
case Types.DATETIME:
case Types.TIMESTAMP:
if (helpers.typeMatch(type, 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 config.jsonStrings
? packet.readLengthCodedString('utf8')
: JSON.parse(packet.readLengthCodedString('utf8'));
default:
if (charset === Charsets.BINARY) {
return packet.readLengthCodedBuffer();
}
return packet.readLengthCodedString(encoding);
}
}

function createTypecastField(field, packet) {
return {
type: typeNames[field.columnType],
length: field.columnLength,
db: field.schema,
table: field.table,
name: field.name,
string: function (encoding = field.encoding) {
if (field.columnType === Types.JSON && encoding === field.encoding) {
// 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/1661
console.warn(
`typeCast: JSON column "${field.name}" is interpreted as BINARY by default, recommended to manually set utf8 encoding: \`field.string("utf8")\``,
);
}
return packet.readLengthCodedString(encoding);
},
buffer: function () {
return packet.readLengthCodedBuffer();
},
geometry: function () {
return packet.parseGeometryValue();
},
};
}

function getTextParser(_fields, _options, config) {
return {
next(packet, fields, options) {
const result = options.rowsAsArray ? [] : {};
for (let i = 0; i < fields.length; i++) {
const field = fields[i];
const typeCast = options.typeCast ? options.typeCast : config.typeCast;
const next = () =>
readField({
packet,
type: field.columnType,
encoding: field.encoding,
charset: field.characterSet,
config,
options,
});

let value;

if (options.typeCast === false) {
value = packet.readLengthCodedBuffer();
} else if (typeof typeCast === 'function') {
value = typeCast(createTypecastField(field, packet), next);
} else {
value = next();
}

if (options.rowsAsArray) {
result.push(value);
} else if (typeof options.nestTables === 'string') {
result[
`${helpers.fieldEscape(field.table, false)}${options.nestTables}${helpers.fieldEscape(field.name, false)}`
] = value;
} else if (options.nestTables) {
const tableName = helpers.fieldEscape(field.table, false);
if (!result[tableName]) {
result[tableName] = {};
}
result[tableName][helpers.fieldEscape(field.name, false)] = value;
} else {
result[helpers.fieldEscape(field.name, false)] = value;
}
}

return result;
},
};
}

module.exports = getTextParser;
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
"lint:docs": "eslint Contributing.md README.md",
"lint:typings": "npx prettier --check ./typings",
"lint:tests": "npx prettier --check ./test",
"test": "poku -d --sequential test/esm test/unit test/integration",
"test": "poku -d -r=verbose --sequential test/esm test/unit test/integration",
"test:bun": "bun poku -d --sequential test/esm test/unit test/integration",
"test:deno": "deno run --allow-read --allow-env --allow-run npm:poku -d --sequential --denoAllow=\"read,env,net,sys\" test/esm test/unit test/integration",
"test:tsc-build": "cd \"test/tsc-build\" && npx tsc -p \"tsconfig.json\"",
Expand Down
6 changes: 6 additions & 0 deletions test/common.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,17 @@ const fs = require('node:fs');
const path = require('node:path');
const process = require('node:process');

const useStaticParser = process.env.STATIC_PARSER === '1';
exports.useStaticParser = useStaticParser;

const config = {
host: process.env.MYSQL_HOST || 'localhost',
user: process.env.MYSQL_USER || 'root',
password: (process.env.CI ? process.env.MYSQL_PASSWORD : '') || '',
database: process.env.MYSQL_DATABASE || 'test',
compress: process.env.MYSQL_USE_COMPRESSION,
port: process.env.MYSQL_PORT || 3306,
useStaticParser,
};

if (process.env.MYSQL_USE_TLS === '1') {
Expand Down Expand Up @@ -109,6 +113,7 @@ exports.createConnection = function (args) {
nestTables: args && args.nestTables,
ssl: (args && args.ssl) ?? config.ssl,
jsonStrings: args && args.jsonStrings,
useStaticParser,
};

const conn = driver.createConnection(params);
Expand Down Expand Up @@ -139,6 +144,7 @@ exports.getConfig = function (input) {
maxIdle: args && args.maxIdle,
idleTimeout: args && args.idleTimeout,
jsonStrings: args && args.jsonStrings,
useStaticParser,
};
return params;
};
Expand Down
Loading
Loading