创建并连接数据库,并修改登录和注册接口
This commit is contained in:
+35
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
File diff suppressed because it is too large
Load Diff
+39
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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,
|
||||
};
|
||||
Reference in New Issue
Block a user