创建并连接数据库,并修改登录和注册接口

This commit is contained in:
2026-08-30 18:14:49 +08:00
parent 4d11a1237f
commit 51c41e8fb7
331 changed files with 90228 additions and 16 deletions
+35
View File
@@ -0,0 +1,35 @@
// Copyright (c) 2021, Oracle and/or its affiliates.
'use strict';
const Packet = require('../packets/packet');
class AuthNextFactor {
constructor(opts) {
this.pluginName = opts.pluginName;
this.pluginData = opts.pluginData;
}
toPacket(encoding) {
const length = 6 + this.pluginName.length + this.pluginData.length;
const buffer = Buffer.allocUnsafe(length);
const packet = new Packet(0, buffer, 0, length);
packet.offset = 4;
packet.writeInt8(0x02);
packet.writeNullTerminatedString(this.pluginName, encoding);
packet.writeBuffer(this.pluginData);
return packet;
}
static fromPacket(packet, encoding) {
packet.readInt8(); // marker
const name = packet.readNullTerminatedString(encoding);
const data = packet.readBuffer();
return new AuthNextFactor({
pluginName: name,
pluginData: data,
});
}
}
module.exports = AuthNextFactor;
+38
View File
@@ -0,0 +1,38 @@
'use strict';
// http://dev.mysql.com/doc/internals/en/connection-phase-packets.html#packet-Protocol::AuthSwitchRequest
const Packet = require('../packets/packet');
class AuthSwitchRequest {
constructor(opts) {
this.pluginName = opts.pluginName;
this.pluginData = opts.pluginData;
}
toPacket() {
const length = 6 + this.pluginName.length + this.pluginData.length;
const buffer = Buffer.allocUnsafe(length);
const packet = new Packet(0, buffer, 0, length);
packet.offset = 4;
packet.writeInt8(0xfe);
// TODO: use server encoding
packet.writeNullTerminatedString(this.pluginName, 'cesu8');
packet.writeBuffer(this.pluginData);
return packet;
}
static fromPacket(packet) {
packet.readInt8(); // marker
// assert marker == 0xfe?
// TODO: use server encoding
const name = packet.readNullTerminatedString('cesu8');
const data = packet.readBuffer();
return new AuthSwitchRequest({
pluginName: name,
pluginData: data,
});
}
}
module.exports = AuthSwitchRequest;
+33
View File
@@ -0,0 +1,33 @@
'use strict';
// http://dev.mysql.com/doc/internals/en/connection-phase-packets.html#packet-Protocol::AuthSwitchRequest
const Packet = require('../packets/packet');
class AuthSwitchRequestMoreData {
constructor(data) {
this.data = data;
}
toPacket() {
const length = 5 + this.data.length;
const buffer = Buffer.allocUnsafe(length);
const packet = new Packet(0, buffer, 0, length);
packet.offset = 4;
packet.writeInt8(0x01);
packet.writeBuffer(this.data);
return packet;
}
static fromPacket(packet) {
packet.readInt8(); // marker
const data = packet.readBuffer();
return new AuthSwitchRequestMoreData(data);
}
static verifyMarker(packet) {
return packet.peekByte() === 0x01;
}
}
module.exports = AuthSwitchRequestMoreData;
+30
View File
@@ -0,0 +1,30 @@
'use strict';
// http://dev.mysql.com/doc/internals/en/connection-phase-packets.html#packet-Protocol::AuthSwitchRequest
const Packet = require('../packets/packet');
class AuthSwitchResponse {
constructor(data) {
if (!Buffer.isBuffer(data)) {
data = Buffer.from(data);
}
this.data = data;
}
toPacket() {
const length = 4 + this.data.length;
const buffer = Buffer.allocUnsafe(length);
const packet = new Packet(0, buffer, 0, length);
packet.offset = 4;
packet.writeBuffer(this.data);
return packet;
}
static fromPacket(packet) {
const data = packet.readBuffer();
return new AuthSwitchResponse(data);
}
}
module.exports = AuthSwitchResponse;
+95
View File
@@ -0,0 +1,95 @@
'use strict';
const Types = require('../constants/types');
const Packet = require('../packets/packet');
const binaryReader = new Array(256);
class BinaryRow {
constructor(columns) {
this.columns = columns || [];
}
static toPacket(columns, encoding) {
// throw new Error('Not implemented');
const sequenceId = 0; // TODO remove, this is calculated now in connection
let length = 0;
columns.forEach((val) => {
if (val === null || typeof val === 'undefined') {
++length;
return;
}
length += Packet.lengthCodedStringLength(val.toString(10), encoding);
});
length = length + 2;
const buffer = Buffer.allocUnsafe(length + 4);
const packet = new Packet(sequenceId, buffer, 0, length + 4);
packet.offset = 4;
packet.writeInt8(0);
let bitmap = 0;
let bitValue = 1;
columns.forEach((parameter) => {
if (parameter.type === Types.NULL) {
bitmap += bitValue;
}
bitValue *= 2;
if (bitValue === 256) {
packet.writeInt8(bitmap);
bitmap = 0;
bitValue = 1;
}
});
if (bitValue !== 1) {
packet.writeInt8(bitmap);
}
columns.forEach((val) => {
if (val === null) {
packet.writeNull();
return;
}
if (typeof val === 'undefined') {
packet.writeInt8(0);
return;
}
packet.writeLengthCodedString(val.toString(10), encoding);
});
return packet;
}
// TODO: complete list of types...
static fromPacket(fields, packet) {
const columns = new Array(fields.length);
packet.readInt8(); // TODO check it's 0
const nullBitmapLength = Math.floor((fields.length + 7 + 2) / 8);
// TODO: read and interpret null bitmap
packet.skip(nullBitmapLength);
for (let i = 0; i < columns.length; ++i) {
columns[i] = binaryReader[fields[i].columnType].apply(packet);
}
return new BinaryRow(columns);
}
}
// TODO: replace with constants.MYSQL_TYPE_*
binaryReader[Types.DECIMAL] = Packet.prototype.readLengthCodedString;
binaryReader[1] = Packet.prototype.readInt8; // tiny
binaryReader[2] = Packet.prototype.readInt16; // short
binaryReader[3] = Packet.prototype.readInt32; // long
binaryReader[4] = Packet.prototype.readFloat; // float
binaryReader[5] = Packet.prototype.readDouble; // double
binaryReader[6] = Packet.prototype.assertInvalid; // null, should be skipped via null bitmap
binaryReader[7] = Packet.prototype.readTimestamp; // timestamp, http://dev.mysql.com/doc/internals/en/prepared-statements.html#packet-ProtocolBinary::MYSQL_TYPE_TIMESTAMP
binaryReader[8] = Packet.prototype.readInt64; // long long
binaryReader[9] = Packet.prototype.readInt32; // int24
binaryReader[10] = Packet.prototype.readTimestamp; // date
binaryReader[11] = Packet.prototype.readTime; // time, http://dev.mysql.com/doc/internals/en/prepared-statements.html#packet-ProtocolBinary::MYSQL_TYPE_TIME
binaryReader[12] = Packet.prototype.readDateTime; // datetime, http://dev.mysql.com/doc/internals/en/prepared-statements.html#packet-ProtocolBinary::MYSQL_TYPE_DATETIME
binaryReader[13] = Packet.prototype.readInt16; // year
binaryReader[Types.VAR_STRING] = Packet.prototype.readLengthCodedString; // var string
module.exports = BinaryRow;
+33
View File
@@ -0,0 +1,33 @@
'use strict';
// http://dev.mysql.com/doc/internals/en/com-binlog-dump.html#packet-COM_BINLOG_DUMP
const Packet = require('../packets/packet');
const CommandCodes = require('../constants/commands');
// TODO: add flag to constants
// 0x01 - BINLOG_DUMP_NON_BLOCK
// send EOF instead of blocking
class BinlogDump {
constructor(opts) {
this.binlogPos = opts.binlogPos || 0;
this.serverId = opts.serverId || 0;
this.flags = opts.flags || 0;
this.filename = opts.filename || '';
}
toPacket() {
const length = 15 + Buffer.byteLength(this.filename, 'utf8'); // TODO: should be ascii?
const buffer = Buffer.allocUnsafe(length);
const packet = new Packet(0, buffer, 0, length);
packet.offset = 4;
packet.writeInt8(CommandCodes.BINLOG_DUMP);
packet.writeInt32(this.binlogPos);
packet.writeInt16(this.flags);
packet.writeInt32(this.serverId);
packet.writeString(this.filename);
return packet;
}
}
module.exports = BinlogDump;
+115
View File
@@ -0,0 +1,115 @@
'use strict';
// http://dev.mysql.com/doc/internals/en/query-event.html
const keys = {
FLAGS2: 0,
SQL_MODE: 1,
CATALOG: 2,
AUTO_INCREMENT: 3,
CHARSET: 4,
TIME_ZONE: 5,
CATALOG_NZ: 6,
LC_TIME_NAMES: 7,
CHARSET_DATABASE: 8,
TABLE_MAP_FOR_UPDATE: 9,
MASTER_DATA_WRITTEN: 10,
INVOKERS: 11,
UPDATED_DB_NAMES: 12,
MICROSECONDS: 3,
};
module.exports = function parseStatusVars(buffer) {
const result = {};
let offset = 0;
let key, length, prevOffset;
while (offset < buffer.length) {
key = buffer[offset++];
switch (key) {
case keys.FLAGS2:
result.flags = buffer.readUInt32LE(offset);
offset += 4;
break;
case keys.SQL_MODE:
// value is 8 bytes, but all documented flags are in first 4 bytes
result.sqlMode = buffer.readUInt32LE(offset);
offset += 8;
break;
case keys.CATALOG:
length = buffer[offset++];
result.catalog = buffer.toString('utf8', offset, offset + length);
offset += length + 1; // null byte after string
break;
case keys.CHARSET:
result.clientCharset = buffer.readUInt16LE(offset);
result.connectionCollation = buffer.readUInt16LE(offset + 2);
result.serverCharset = buffer.readUInt16LE(offset + 4);
offset += 6;
break;
case keys.TIME_ZONE:
length = buffer[offset++];
result.timeZone = buffer.toString('utf8', offset, offset + length);
offset += length; // no null byte
break;
case keys.CATALOG_NZ:
length = buffer[offset++];
result.catalogNz = buffer.toString('utf8', offset, offset + length);
offset += length; // no null byte
break;
case keys.LC_TIME_NAMES:
result.lcTimeNames = buffer.readUInt16LE(offset);
offset += 2;
break;
case keys.CHARSET_DATABASE:
result.schemaCharset = buffer.readUInt16LE(offset);
offset += 2;
break;
case keys.TABLE_MAP_FOR_UPDATE:
result.mapForUpdate1 = buffer.readUInt32LE(offset);
result.mapForUpdate2 = buffer.readUInt32LE(offset + 4);
offset += 8;
break;
case keys.MASTER_DATA_WRITTEN:
result.masterDataWritten = buffer.readUInt32LE(offset);
offset += 4;
break;
case keys.INVOKERS:
length = buffer[offset++];
result.invokerUsername = buffer.toString(
'utf8',
offset,
offset + length
);
offset += length;
length = buffer[offset++];
result.invokerHostname = buffer.toString(
'utf8',
offset,
offset + length
);
offset += length;
break;
case keys.UPDATED_DB_NAMES:
length = buffer[offset++];
// length - number of null-terminated strings
result.updatedDBs = []; // we'll store them as array here
for (; length; --length) {
prevOffset = offset;
// fast forward to null terminating byte
while (buffer[offset++] && offset < buffer.length) {
// empty body, everything inside while condition
}
result.updatedDBs.push(
buffer.toString('utf8', prevOffset, offset - 1)
);
}
break;
case keys.MICROSECONDS:
result.microseconds =
// REVIEW: INVALID UNKNOWN VARIABLE!
buffer.readInt16LE(offset) + (buffer[offset + 2] << 16);
offset += 3;
}
}
return result;
};
+97
View File
@@ -0,0 +1,97 @@
'use strict';
const CommandCode = require('../constants/commands.js');
const ClientConstants = require('../constants/client.js');
const Packet = require('../packets/packet.js');
const auth41 = require('../auth_41.js');
const CharsetToEncoding = require('../constants/charset_encodings.js');
// https://dev.mysql.com/doc/internals/en/com-change-user.html#packet-COM_CHANGE_USER
class ChangeUser {
constructor(opts) {
this.flags = opts.flags;
this.user = opts.user || '';
this.database = opts.database || '';
this.password = opts.password || '';
this.passwordSha1 = opts.passwordSha1;
this.authPluginData1 = opts.authPluginData1;
this.authPluginData2 = opts.authPluginData2;
this.connectAttributes = opts.connectAttributes || {};
let authToken;
if (this.passwordSha1) {
authToken = auth41.calculateTokenFromPasswordSha(
this.passwordSha1,
this.authPluginData1,
this.authPluginData2
);
} else {
authToken = auth41.calculateToken(
this.password,
this.authPluginData1,
this.authPluginData2
);
}
this.authToken = authToken;
this.charsetNumber = opts.charsetNumber;
}
// TODO
// ChangeUser.fromPacket = function(packet)
// };
serializeToBuffer(buffer) {
const isSet = (flag) => this.flags & ClientConstants[flag];
const packet = new Packet(0, buffer, 0, buffer.length);
packet.offset = 4;
const encoding = CharsetToEncoding[this.charsetNumber];
packet.writeInt8(CommandCode.CHANGE_USER);
packet.writeNullTerminatedString(this.user, encoding);
if (isSet('SECURE_CONNECTION')) {
packet.writeInt8(this.authToken.length);
packet.writeBuffer(this.authToken);
} else {
packet.writeBuffer(this.authToken);
packet.writeInt8(0);
}
packet.writeNullTerminatedString(this.database, encoding);
packet.writeInt16(this.charsetNumber);
if (isSet('PLUGIN_AUTH')) {
// TODO: read this from parameters
packet.writeNullTerminatedString('mysql_native_password', 'latin1');
}
if (isSet('CONNECT_ATTRS')) {
const connectAttributes = this.connectAttributes;
const attrNames = Object.keys(connectAttributes);
let keysLength = 0;
for (let k = 0; k < attrNames.length; ++k) {
keysLength += Packet.lengthCodedStringLength(attrNames[k], encoding);
keysLength += Packet.lengthCodedStringLength(
connectAttributes[attrNames[k]],
encoding
);
}
packet.writeLengthCodedNumber(keysLength);
for (let k = 0; k < attrNames.length; ++k) {
packet.writeLengthCodedString(attrNames[k], encoding);
packet.writeLengthCodedString(
connectAttributes[attrNames[k]],
encoding
);
}
}
return packet;
}
toPacket() {
if (typeof this.user !== 'string') {
throw new Error('"user" connection config property must be a string');
}
if (typeof this.database !== 'string') {
throw new Error('"database" connection config property must be a string');
}
// dry run: calculate resulting packet length
const p = this.serializeToBuffer(Packet.MockBuffer());
return this.serializeToBuffer(Buffer.allocUnsafe(p.offset));
}
}
module.exports = ChangeUser;
+21
View File
@@ -0,0 +1,21 @@
'use strict';
const Packet = require('../packets/packet');
const CommandCodes = require('../constants/commands');
class CloseStatement {
constructor(id) {
this.id = id;
}
// note: no response sent back
toPacket() {
const packet = new Packet(0, Buffer.allocUnsafe(9), 0, 9);
packet.offset = 4;
packet.writeInt8(CommandCodes.STMT_CLOSE);
packet.writeInt32(this.id);
return packet;
}
}
module.exports = CloseStatement;
+326
View File
@@ -0,0 +1,326 @@
'use strict';
const Packet = require('../packets/packet');
const StringParser = require('../parsers/string');
const CharsetToEncoding = require('../constants/charset_encodings.js');
const fields = ['catalog', 'schema', 'table', 'orgTable', 'name', 'orgName'];
// creating JS string is relatively expensive (compared to
// reading few bytes from buffer) because all string properties
// except for name are unlikely to be used we postpone
// string conversion until property access
//
// TODO: watch for integration benchmarks (one with real network buffer)
// there could be bad side effect as keeping reference to a buffer makes it
// sit in the memory longer (usually until final .query() callback)
// Latest v8 perform much better in regard to bufferer -> string conversion,
// at some point of time this optimisation might become unnecessary
// see https://github.com/sidorares/node-mysql2/pull/137
//
class ColumnDefinition {
constructor(packet, clientEncoding, mariadbExtendedMetadata) {
this._buf = packet.buffer;
this._clientEncoding = clientEncoding;
this._catalogLength = packet.readLengthCodedNumber();
this._catalogStart = packet.offset;
packet.offset += this._catalogLength;
this._schemaLength = packet.readLengthCodedNumber();
this._schemaStart = packet.offset;
packet.offset += this._schemaLength;
this._tableLength = packet.readLengthCodedNumber();
this._tableStart = packet.offset;
packet.offset += this._tableLength;
this._orgTableLength = packet.readLengthCodedNumber();
this._orgTableStart = packet.offset;
packet.offset += this._orgTableLength;
// name is always used, don't make it lazy
const _nameLength = packet.readLengthCodedNumber();
const _nameStart = packet.offset;
packet.offset += _nameLength;
this._orgNameLength = packet.readLengthCodedNumber();
this._orgNameStart = packet.offset;
packet.offset += this._orgNameLength;
// MariaDB-specific column types (UUID, INET4, INET6, VECTOR and the JSON
// alias) are sent as standard string/blob types on the wire; the real
// type is only visible here, in the extended metadata block negotiated
// via the MARIADB_CLIENT_EXTENDED_METADATA capability (parsed out of
// line and with prototype defaults to keep this hot constructor small)
if (mariadbExtendedMetadata) {
this._parseMariadbExtendedMetadata(packet);
}
packet.skip(1); // length of the following fields (always 0x0c)
this.characterSet = packet.readInt16();
this.encoding = CharsetToEncoding[this.characterSet];
this.name = StringParser.decode(
this._buf,
this.encoding === 'binary' ? this._clientEncoding : this.encoding,
_nameStart,
_nameStart + _nameLength
);
this.columnLength = packet.readInt32();
this.columnType = packet.readInt8();
this.type = this.columnType;
this.flags = packet.readInt16();
this.decimals = packet.readInt8();
}
_parseMariadbExtendedMetadata(packet) {
// length-encoded block of { int<1> id, string<lenenc> value } pairs
const extendedMetadataLength = packet.readLengthCodedNumber() || 0;
const extendedMetadataEnd = Math.min(
packet.offset + extendedMetadataLength,
packet.end
);
while (packet.offset < extendedMetadataEnd) {
const id = packet.readInt8();
const value = packet.readLengthCodedString('ascii');
if (id === 0) {
this.extendedTypeName = value; // e.g. 'uuid', 'inet4', 'inet6', 'point'
} else if (id === 1) {
this.extendedFormat = value; // e.g. 'json'
}
}
// a malformed entry could read past the block; resynchronise so the
// fixed column definition fields that follow parse from the right offset
packet.offset = extendedMetadataEnd;
}
inspect() {
return {
catalog: this.catalog,
schema: this.schema,
name: this.name,
orgName: this.orgName,
table: this.table,
orgTable: this.orgTable,
characterSet: this.characterSet,
encoding: this.encoding,
columnLength: this.columnLength,
type: this.columnType,
flags: this.flags,
decimals: this.decimals,
};
}
[Symbol.for('nodejs.util.inspect.custom')](depth, inspectOptions, inspect) {
const Types = require('../constants/types.js');
const typeNames = [];
for (const t in Types) {
typeNames[Types[t]] = t;
}
const fiedFlags = require('../constants/field_flags.js');
const flagNames = [];
// TODO: respect inspectOptions.showHidden
//const inspectFlags = inspectOptions.showHidden ? this.flags : this.flags & ~fiedFlags.PRI_KEY;
const inspectFlags = this.flags;
for (const f in fiedFlags) {
if (inspectFlags & fiedFlags[f]) {
if (f === 'PRI_KEY') {
flagNames.push('PRIMARY KEY');
} else if (f === 'NOT_NULL') {
flagNames.push('NOT NULL');
} else if (f === 'BINARY') {
// ignore flag for now
} else if (f === 'MULTIPLE_KEY') {
// not sure if that should be part of inspection.
// in the schema usually this is part of index definition
// example: UNIQUE KEY `my_uniq_id` (`id_box_elements`,`id_router`)
// note that only first column has MULTIPLE_KEY flag set in this case
// so there is no good way of knowing that this is part of index just
// by looking at indifidual field flags
} else if (f === 'NO_DEFAULT_VALUE') {
// almost the same as NOT_NULL?
} else if (f === 'BLOB') {
// included in the type
} else if (f === 'UNSIGNED') {
// this should be first after type
} else if (f === 'TIMESTAMP') {
// timestamp flag is redundant for inspection - already included in type
} else if (f === 'ON_UPDATE_NOW') {
flagNames.push('ON UPDATE CURRENT_TIMESTAMP');
} else {
flagNames.push(f);
}
}
}
if (depth > 1) {
return inspect({
...this.inspect(),
typeName: typeNames[this.columnType],
flags: flagNames,
});
}
const isUnsigned = this.flags & fiedFlags.UNSIGNED;
let typeName = typeNames[this.columnType];
if (typeName === 'BLOB') {
// TODO: check for non-utf8mb4 encoding
if (this.columnLength === 4294967295) {
typeName = 'LONGTEXT';
} else if (this.columnLength === 67108860) {
typeName = 'MEDIUMTEXT';
} else if (this.columnLength === 262140) {
typeName = 'TEXT';
} else if (this.columnLength === 1020) {
// 255*4
typeName = 'TINYTEXT';
} else {
typeName = `BLOB(${this.columnLength})`;
}
} else if (typeName === 'VAR_STRING') {
// TODO: check for non-utf8mb4 encoding
typeName = `VARCHAR(${Math.ceil(this.columnLength / 4)})`;
} else if (typeName === 'TINY') {
if (
(this.columnLength === 3 && isUnsigned) ||
(this.columnLength === 4 && !isUnsigned)
) {
typeName = 'TINYINT';
} else {
typeName = `TINYINT(${this.columnLength})`;
}
} else if (typeName === 'LONGLONG') {
if (this.columnLength === 20) {
typeName = 'BIGINT';
} else {
typeName = `BIGINT(${this.columnLength})`;
}
} else if (typeName === 'SHORT') {
if (isUnsigned && this.columnLength === 5) {
typeName = 'SMALLINT';
} else if (!isUnsigned && this.columnLength === 6) {
typeName = 'SMALLINT';
} else {
typeName = `SMALLINT(${this.columnLength})`;
}
} else if (typeName === 'LONG') {
if (isUnsigned && this.columnLength === 10) {
typeName = 'INT';
} else if (!isUnsigned && this.columnLength === 11) {
typeName = 'INT';
} else {
typeName = `INT(${this.columnLength})`;
}
} else if (typeName === 'INT24') {
if (isUnsigned && this.columnLength === 8) {
typeName = 'MEDIUMINT';
} else if (!isUnsigned && this.columnLength === 9) {
typeName = 'MEDIUMINT';
} else {
typeName = `MEDIUMINT(${this.columnLength})`;
}
} else if (typeName === 'DOUBLE') {
// DOUBLE without modifiers is reported as DOUBLE(22, 31)
if (this.columnLength === 22 && this.decimals === 31) {
typeName = 'DOUBLE';
} else {
typeName = `DOUBLE(${this.columnLength},${this.decimals})`;
}
} else if (typeName === 'FLOAT') {
// FLOAT without modifiers is reported as FLOAT(12, 31)
if (this.columnLength === 12 && this.decimals === 31) {
typeName = 'FLOAT';
} else {
typeName = `FLOAT(${this.columnLength},${this.decimals})`;
}
} else if (typeName === 'NEWDECIMAL') {
if (this.columnLength === 11 && this.decimals === 0) {
typeName = 'DECIMAL';
} else if (this.decimals === 0) {
// not sure why, but DECIMAL(13) is reported as DECIMAL(14, 0)
// and DECIMAL(13, 9) is reported as NEWDECIMAL(15, 9)
if (isUnsigned) {
typeName = `DECIMAL(${this.columnLength})`;
} else {
typeName = `DECIMAL(${this.columnLength - 1})`;
}
} else {
typeName = `DECIMAL(${this.columnLength - 2},${this.decimals})`;
}
} else {
typeName = `${typeNames[this.columnType]}(${this.columnLength})`;
}
if (isUnsigned) {
typeName += ' UNSIGNED';
}
// TODO respect colors option
return `\`${this.name}\` ${[typeName, ...flagNames].join(' ')}`;
}
static toPacket(column, sequenceId) {
let length = 17; // = 4 padding + 1 + 12 for the rest
fields.forEach((field) => {
length += Packet.lengthCodedStringLength(
column[field],
CharsetToEncoding[column.characterSet]
);
});
const buffer = Buffer.allocUnsafe(length);
const packet = new Packet(sequenceId, buffer, 0, length);
function writeField(name) {
packet.writeLengthCodedString(
column[name],
CharsetToEncoding[column.characterSet]
);
}
packet.offset = 4;
fields.forEach(writeField);
packet.writeInt8(0x0c);
packet.writeInt16(column.characterSet);
packet.writeInt32(column.columnLength);
packet.writeInt8(column.columnType);
packet.writeInt16(column.flags);
packet.writeInt8(column.decimals);
packet.writeInt16(0); // filler
return packet;
}
// node-mysql compatibility: alias "db" to "schema"
get db() {
return this.schema;
}
}
const addString = function (name) {
const cacheKey = `_${name}Value`;
const startKey = `_${name}Start`;
const lengthKey = `_${name}Length`;
ColumnDefinition.prototype[cacheKey] = undefined;
Object.defineProperty(ColumnDefinition.prototype, name, {
get: function () {
const cached = this[cacheKey];
if (cached !== undefined) {
return cached;
}
const start = this[startKey];
const end = start + this[lengthKey];
const val = StringParser.decode(
this._buf,
this.encoding === 'binary' ? this._clientEncoding : this.encoding,
start,
end
);
this[cacheKey] = val;
return val;
},
});
};
addString('catalog');
addString('schema');
addString('table');
addString('orgTable');
addString('orgName');
// MariaDB extended metadata defaults; instances only carry own values when
// the server actually tagged the column
ColumnDefinition.prototype.extendedTypeName = undefined;
ColumnDefinition.prototype.extendedFormat = undefined;
module.exports = ColumnDefinition;
+121
View File
@@ -0,0 +1,121 @@
'use strict';
const Types = require('../constants/types');
const Packet = require('../packets/packet');
const StringParser = require('../parsers/string.js');
const {
TypedParameter,
encodeTypedParameter,
integerHint,
} = require('./typed_parameter.js');
const FieldFlags = require('../constants/field_flags.js');
function isJSON(value) {
return (
Array.isArray(value) ||
value.constructor === Object ||
(typeof value.toJSON === 'function' && !Buffer.isBuffer(value))
);
}
function toParameter(value, encoding, timezone, jsonAsString, hint) {
if (value instanceof TypedParameter) {
return encodeTypedParameter(value, encoding, timezone, jsonAsString);
}
if (hint) {
const hinted = integerHint(
value,
hint.columnType,
Boolean(hint.flags & FieldFlags.UNSIGNED)
);
if (hinted) {
return encodeTypedParameter(hinted, encoding, timezone, jsonAsString);
}
}
let type = Types.VAR_STRING;
let length;
let writer = Packet.prototype.writeLengthCodedBuffer;
if (value !== null) {
switch (typeof value) {
case 'undefined':
throw new TypeError('Bind parameters must not contain undefined');
case 'number':
type = Types.DOUBLE;
length = 8;
writer = Packet.prototype.writeDouble;
break;
case 'boolean':
value = value | 0;
type = Types.TINY;
length = 1;
writer = Packet.prototype.writeInt8;
break;
case 'object':
if (Object.prototype.toString.call(value) === '[object Date]') {
type = Types.DATETIME;
length = 12;
writer = function (value) {
return Packet.prototype.writeDate.call(this, value, timezone);
};
} else if (isJSON(value)) {
value = JSON.stringify(value);
// MariaDB rejects the JSON parameter type with "Incorrect
// arguments to mysqld_stmt_execute"; it expects JSON values
// as plain strings
if (!jsonAsString) {
type = Types.JSON;
}
} else if (Buffer.isBuffer(value)) {
// send buffers as BLOB so servers treat the value as binary data
// rather than a string in the connection charset (MariaDB converts
// string parameters when storing into binary columns such as
// VECTOR, corrupting the value)
type = Types.BLOB;
length = Packet.lengthCodedNumberLength(value.length) + value.length;
writer = Packet.prototype.writeLengthCodedBuffer;
}
break;
default:
value = value.toString();
}
} else {
value = '';
type = Types.NULL;
length = 0;
writer = writeNothing;
}
let byteLength;
if (length === undefined) {
// a non-string here (e.g. a Uint8Array) keeps the Buffer.from coercion
// inside StringParser.encode
if (
typeof value === 'string' &&
StringParser.hasFastUtf8Write &&
(encoding === 'utf8' || encoding === 'utf-8')
) {
byteLength = Buffer.byteLength(value, 'utf8');
length = Packet.lengthCodedNumberLength(byteLength) + byteLength;
writer = Packet.prototype.writeLengthCodedUtf8String;
} else {
value = StringParser.encode(value, encoding);
length = Packet.lengthCodedNumberLength(value.length) + value.length;
}
}
return {
value,
type,
length,
byteLength,
writer,
unsigned: false,
isNull: type === Types.NULL,
};
}
function writeNothing() {}
module.exports = { toParameter, isJSON };
+223
View File
@@ -0,0 +1,223 @@
'use strict';
const CursorType = require('../constants/cursor');
const CommandCodes = require('../constants/commands');
const ClientConstants = require('../constants/client');
const Types = require('../constants/types');
const Packet = require('../packets/packet');
const StringParser = require('../parsers/string.js');
const CharsetToEncoding = require('../constants/charset_encodings.js');
const { toParameter } = require('./encode_parameter.js');
class Execute {
constructor(
id,
parameters,
charsetNumber,
timezone,
attributes,
clientFlags,
jsonAsString,
parameterDefinitions
) {
this.id = id;
this.parameters = parameters;
this.encoding = CharsetToEncoding[charsetNumber];
this.timezone = timezone;
this.attributes = attributes;
this.clientFlags = clientFlags || 0;
this.jsonAsString = jsonAsString || false;
this.parameterDefinitions = parameterDefinitions || [];
}
static fromPacket(packet, encoding) {
const stmtId = packet.readInt32();
const flags = packet.readInt8();
const iterationCount = packet.readInt32();
let i = packet.offset;
while (i < packet.end - 1) {
if (
(packet.buffer[i + 1] === Types.VAR_STRING ||
packet.buffer[i + 1] === Types.BLOB ||
packet.buffer[i + 1] === Types.NULL ||
packet.buffer[i + 1] === Types.DOUBLE ||
packet.buffer[i + 1] === Types.TINY ||
packet.buffer[i + 1] === Types.DATETIME ||
packet.buffer[i + 1] === Types.JSON) &&
packet.buffer[i] === 1 &&
packet.buffer[i + 2] === 0
) {
break;
} else {
packet.readInt8();
}
i++;
}
const types = [];
for (let i = packet.offset + 1; i < packet.end - 1; i++) {
if (
(packet.buffer[i] === Types.VAR_STRING ||
packet.buffer[i] === Types.BLOB ||
packet.buffer[i] === Types.NULL ||
packet.buffer[i] === Types.DOUBLE ||
packet.buffer[i] === Types.TINY ||
packet.buffer[i] === Types.DATETIME ||
packet.buffer[i] === Types.JSON) &&
packet.buffer[i + 1] === 0
) {
types.push(packet.buffer[i]);
packet.skip(2);
}
}
packet.skip(1);
const values = [];
for (let i = 0; i < types.length; i++) {
if (types[i] === Types.VAR_STRING) {
values.push(packet.readLengthCodedString(encoding));
} else if (types[i] === Types.BLOB) {
values.push(packet.readLengthCodedBuffer());
} else if (types[i] === Types.DOUBLE) {
values.push(packet.readDouble());
} else if (types[i] === Types.TINY) {
values.push(packet.readInt8());
} else if (types[i] === Types.DATETIME) {
values.push(packet.readDateTime());
} else if (types[i] === Types.JSON) {
values.push(JSON.parse(packet.readLengthCodedString(encoding)));
}
if (types[i] === Types.NULL) {
values.push(null);
}
}
return { stmtId, flags, iterationCount, values };
}
toPacket() {
const useQueryAttributes =
this.clientFlags & ClientConstants.CLIENT_QUERY_ATTRIBUTES;
const attrNames =
useQueryAttributes && this.attributes ? Object.keys(this.attributes) : [];
const numParams = this.parameters ? this.parameters.length : 0;
const numAttrs = attrNames.length;
const totalParams = numParams + numAttrs;
// packet header, command, statement id, cursor flags, iteration count
let length = 14;
if (useQueryAttributes) {
length += Packet.lengthCodedNumberLength(totalParams);
}
let allParams = null;
let attrNameBuffers = null;
if (totalParams > 0) {
allParams = new Array(totalParams);
for (let i = 0; i < numParams; i++) {
allParams[i] = toParameter(
this.parameters[i],
this.encoding,
this.timezone,
this.jsonAsString,
this.parameterDefinitions[i]
);
}
for (let i = 0; i < numAttrs; i++) {
allParams[numParams + i] = toParameter(
this.attributes[attrNames[i]],
this.encoding,
this.timezone
);
}
// null bitmap, new-params-bound flag, type and unsigned byte per parameter
length += ((totalParams + 7) >> 3) + 1 + totalParams * 2;
if (useQueryAttributes) {
// one empty length-coded name per bind parameter
length += numParams;
attrNameBuffers = new Array(numAttrs);
for (let i = 0; i < numAttrs; i++) {
const name = StringParser.encode(attrNames[i], this.encoding);
attrNameBuffers[i] = name;
length += Packet.lengthCodedNumberLength(name.length) + name.length;
}
}
for (let i = 0; i < totalParams; i++) {
if (!allParams[i].isNull) {
length += allParams[i].length;
}
}
}
const packet = new Packet(0, Buffer.allocUnsafe(length), 0, length);
packet.offset = 4;
packet.writeInt8(CommandCodes.STMT_EXECUTE);
packet.writeInt32(this.id);
let cursorFlags = CursorType.NO_CURSOR;
if (useQueryAttributes) {
cursorFlags |= CursorType.PARAMETER_COUNT_AVAILABLE;
}
packet.writeInt8(cursorFlags);
packet.writeInt32(1); // iteration-count, always 1
if (useQueryAttributes) {
packet.writeLengthCodedNumber(totalParams);
}
if (totalParams > 0) {
let bitmap = 0;
let bitValue = 1;
for (let i = 0; i < totalParams; i++) {
if (allParams[i].isNull) {
bitmap |= bitValue;
}
bitValue *= 2;
if (bitValue === 256) {
packet.writeInt8(bitmap);
bitmap = 0;
bitValue = 1;
}
}
if (bitValue !== 1) {
packet.writeInt8(bitmap);
}
packet.writeInt8(1); // new-params-bound-flag
for (let i = 0; i < totalParams; i++) {
const parameter = allParams[i];
packet.writeInt8(parameter.type);
packet.writeInt8(parameter.unsigned ? 0x80 : 0);
if (useQueryAttributes) {
if (i < numParams) {
packet.writeInt8(0); // bind parameters have an empty name
} else {
packet.writeLengthCodedBuffer(attrNameBuffers[i - numParams]);
}
}
}
for (let i = 0; i < totalParams; i++) {
const parameter = allParams[i];
if (!parameter.isNull) {
parameter.writer.call(packet, parameter.value, parameter.byteLength);
}
}
}
if (packet.offset !== length) {
throw new Error(
`Internal error: COM_STMT_EXECUTE serialized ${packet.offset - 4} bytes, expected ${length - 4}`
);
}
return packet;
}
}
module.exports = Execute;
+124
View File
@@ -0,0 +1,124 @@
'use strict';
const Packet = require('../packets/packet');
const ClientConstants = require('../constants/client.js');
// https://dev.mysql.com/doc/internals/en/connection-phase-packets.html#packet-Protocol::Handshake
class Handshake {
constructor(args) {
this.protocolVersion = args.protocolVersion;
this.serverVersion = args.serverVersion;
this.capabilityFlags = args.capabilityFlags;
this.connectionId = args.connectionId;
this.authPluginData1 = args.authPluginData1;
this.authPluginData2 = args.authPluginData2;
this.characterSet = args.characterSet;
this.statusFlags = args.statusFlags;
this.authPluginName = args.authPluginName;
this.mariadbExtendedCapabilityFlags = args.mariadbExtendedCapabilityFlags;
}
setScrambleData(cb) {
require('crypto').randomBytes(20, (err, data) => {
if (err) {
cb(err);
return;
}
this.authPluginData1 = data.slice(0, 8);
this.authPluginData2 = data.slice(8, 20);
cb();
});
}
toPacket(sequenceId) {
const length = 68 + Buffer.byteLength(this.serverVersion, 'utf8');
const buffer = Buffer.alloc(length + 4, 0); // zero fill, 10 bytes filler later needs to contain zeros
const packet = new Packet(sequenceId, buffer, 0, length + 4);
packet.offset = 4;
packet.writeInt8(this.protocolVersion);
packet.writeString(this.serverVersion, 'cesu8');
packet.writeInt8(0);
packet.writeInt32(this.connectionId);
packet.writeBuffer(this.authPluginData1);
packet.writeInt8(0);
const capabilityFlagsBuffer = Buffer.allocUnsafe(4);
capabilityFlagsBuffer.writeUInt32LE(this.capabilityFlags, 0);
packet.writeBuffer(capabilityFlagsBuffer.slice(0, 2));
packet.writeInt8(this.characterSet);
packet.writeInt16(this.statusFlags);
packet.writeBuffer(capabilityFlagsBuffer.slice(2, 4));
packet.writeInt8(21); // authPluginDataLength
packet.skip(10);
packet.writeBuffer(this.authPluginData2);
packet.writeInt8(0);
packet.writeString('mysql_native_password', 'latin1');
packet.writeInt8(0);
return packet;
}
static fromPacket(packet) {
const args = {};
args.protocolVersion = packet.readInt8();
args.serverVersion = packet.readNullTerminatedString('cesu8');
args.connectionId = packet.readInt32();
args.authPluginData1 = packet.readBuffer(8);
packet.skip(1);
const capabilityFlagsBuffer = Buffer.allocUnsafe(4);
capabilityFlagsBuffer[0] = packet.readInt8();
capabilityFlagsBuffer[1] = packet.readInt8();
if (packet.haveMoreData()) {
args.characterSet = packet.readInt8();
args.statusFlags = packet.readInt16();
// upper 2 bytes
capabilityFlagsBuffer[2] = packet.readInt8();
capabilityFlagsBuffer[3] = packet.readInt8();
args.capabilityFlags = capabilityFlagsBuffer.readUInt32LE(0);
if (args.capabilityFlags & ClientConstants.PLUGIN_AUTH) {
args.authPluginDataLength = packet.readInt8();
} else {
args.authPluginDataLength = 0;
packet.skip(1);
}
packet.skip(6);
if (args.capabilityFlags & ClientConstants.LONG_PASSWORD) {
// CLIENT_MYSQL (LONG_PASSWORD) set: a MySQL server, the remaining
// 4 reserved bytes are filler
packet.skip(4);
args.mariadbExtendedCapabilityFlags = 0;
} else {
// MariaDB servers (10.2+) leave CLIENT_MYSQL unset and use these
// 4 bytes to advertise MariaDB-specific capabilities
args.mariadbExtendedCapabilityFlags = packet.readInt32();
}
} else {
args.capabilityFlags = capabilityFlagsBuffer.readUInt16LE(0);
args.mariadbExtendedCapabilityFlags = 0;
}
const isSecureConnection =
args.capabilityFlags & ClientConstants.SECURE_CONNECTION;
if (isSecureConnection) {
const authPluginDataLength = args.authPluginDataLength;
if (authPluginDataLength === 0) {
// for Secure Password Authentication
args.authPluginDataLength = 20;
args.authPluginData2 = packet.readBuffer(12);
packet.skip(1);
} else {
// length > 0
// for Custom Auth Plugin (PLUGIN_AUTH)
const len = Math.max(13, authPluginDataLength - 8);
args.authPluginData2 = packet.readBuffer(len);
}
}
if (args.capabilityFlags & ClientConstants.PLUGIN_AUTH) {
args.authPluginName = packet.readNullTerminatedString('ascii');
}
return new Handshake(args);
}
}
module.exports = Handshake;
+177
View File
@@ -0,0 +1,177 @@
'use strict';
const ClientConstants = require('../constants/client.js');
const CharsetToEncoding = require('../constants/charset_encodings.js');
const Packet = require('../packets/packet.js');
const auth41 = require('../auth_41.js');
class HandshakeResponse {
constructor(handshake) {
this.user = handshake.user || '';
this.database = handshake.database || '';
this.password = handshake.password || '';
this.passwordSha1 = handshake.passwordSha1;
this.authPluginData1 = handshake.authPluginData1;
this.authPluginData2 = handshake.authPluginData2;
this.compress = handshake.compress;
this.clientFlags = handshake.flags;
this.mariadbExtendedClientFlags = handshake.mariadbExtendedClientFlags || 0;
// Accept pre-calculated authToken and authPluginName from caller
// This allows the caller to optimize by using the server's preferred auth method
if (
handshake.authToken !== undefined &&
handshake.authPluginName !== undefined
) {
// Validate types to fail fast with clear errors
if (!Buffer.isBuffer(handshake.authToken)) {
throw new TypeError(
'HandshakeResponse authToken must be a Buffer when provided'
);
}
if (typeof handshake.authPluginName !== 'string') {
throw new TypeError(
'HandshakeResponse authPluginName must be a string when provided'
);
}
this.authToken = handshake.authToken;
this.authPluginName = handshake.authPluginName;
} else {
// Fallback to legacy behavior: calculate mysql_native_password token
// TODO: pre-4.1 auth support
let authToken;
if (this.passwordSha1) {
authToken = auth41.calculateTokenFromPasswordSha(
this.passwordSha1,
this.authPluginData1,
this.authPluginData2
);
} else {
authToken = auth41.calculateToken(
this.password,
this.authPluginData1,
this.authPluginData2
);
}
this.authToken = authToken;
this.authPluginName = 'mysql_native_password';
}
this.charsetNumber = handshake.charsetNumber;
this.encoding = CharsetToEncoding[handshake.charsetNumber];
this.connectAttributes = handshake.connectAttributes;
}
serializeResponse(buffer) {
const isSet = (flag) => this.clientFlags & ClientConstants[flag];
const packet = new Packet(0, buffer, 0, buffer.length);
packet.offset = 4;
packet.writeInt32(this.clientFlags);
packet.writeInt32(0); // max packet size. todo: move to config
packet.writeInt8(this.charsetNumber);
// the last 4 of the 23 reserved bytes carry the MariaDB extended client
// capabilities (zero when not negotiated, i.e. plain filler)
packet.skip(19);
packet.writeInt32(this.mariadbExtendedClientFlags);
const encoding = this.encoding;
packet.writeNullTerminatedString(this.user, encoding);
let k;
if (isSet('PLUGIN_AUTH_LENENC_CLIENT_DATA')) {
packet.writeLengthCodedNumber(this.authToken.length);
packet.writeBuffer(this.authToken);
} else if (isSet('SECURE_CONNECTION')) {
packet.writeInt8(this.authToken.length);
packet.writeBuffer(this.authToken);
} else {
packet.writeBuffer(this.authToken);
packet.writeInt8(0);
}
if (isSet('CONNECT_WITH_DB')) {
packet.writeNullTerminatedString(this.database, encoding);
}
if (isSet('PLUGIN_AUTH')) {
// Use the auth plugin name specified by the caller (optimized for server's preference)
// or fall back to mysql_native_password for backward compatibility
packet.writeNullTerminatedString(
this.authPluginName || 'mysql_native_password',
'latin1'
);
}
if (isSet('CONNECT_ATTRS')) {
const connectAttributes = this.connectAttributes || {};
const attrNames = Object.keys(connectAttributes);
let keysLength = 0;
for (k = 0; k < attrNames.length; ++k) {
keysLength += Packet.lengthCodedStringLength(attrNames[k], encoding);
keysLength += Packet.lengthCodedStringLength(
connectAttributes[attrNames[k]],
encoding
);
}
packet.writeLengthCodedNumber(keysLength);
for (k = 0; k < attrNames.length; ++k) {
packet.writeLengthCodedString(attrNames[k], encoding);
packet.writeLengthCodedString(
connectAttributes[attrNames[k]],
encoding
);
}
}
return packet;
}
toPacket() {
if (typeof this.user !== 'string') {
throw new Error('"user" connection config property must be a string');
}
if (typeof this.database !== 'string') {
throw new Error('"database" connection config property must be a string');
}
// dry run: calculate resulting packet length
const p = this.serializeResponse(Packet.MockBuffer());
return this.serializeResponse(Buffer.alloc(p.offset));
}
static fromPacket(packet, serverFlags = 0xffffffff) {
const args = {};
args.clientFlags = packet.readInt32();
function isSet(flag) {
return args.clientFlags & serverFlags & ClientConstants[flag];
}
args.maxPacketSize = packet.readInt32();
args.charsetNumber = packet.readInt8();
const encoding = CharsetToEncoding[args.charsetNumber];
args.encoding = encoding;
packet.skip(23);
args.user = packet.readNullTerminatedString(encoding);
let authTokenLength;
if (isSet('PLUGIN_AUTH_LENENC_CLIENT_DATA')) {
authTokenLength = packet.readLengthCodedNumber(encoding);
args.authToken = packet.readBuffer(authTokenLength);
} else if (isSet('SECURE_CONNECTION')) {
authTokenLength = packet.readInt8();
args.authToken = packet.readBuffer(authTokenLength);
} else {
args.authToken = packet.readNullTerminatedString(encoding);
}
if (isSet('CONNECT_WITH_DB')) {
args.database = packet.readNullTerminatedString(encoding);
}
if (isSet('PLUGIN_AUTH')) {
args.authPluginName = packet.readNullTerminatedString(encoding);
}
if (isSet('CONNECT_ATTRS')) {
const keysLength = packet.readLengthCodedNumber(encoding);
const keysEnd = packet.offset + keysLength;
const attrs = {};
while (packet.offset < keysEnd) {
attrs[packet.readLengthCodedString(encoding)] =
packet.readLengthCodedString(encoding);
}
args.connectAttributes = attrs;
}
return args;
}
}
module.exports = HandshakeResponse;
+154
View File
@@ -0,0 +1,154 @@
// This file was modified by Oracle on June 1, 2021.
// A utility method was introduced to generate an Error instance from a
// binary server packet.
// Modifications copyright (c) 2021, Oracle and/or its affiliates.
// This file was modified by Oracle on September 21, 2021.
// The new AuthNextFactor packet is now available.
// Modifications copyright (c) 2021, Oracle and/or its affiliates.
'use strict';
const process = require('process');
const AuthNextFactor = require('./auth_next_factor');
const AuthSwitchRequest = require('./auth_switch_request');
const AuthSwitchRequestMoreData = require('./auth_switch_request_more_data');
const AuthSwitchResponse = require('./auth_switch_response');
const BinaryRow = require('./binary_row');
const BinlogDump = require('./binlog_dump');
const ChangeUser = require('./change_user');
const CloseStatement = require('./close_statement');
const ColumnDefinition = require('./column_definition');
const Execute = require('./execute');
const Handshake = require('./handshake');
const HandshakeResponse = require('./handshake_response');
const PrepareStatement = require('./prepare_statement');
const PreparedStatementHeader = require('./prepared_statement_header');
const Query = require('./query');
const RegisterSlave = require('./register_slave');
const ResetConnection = require('./reset_connection');
const ResultSetHeader = require('./resultset_header');
const SSLRequest = require('./ssl_request');
const TextRow = require('./text_row');
const ctorMap = {
AuthNextFactor,
AuthSwitchRequest,
AuthSwitchRequestMoreData,
AuthSwitchResponse,
BinaryRow,
BinlogDump,
ChangeUser,
CloseStatement,
ColumnDefinition,
Execute,
Handshake,
HandshakeResponse,
PrepareStatement,
PreparedStatementHeader,
Query,
RegisterSlave,
ResetConnection,
ResultSetHeader,
SSLRequest,
TextRow,
};
Object.entries(ctorMap).forEach(([name, ctor]) => {
module.exports[name] = ctor;
// monkey-patch it to include name if debug is on
if (process.env.NODE_DEBUG) {
if (ctor.prototype.toPacket) {
const old = ctor.prototype.toPacket;
ctor.prototype.toPacket = function () {
const p = old.call(this);
p._name = name;
return p;
};
}
}
});
// simple packets:
const Packet = require('./packet');
exports.Packet = Packet;
class OK {
static toPacket(args, encoding) {
args = args || {};
const affectedRows = args.affectedRows || 0;
const insertId = args.insertId || 0;
const serverStatus = args.serverStatus || 0;
const warningCount = args.warningCount || 0;
const message = args.message || '';
let length = 9 + Packet.lengthCodedNumberLength(affectedRows);
length += Packet.lengthCodedNumberLength(insertId);
const buffer = Buffer.allocUnsafe(length);
const packet = new Packet(0, buffer, 0, length);
packet.offset = 4;
packet.writeInt8(0);
packet.writeLengthCodedNumber(affectedRows);
packet.writeLengthCodedNumber(insertId);
packet.writeInt16(serverStatus);
packet.writeInt16(warningCount);
packet.writeString(message, encoding);
packet._name = 'OK';
return packet;
}
}
exports.OK = OK;
// warnings, statusFlags
class EOF {
static toPacket(warnings, statusFlags) {
if (typeof warnings === 'undefined') {
warnings = 0;
}
if (typeof statusFlags === 'undefined') {
statusFlags = 0;
}
const packet = new Packet(0, Buffer.allocUnsafe(9), 0, 9);
packet.offset = 4;
packet.writeInt8(0xfe);
packet.writeInt16(warnings);
packet.writeInt16(statusFlags);
packet._name = 'EOF';
return packet;
}
}
exports.EOF = EOF;
class Error {
static toPacket(args, encoding) {
const length = 13 + Buffer.byteLength(args.message, 'utf8');
const packet = new Packet(0, Buffer.allocUnsafe(length), 0, length);
packet.offset = 4;
packet.writeInt8(0xff);
packet.writeInt16(args.code);
// TODO: sql state parameter
packet.writeString('#_____', encoding);
packet.writeString(args.message, encoding);
packet._name = 'Error';
return packet;
}
static fromPacket(packet) {
packet.readInt8(); // marker
const code = packet.readInt16();
packet.readString(1, 'ascii'); // sql state marker
// The SQL state of the ERR_Packet which is always 5 bytes long.
// https://dev.mysql.com/doc/dev/mysql-server/8.0.11/page_protocol_basic_dt_strings.html#sect_protocol_basic_dt_string_fix
packet.readString(5, 'ascii'); // sql state (ignore for now)
const message = packet.readNullTerminatedString('utf8');
const error = new Error();
error.message = message;
error.code = code;
return error;
}
}
exports.Error = Error;
+1165
View File
File diff suppressed because it is too large Load Diff
+39
View File
@@ -0,0 +1,39 @@
'use strict';
const Packet = require('../packets/packet');
const CommandCodes = require('../constants/commands');
const StringParser = require('../parsers/string.js');
const CharsetToEncoding = require('../constants/charset_encodings.js');
class PrepareStatement {
constructor(sql, charsetNumber) {
this.query = sql;
this.charsetNumber = charsetNumber;
this.encoding = CharsetToEncoding[charsetNumber];
}
toPacket() {
if (
StringParser.hasFastUtf8Write &&
(this.encoding === 'utf8' || this.encoding === 'utf-8')
) {
const length = 5 + Buffer.byteLength(this.query, 'utf8');
const buffer = Buffer.allocUnsafe(length);
buffer[4] = CommandCodes.STMT_PREPARE;
buffer.utf8Write(this.query, 5, length - 5);
const packet = new Packet(0, buffer, 0, length);
packet.offset = length;
return packet;
}
const buf = StringParser.encode(this.query, this.encoding);
const length = 5 + buf.length;
const buffer = Buffer.allocUnsafe(length);
const packet = new Packet(0, buffer, 0, length);
packet.offset = 4;
packet.writeInt8(CommandCodes.STMT_PREPARE);
packet.writeBuffer(buf);
return packet;
}
}
module.exports = PrepareStatement;
+16
View File
@@ -0,0 +1,16 @@
'use strict';
class PreparedStatementHeader {
constructor(packet) {
packet.skip(1); // should be 0
this.id = packet.readInt32();
this.fieldCount = packet.readInt16();
this.parameterCount = packet.readInt16();
packet.skip(1); // should be 0
this.warningCount = packet.readInt16();
}
}
// TODO: toPacket
module.exports = PreparedStatementHeader;
+149
View File
@@ -0,0 +1,149 @@
'use strict';
const Packet = require('../packets/packet.js');
const CommandCode = require('../constants/commands.js');
const StringParser = require('../parsers/string.js');
const CharsetToEncoding = require('../constants/charset_encodings.js');
const ClientConstants = require('../constants/client.js');
const { toParameter } = require('./encode_parameter.js');
const { hasFastUtf8Write } = StringParser;
function toQueryPacket(buffer, headerLength, length) {
buffer[4] = CommandCode.QUERY;
if (headerLength === 7) {
buffer[5] = 0; // parameter_count
buffer[6] = 1; // parameter_set_count, always 1
}
const packet = new Packet(0, buffer, 0, length);
packet.offset = length;
return packet;
}
class Query {
constructor(sql, charsetNumber, attributes, clientFlags) {
this.query = sql;
this.charsetNumber = charsetNumber;
this.encoding = CharsetToEncoding[charsetNumber];
this.attributes = attributes;
this.clientFlags = clientFlags || 0;
}
toPacket() {
const useQueryAttributes =
this.clientFlags & ClientConstants.CLIENT_QUERY_ATTRIBUTES;
const attributeCount =
useQueryAttributes && this.attributes
? Object.keys(this.attributes).length
: 0;
if (attributeCount === 0) {
// fast path: no attribute values to serialize, so the packet is the
// header plus the encoded SQL
const headerLength = useQueryAttributes ? 7 : 5;
if (
hasFastUtf8Write &&
(this.encoding === 'utf8' || this.encoding === 'utf-8')
) {
const length = headerLength + Buffer.byteLength(this.query, 'utf8');
const buffer = Buffer.allocUnsafe(length);
buffer.utf8Write(this.query, headerLength, length - headerLength);
return toQueryPacket(buffer, headerLength, length);
}
if (Buffer.isEncoding(this.encoding)) {
const length =
headerLength + Buffer.byteLength(this.query, this.encoding);
const buffer = Buffer.allocUnsafe(length);
buffer.write(this.query, headerLength, this.encoding);
return toQueryPacket(buffer, headerLength, length);
}
const sqlBuffer = StringParser.encode(this.query, this.encoding);
const length = headerLength + sqlBuffer.length;
const buffer = Buffer.allocUnsafe(length);
sqlBuffer.copy(buffer, headerLength);
return toQueryPacket(buffer, headerLength, length);
}
const names = Object.keys(this.attributes);
const parameters = new Array(attributeCount);
const nameBuffers = new Array(attributeCount);
// packet header, command, parameter count, parameter_set_count (always
// the single-byte form), null bitmap, new_params_bind_flag, type and
// unsigned byte per parameter
let length =
5 +
Packet.lengthCodedNumberLength(attributeCount) +
1 +
((attributeCount + 7) >> 3) +
1 +
attributeCount * 2;
for (let i = 0; i < attributeCount; i++) {
parameters[i] = toParameter(
this.attributes[names[i]],
this.encoding,
'local'
);
const name = StringParser.encode(names[i], this.encoding);
nameBuffers[i] = name;
length += Packet.lengthCodedNumberLength(name.length) + name.length;
if (!parameters[i].isNull) {
length += parameters[i].length;
}
}
const sqlBuffer = StringParser.encode(this.query, this.encoding);
length += sqlBuffer.length;
const packet = new Packet(0, Buffer.allocUnsafe(length), 0, length);
packet.offset = 4;
packet.writeInt8(CommandCode.QUERY);
packet.writeLengthCodedNumber(attributeCount);
packet.writeLengthCodedNumber(1); // parameter_set_count, always 1
let bitmap = 0;
let bitValue = 1;
for (let i = 0; i < attributeCount; i++) {
if (parameters[i].isNull) {
bitmap |= bitValue;
}
bitValue *= 2;
if (bitValue === 256) {
packet.writeInt8(bitmap);
bitmap = 0;
bitValue = 1;
}
}
if (bitValue !== 1) {
packet.writeInt8(bitmap);
}
packet.writeInt8(1); // new_params_bind_flag
for (let i = 0; i < attributeCount; i++) {
packet.writeInt8(parameters[i].type);
packet.writeInt8(parameters[i].unsigned ? 0x80 : 0);
packet.writeLengthCodedBuffer(nameBuffers[i]);
}
for (let i = 0; i < attributeCount; i++) {
if (!parameters[i].isNull) {
parameters[i].writer.call(
packet,
parameters[i].value,
parameters[i].byteLength
);
}
}
packet.writeBuffer(sqlBuffer);
if (packet.offset !== length) {
throw new Error(
`Internal error: COM_QUERY serialized ${packet.offset - 4} bytes, expected ${length - 4}`
);
}
return packet;
}
}
module.exports = Query;
+46
View File
@@ -0,0 +1,46 @@
'use strict';
// http://dev.mysql.com/doc/internals/en/com-register-slave.html
// note that documentation is incorrect, for example command code is actually 0x15 but documented as 0x14
const Packet = require('../packets/packet');
const CommandCodes = require('../constants/commands');
class RegisterSlave {
constructor(opts) {
this.serverId = opts.serverId || 0;
this.slaveHostname = opts.slaveHostname || '';
this.slaveUser = opts.slaveUser || '';
this.slavePassword = opts.slavePassword || '';
this.slavePort = opts.slavePort || 0;
this.replicationRank = opts.replicationRank || 0;
this.masterId = opts.masterId || 0;
}
toPacket() {
const length =
15 + // TODO: should be ascii?
Buffer.byteLength(this.slaveHostname, 'utf8') +
Buffer.byteLength(this.slaveUser, 'utf8') +
Buffer.byteLength(this.slavePassword, 'utf8') +
3 +
4;
const buffer = Buffer.allocUnsafe(length);
const packet = new Packet(0, buffer, 0, length);
packet.offset = 4;
packet.writeInt8(CommandCodes.REGISTER_SLAVE);
packet.writeInt32(this.serverId);
packet.writeInt8(Buffer.byteLength(this.slaveHostname, 'utf8'));
packet.writeString(this.slaveHostname);
packet.writeInt8(Buffer.byteLength(this.slaveUser, 'utf8'));
packet.writeString(this.slaveUser);
packet.writeInt8(Buffer.byteLength(this.slavePassword, 'utf8'));
packet.writeString(this.slavePassword);
packet.writeInt16(this.slavePort);
packet.writeInt32(this.replicationRank);
packet.writeInt32(this.masterId);
return packet;
}
}
module.exports = RegisterSlave;
+17
View File
@@ -0,0 +1,17 @@
'use strict';
const Packet = require('../packets/packet');
const CommandCodes = require('../constants/commands');
class ResetConnection {
constructor() {}
toPacket() {
const packet = new Packet(0, Buffer.alloc(5), 0, 5);
packet.offset = 4;
packet.writeInt8(CommandCodes.RESET_CONNECTION);
return packet;
}
}
module.exports = ResetConnection;
+123
View File
@@ -0,0 +1,123 @@
'use strict';
// TODO: rename to OK packet
// https://dev.mysql.com/doc/internals/en/packet-OK_Packet.html
const Packet = require('./packet.js');
const ClientConstants = require('../constants/client.js');
const ServerSatusFlags = require('../constants/server_status.js');
const EncodingToCharset = require('../constants/encoding_charset.js');
const sessionInfoTypes = require('../constants/session_track.js');
class ResultSetHeader {
constructor(packet, connection) {
const bigNumberStrings = connection.config.bigNumberStrings;
const encoding = connection.serverEncoding;
const flags = connection._handshakePacket.capabilityFlags;
const isSet = function (flag) {
return flags & ClientConstants[flag];
};
if (packet.buffer[packet.offset] !== 0) {
this.fieldCount = packet.readLengthCodedNumber();
if (this.fieldCount === null) {
this.infileName = packet.readString(undefined, encoding);
}
return;
}
this.fieldCount = packet.readInt8(); // skip OK byte
this.affectedRows = packet.readLengthCodedNumber(bigNumberStrings);
this.insertId = packet.readLengthCodedNumberSigned(bigNumberStrings);
this.info = '';
if (isSet('PROTOCOL_41')) {
this.serverStatus = packet.readInt16();
this.warningStatus = packet.readInt16();
} else if (isSet('TRANSACTIONS')) {
this.serverStatus = packet.readInt16();
}
let stateChanges = null;
if (isSet('SESSION_TRACK') && packet.offset < packet.end) {
this.info = packet.readLengthCodedString(encoding);
if (this.serverStatus & ServerSatusFlags.SERVER_SESSION_STATE_CHANGED) {
// session change info record - see
// https://dev.mysql.com/doc/internals/en/packet-OK_Packet.html#cs-sect-packet-ok-sessioninfo
let len =
packet.offset < packet.end ? packet.readLengthCodedNumber() : 0;
const end = packet.offset + len;
let type, key, stateEnd;
if (len > 0) {
stateChanges = {
systemVariables: {},
schema: null,
gtids: [],
trackStateChange: null,
};
}
while (packet.offset < end) {
type = packet.readInt8();
len = packet.readLengthCodedNumber();
stateEnd = packet.offset + len;
if (type === sessionInfoTypes.SYSTEM_VARIABLES) {
key = packet.readLengthCodedString(encoding);
const val = packet.readLengthCodedString(encoding);
stateChanges.systemVariables[key] = val;
if (key === 'character_set_client') {
const charsetNumber = EncodingToCharset[val];
// TODO - better api for driver users to handle unknown encodings?
// maybe custom converter in the config?
// For now just ignore character_set_client command if there is
// no known mapping from reported encoding to a charset code
if (typeof charsetNumber !== 'undefined') {
connection.config.charsetNumber = charsetNumber;
}
}
} else if (type === sessionInfoTypes.SCHEMA) {
key = packet.readLengthCodedString(encoding);
stateChanges.schema = key;
} else if (type === sessionInfoTypes.STATE_CHANGE) {
stateChanges.trackStateChange =
packet.readLengthCodedString(encoding);
} else if (type === sessionInfoTypes.STATE_GTIDS) {
// TODO: find if the first length coded string means anything. Usually comes as empty
const _unknownString = packet.readLengthCodedString(encoding);
const gtid = packet.readLengthCodedString(encoding);
stateChanges.gtids = gtid.split(',');
} else {
// unsupported session track type. For now just ignore
}
packet.offset = stateEnd;
}
}
} else {
this.info = packet.readString(undefined, encoding);
}
if (stateChanges) {
this.stateChanges = stateChanges;
}
const m = this.info.match(/\schanged:\s*(\d+)/i);
if (m !== null) {
this.changedRows = parseInt(m[1], 10);
} else {
this.changedRows = 0;
}
}
// TODO: should be consistent instance member, but it's just easier here to have just function
static toPacket(fieldCount, insertId) {
let length = 4 + Packet.lengthCodedNumberLength(fieldCount);
if (typeof insertId !== 'undefined') {
length += Packet.lengthCodedNumberLength(insertId);
}
const buffer = Buffer.allocUnsafe(length);
const packet = new Packet(0, buffer, 0, length);
packet.offset = 4;
packet.writeLengthCodedNumber(fieldCount);
if (typeof insertId !== 'undefined') {
packet.writeLengthCodedNumber(insertId);
}
return packet;
}
}
module.exports = ResultSetHeader;
+30
View File
@@ -0,0 +1,30 @@
'use strict';
const ClientConstants = require('../constants/client');
const Packet = require('../packets/packet');
class SSLRequest {
constructor(flags, charset, mariadbExtendedClientFlags) {
this.clientFlags = flags | ClientConstants.SSL;
this.charset = charset;
this.mariadbExtendedClientFlags = mariadbExtendedClientFlags || 0;
}
toPacket() {
const length = 36;
const buffer = Buffer.allocUnsafe(length);
const packet = new Packet(0, buffer, 0, length);
buffer.fill(0);
packet.offset = 4;
packet.writeInt32(this.clientFlags);
packet.writeInt32(0); // max packet size. todo: move to config
packet.writeInt8(this.charset);
// the last 4 of the 23 reserved bytes carry the MariaDB extended client
// capabilities (zero when not negotiated, i.e. plain filler)
packet.skip(19);
packet.writeInt32(this.mariadbExtendedClientFlags);
return packet;
}
}
module.exports = SSLRequest;
+47
View File
@@ -0,0 +1,47 @@
'use strict';
const Packet = require('../packets/packet');
class TextRow {
constructor(columns) {
this.columns = columns || [];
}
static fromPacket(packet) {
// packet.reset(); // set offset to starting point?
const columns = [];
while (packet.haveMoreData()) {
columns.push(packet.readLengthCodedString());
}
return new TextRow(columns);
}
static toPacket(columns, encoding) {
const sequenceId = 0; // TODO remove, this is calculated now in connection
let length = 0;
columns.forEach((val) => {
if (val === null || typeof val === 'undefined') {
++length;
return;
}
length += Packet.lengthCodedStringLength(val.toString(10), encoding);
});
const buffer = Buffer.allocUnsafe(length + 4);
const packet = new Packet(sequenceId, buffer, 0, length + 4);
packet.offset = 4;
columns.forEach((val) => {
if (val === null) {
packet.writeNull();
return;
}
if (typeof val === 'undefined') {
packet.writeInt8(0);
return;
}
packet.writeLengthCodedString(val.toString(10), encoding);
});
return packet;
}
}
module.exports = TextRow;
+391
View File
@@ -0,0 +1,391 @@
'use strict';
const Types = require('../constants/types.js');
const Packet = require('./packet.js');
const StringParser = require('../parsers/string.js');
const INTEGER_BYTES = {
[Types.TINY]: 1,
[Types.SHORT]: 2,
[Types.YEAR]: 2,
[Types.INT24]: 4,
[Types.LONG]: 4,
[Types.LONGLONG]: 8,
};
const TEMPORAL = [Types.DATE, Types.DATETIME, Types.TIMESTAMP];
// MySQL rejects INT24, YEAR, ENUM, SET, BIT and GEOMETRY as bind types, and
// MariaDB rejects JSON and VECTOR. Each is transmitted as the narrowest type
// both servers accept, which carries the same value.
const WIRE_TYPE = {
[Types.INT24]: Types.LONG,
[Types.YEAR]: Types.SHORT,
[Types.ENUM]: Types.STRING,
[Types.SET]: Types.STRING,
[Types.VECTOR]: Types.BLOB,
};
// Only these may be adopted from a server hint: they are valid bind types on
// every server, so upgrading can never make a working statement fail.
const HINT_UPGRADABLE = new Set([
Types.TINY,
Types.SHORT,
Types.LONG,
Types.LONGLONG,
]);
const LENGTH_CODED = [
Types.DECIMAL,
Types.NEWDECIMAL,
Types.VARCHAR,
Types.VAR_STRING,
Types.STRING,
Types.ENUM,
Types.SET,
Types.JSON,
Types.VECTOR,
Types.TINY_BLOB,
Types.MEDIUM_BLOB,
Types.LONG_BLOB,
Types.BLOB,
];
function writeNothing() {}
const TIME_PATTERN = /^(-)?(\d+):([0-5]?\d):([0-5]?\d)(?:\.(\d{1,6}))?$/;
class TypedParameter {
constructor(type, value, unsigned) {
this.type = type;
this.value = value;
this.unsigned = unsigned;
}
[Symbol.for('nodejs.util.inspect.custom')]() {
const name = Types[this.type] || `0x${this.type.toString(16)}`;
return `${name}${this.unsigned ? ' UNSIGNED' : ''}(${String(this.value)})`;
}
}
function toInteger(value, name) {
switch (typeof value) {
case 'bigint':
return value;
case 'boolean':
return value ? 1n : 0n;
case 'number':
if (!Number.isInteger(value)) {
throw new TypeError(
`${name} parameter must be an integer, got ${value}`
);
}
if (!Number.isSafeInteger(value)) {
throw new RangeError(
`${name} parameter ${value} exceeds Number.MAX_SAFE_INTEGER and has already lost precision; pass a string or BigInt instead`
);
}
return BigInt(value);
case 'string':
try {
return BigInt(value.trim());
} catch (cause) {
throw new TypeError(
`${name} parameter must be an integer, got ${JSON.stringify(value)}`,
{ cause }
);
}
default:
throw new TypeError(
`${name} parameter must be an integer, got ${typeof value}`
);
}
}
function checkedInteger(value, type, bytes, unsigned) {
const name = Types[type];
const bits = BigInt(bytes * 8);
const n = toInteger(value, name);
const min = unsigned ? 0n : -(1n << (bits - 1n));
const max = unsigned ? (1n << bits) - 1n : (1n << (bits - 1n)) - 1n;
if (n < min || n > max) {
throw new RangeError(
`${name}${unsigned ? ' UNSIGNED' : ''} parameter out of range: ${n} is not within ${min}..${max}`
);
}
return n;
}
// The server reports an integer type whenever it knows a placeholder holds one,
// and MySQL rejects a DOUBLE in some of those positions. Adopting the hint is
// only safe when the value is already an integer that fits, so every other case
// returns null and keeps the type inferred from JavaScript.
function integerHint(value, type, unsigned) {
const bytes = INTEGER_BYTES[type];
if (!bytes || !HINT_UPGRADABLE.has(type)) {
return null;
}
let n;
if (typeof value === 'bigint') {
n = value;
} else if (typeof value === 'boolean') {
n = value ? 1n : 0n;
} else if (typeof value === 'number' && Number.isSafeInteger(value)) {
n = BigInt(value);
} else {
return null;
}
const bits = BigInt(bytes * 8);
const min = unsigned ? 0n : -(1n << (bits - 1n));
const max = unsigned ? (1n << bits) - 1n : (1n << (bits - 1n)) - 1n;
if (n < min || n > max) {
return null;
}
return new TypedParameter(type, n, unsigned);
}
function wireType(type, jsonAsString) {
if (type === Types.JSON && jsonAsString) {
return Types.VAR_STRING;
}
return WIRE_TYPE[type] || type;
}
function integerEncoder(type, bytes) {
return (value, unsigned) => {
const wire = BigInt.asUintN(
bytes * 8,
checkedInteger(value, type, bytes, unsigned)
);
return {
value: bytes === 8 ? wire : Number(wire),
length: bytes,
writer(v) {
this.writeUIntLE(v, bytes);
},
};
};
}
function toDate(value, name) {
const date = value instanceof Date ? value : new Date(value);
if (Number.isNaN(date.getTime())) {
throw new TypeError(
`${name} parameter must be a valid Date, got ${String(value)}`
);
}
return date;
}
function temporalEncoder(type, timezone) {
const name = Types[type];
return (value) => ({
value: toDate(value, name),
length: 12,
writer(v) {
this.writeDate(v, timezone);
},
});
}
function toTimeParts(value) {
if (typeof value === 'number') {
const negative = value < 0;
let rest = Math.abs(value);
const microseconds = Math.round((rest % 1000) * 1000);
rest = Math.floor(rest / 1000);
const seconds = rest % 60;
const minutes = Math.floor(rest / 60) % 60;
const totalHours = Math.floor(rest / 3600);
return {
negative,
days: Math.floor(totalHours / 24),
hours: totalHours % 24,
minutes,
seconds,
microseconds,
};
}
const match = TIME_PATTERN.exec(String(value));
if (!match) {
throw new TypeError(
`TIME parameter must be 'HH:MM:SS[.ffffff]' or milliseconds, got ${JSON.stringify(String(value))}`
);
}
const hours = Number(match[2]);
return {
negative: Boolean(match[1]),
days: Math.floor(hours / 24),
hours: hours % 24,
minutes: Number(match[3]),
seconds: Number(match[4]),
microseconds: match[5] ? Number(match[5].padEnd(6, '0')) : 0,
};
}
function timeEncoder() {
return (value) => {
const parts = toTimeParts(value);
return {
value: parts,
length: Packet.timeLength(parts),
writer(v) {
this.writeTime(v);
},
};
};
}
function lengthCodedEncoder(encoding) {
return (value) => {
if (!Buffer.isBuffer(value)) {
const string = typeof value === 'string' ? value : String(value);
if (
StringParser.hasFastUtf8Write &&
(encoding === 'utf8' || encoding === 'utf-8')
) {
const byteLength = Buffer.byteLength(string, 'utf8');
return {
value: string,
length: Packet.lengthCodedNumberLength(byteLength) + byteLength,
byteLength,
writer: Packet.prototype.writeLengthCodedUtf8String,
};
}
value = StringParser.encode(string, encoding);
}
return {
value,
length: Packet.lengthCodedNumberLength(value.length) + value.length,
writer: Packet.prototype.writeLengthCodedBuffer,
};
};
}
function jsonEncoder(encoding) {
const encodeText = lengthCodedEncoder(encoding);
return (value) =>
encodeText(
typeof value === 'string' || Buffer.isBuffer(value)
? value
: JSON.stringify(value)
);
}
function encoderFor(type, encoding, timezone) {
if (INTEGER_BYTES[type]) {
return integerEncoder(type, INTEGER_BYTES[type]);
}
if (type === Types.DOUBLE) {
return (value) => ({
value: Number(value),
length: 8,
writer: Packet.prototype.writeDouble,
});
}
if (type === Types.FLOAT) {
return (value) => ({
value: Number(value),
length: 4,
writer: Packet.prototype.writeFloat,
});
}
if (TEMPORAL.includes(type)) {
return temporalEncoder(type, timezone);
}
if (type === Types.TIME) {
return timeEncoder();
}
if (type === Types.JSON) {
return jsonEncoder(encoding);
}
if (LENGTH_CODED.includes(type)) {
return lengthCodedEncoder(encoding);
}
throw new TypeError(
`No parameter encoder for MySQL type 0x${type.toString(16)}`
);
}
function encodeTypedParameter(parameter, encoding, timezone, jsonAsString) {
if (parameter.value === null || parameter.type === Types.NULL) {
return {
value: '',
type: wireType(parameter.type, jsonAsString),
length: 0,
writer: writeNothing,
unsigned: parameter.unsigned,
isNull: true,
};
}
const encoded = encoderFor(
parameter.type,
encoding,
timezone
)(parameter.value, parameter.unsigned);
return {
...encoded,
type: wireType(parameter.type, jsonAsString),
unsigned: parameter.unsigned,
};
}
const ALIASES = {
MEDIUMTEXT: 'MEDIUM_BLOB',
LONGTEXT: 'LONG_BLOB',
TINYINT: 'TINY',
SMALLINT: 'SHORT',
MEDIUMINT: 'INT24',
INT: 'LONG',
INTEGER: 'LONG',
BIGINT: 'LONGLONG',
REAL: 'DOUBLE',
CHAR: 'STRING',
VARBINARY: 'VAR_STRING',
BINARY: 'STRING',
TEXT: 'BLOB',
};
const SUPPORTED = [
...Object.keys(INTEGER_BYTES),
Types.FLOAT,
Types.DOUBLE,
Types.TIME,
...TEMPORAL,
...LENGTH_CODED,
].map(Number);
const types = Object.create(null);
for (const type of SUPPORTED) {
const name = Types[type];
const bytes = INTEGER_BYTES[type];
const build = bytes
? (value, unsigned) =>
new TypedParameter(
type,
value === null ? null : checkedInteger(value, type, bytes, unsigned),
unsigned
)
: (value, unsigned) => new TypedParameter(type, value, unsigned);
const factory = (value) => build(value, false);
if (bytes) {
factory.unsigned = (value) => build(value, true);
}
types[name] = factory;
}
types.NULL = () => new TypedParameter(Types.NULL, null, false);
for (const [alias, target] of Object.entries(ALIASES)) {
if (types[target] && !types[alias]) {
types[alias] = types[target];
}
}
module.exports = {
TypedParameter,
encodeTypedParameter,
integerHint,
types,
};