jsql-neo 4.2.0 → 4.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -8,7 +8,7 @@ const net = require('net');
8
8
  const crypto = require('crypto');
9
9
  const path = require('path');
10
10
  const fs = require('fs');
11
- const { executeSQL, parseSQL, splitStatements } = require('./sql');
11
+ const { executeSQL, parseSQL, splitStatements, applyParams } = require('./sql');
12
12
  const Database = require('./database');
13
13
 
14
14
  const SERVER_VERSION = '8.0.0-jsql-neo';
@@ -31,6 +31,7 @@ const MYSQL_TYPE_TINY = 1, MYSQL_TYPE_LONG = 3, MYSQL_TYPE_LONGLONG = 8,
31
31
  MYSQL_TYPE_JSON = 245, MYSQL_TYPE_NULL = 6;
32
32
 
33
33
  function encodeLenenc(value) {
34
+ if (typeof value === 'bigint') value = Number(value);
34
35
  if (value === null) return Buffer.from([0xfb]);
35
36
  if (value < 0xfb) return Buffer.from([value]);
36
37
  if (value <= 0xffff) {
@@ -62,6 +63,13 @@ function parseLenenc(buf, offset) {
62
63
  return { value: buf.readUInt32LE(offset + 1), size: 5 };
63
64
  }
64
65
 
66
+ function readLenenc(buf, offset) {
67
+ const { value, size } = parseLenenc(buf, offset);
68
+ if (value === null) return { value: null, off: offset + size };
69
+ const str = buf.slice(offset + size, offset + size + value).toString('utf8');
70
+ return { value: str, off: offset + size + value };
71
+ }
72
+
65
73
  class PacketBuilder {
66
74
  constructor() {
67
75
  this.bufs = [];
@@ -128,7 +136,38 @@ function toMysqlErrno(e) {
128
136
  if (e && typeof e.code === 'number') return e.code;
129
137
  const m = e && e.message ? String(e.message).match(/^(ER_[A-Z_]+)/) : null;
130
138
  if (m) {
131
- const known = { ER_DUP_ENTRY: 1062, ER_NO_SUCH_TABLE: 1146, ER_TABLE_EXISTS: 1050, ER_PARSE_ERROR: 1064 };
139
+ const known = {
140
+ ER_DUP_ENTRY: 1062,
141
+ ER_NO_SUCH_TABLE: 1146,
142
+ ER_TABLE_EXISTS: 1050,
143
+ ER_TABLE_EXISTS_ERROR: 1050,
144
+ ER_PARSE_ERROR: 1064,
145
+ ER_BAD_FIELD_ERROR: 1054,
146
+ ER_BAD_NULL_ERROR: 1048,
147
+ ER_ACCESS_DENIED_ERROR: 1045,
148
+ ER_DBACCESS_DENIED_ERROR: 1044,
149
+ ER_BAD_DB_ERROR: 1049,
150
+ ER_WRONG_DB_NAME: 1102,
151
+ ER_WRONG_TABLE_NAME: 1103,
152
+ ER_WRONG_COLUMN_NAME: 1166,
153
+ ER_DATA_TOO_LONG: 1406,
154
+ ER_OUT_OF_RANGE: 1264,
155
+ ER_CHECK_CONSTRAINT: 3819,
156
+ ER_NO_DEFAULT_FOR_FIELD: 1364,
157
+ ER_CANT_DROP_FIELD_OR_KEY: 1091,
158
+ ER_CANT_DROP_DATABASE: 1008,
159
+ ER_EMPTY_QUERY: 1065,
160
+ ER_UNKNOWN_TABLE: 1109,
161
+ ER_NON_UNIQ_ERROR: 1052,
162
+ ER_WRONG_FIELD_WITH_GROUP: 1055,
163
+ ER_WRONG_VALUE_COUNT_ON_ROW: 1136,
164
+ ER_MISSING_TABLE: 1052,
165
+ ER_SP_DOES_NOT_EXIST: 1305,
166
+ ER_NOT_SUPPORTED_YET: 1235,
167
+ ER_LOCK_DEADLOCK: 1213,
168
+ ER_LOCK_WAIT_TIMEOUT: 1205,
169
+ ER_UNKNOWN_ERROR: 1105,
170
+ };
132
171
  if (known[m[1]]) return known[m[1]];
133
172
  }
134
173
  return 1105;
@@ -224,6 +263,102 @@ function resultSetPacket(result, tableSchema, baseSeq, rawRow) {
224
263
  return { packets, sequence };
225
264
  }
226
265
 
266
+ function binaryResultSetPacket(result, tableSchema, baseSeq) {
267
+ const packets = [];
268
+ let sequence = baseSeq || 0;
269
+ const push = (buf) => {
270
+ const header = Buffer.alloc(4);
271
+ header.writeUIntLE(buf.length, 0, 3);
272
+ header[3] = sequence;
273
+ sequence++;
274
+ packets.push(header);
275
+ packets.push(buf);
276
+ };
277
+
278
+ const inferType = (name, value) => {
279
+ if (tableSchema && tableSchema[name]) return columnTypeFromSchema(tableSchema[name]);
280
+ if (value === null || value === undefined) return MYSQL_TYPE_NULL;
281
+ if (typeof value === 'number') return Number.isInteger(value) ? MYSQL_TYPE_LONGLONG : MYSQL_TYPE_DOUBLE;
282
+ if (typeof value === 'boolean') return MYSQL_TYPE_TINY;
283
+ if (typeof value === 'object') return MYSQL_TYPE_JSON;
284
+ return MYSQL_TYPE_VAR_STRING;
285
+ };
286
+
287
+ const cols = result.columns || [];
288
+ push(encodeLenenc(cols.length));
289
+ const sampleRow = (result.rows && result.rows[0]) || null;
290
+ const types = [];
291
+ for (let ci = 0; ci < cols.length; ci++) {
292
+ let sample;
293
+ if (sampleRow !== null) {
294
+ if (Array.isArray(sampleRow)) sample = sampleRow[ci];
295
+ else sample = sampleRow[cols[ci]];
296
+ }
297
+ const t = inferType(cols[ci], sample);
298
+ types.push(t);
299
+ push(columnDefinition({ name: cols[ci], table: result.table || '', type: t, length: 1024 }));
300
+ }
301
+ push(eofPacket());
302
+
303
+ const encodeValue = (v, type) => {
304
+ if (v === null || v === undefined) return null;
305
+ if (type === MYSQL_TYPE_LONG || type === MYSQL_TYPE_LONGLONG || type === MYSQL_TYPE_DOUBLE) {
306
+ const num = Number(v);
307
+ if (type === MYSQL_TYPE_DOUBLE) {
308
+ if (!Number.isFinite(num)) return encodeLenencString(String(v));
309
+ const b = Buffer.alloc(8); b.writeDoubleLE(num, 0); return b;
310
+ }
311
+ if (Number.isInteger(num) && num <= 2147483647 && num >= -2147483648) {
312
+ const b = Buffer.alloc(4); b.writeInt32LE(num, 0); return b;
313
+ }
314
+ if (!Number.isFinite(num)) return encodeLenencString(String(v));
315
+ const b = Buffer.alloc(8); b.writeBigInt64LE(BigInt(Math.trunc(num)), 0); return b;
316
+ }
317
+ if (type === MYSQL_TYPE_TINY) {
318
+ const b = Buffer.alloc(1); b.writeInt8(v ? 1 : 0, 0); return b;
319
+ }
320
+ if (type === MYSQL_TYPE_DATE || type === MYSQL_TYPE_DATETIME) {
321
+ const s = String(v);
322
+ const m = s.match(/^(\d{4})-(\d{2})-(\d{2})(?:[ T](\d{2}):(\d{2}):(\d{2}))?/);
323
+ if (!m) return encodeLenencString(String(v));
324
+ const hasTime = !!m[4];
325
+ const buf = hasTime ? Buffer.alloc(8) : Buffer.alloc(5);
326
+ let off = 0;
327
+ buf[off++] = hasTime ? 7 : 4;
328
+ buf.writeUInt16LE(parseInt(m[1], 10), off); off += 2;
329
+ buf[off++] = parseInt(m[2], 10);
330
+ buf[off++] = parseInt(m[3], 10);
331
+ if (hasTime) {
332
+ buf[off++] = parseInt(m[4], 10);
333
+ buf[off++] = parseInt(m[5], 10);
334
+ buf[off++] = parseInt(m[6], 10);
335
+ }
336
+ return buf;
337
+ }
338
+ return encodeLenencString(typeof v === 'object' ? JSON.stringify(v) : v);
339
+ };
340
+
341
+ for (const row of result.rows || []) {
342
+ const parts = [Buffer.from([0x00])];
343
+ const nb = Buffer.alloc(Math.ceil(cols.length / 8));
344
+ const encoded = [];
345
+ for (let i = 0; i < cols.length; i++) {
346
+ const v = Array.isArray(row) ? row[i] : row[cols[i]];
347
+ if (v === null || v === undefined) {
348
+ nb[Math.floor(i / 8)] |= (1 << (i % 8));
349
+ encoded.push(null);
350
+ } else {
351
+ encoded.push(encodeValue(v, types[i]));
352
+ }
353
+ }
354
+ parts.push(nb);
355
+ for (const e of encoded) if (e !== null) parts.push(e);
356
+ push(Buffer.concat(parts));
357
+ }
358
+ push(eofPacket());
359
+ return { packets, sequence };
360
+ }
361
+
227
362
  class MysqlConnection {
228
363
  constructor(socket, server) {
229
364
  this.socket = socket;
@@ -236,6 +371,16 @@ class MysqlConnection {
236
371
  this.user = null;
237
372
  this.authFails = 0;
238
373
  this.multiStatements = false;
374
+ this._stmts = new Map();
375
+ this._stmtSeq = 0;
376
+ this.session = {
377
+ lastInsertId: 0,
378
+ rowCount: 0,
379
+ foundRows: 0,
380
+ connectionId: server._connectionCounter,
381
+ currentDb: null,
382
+ sysvars: {},
383
+ };
239
384
  if (server.handshakeTimeout > 0) {
240
385
  this._authTimer = setTimeout(() => {
241
386
  if (!this.authenticated) {
@@ -335,6 +480,11 @@ class MysqlConnection {
335
480
  case 0x11: this._handleChangeUser(body, cmdSeq); break; // COM_CHANGE_USER
336
481
  case 0x0e: this._send(okPacket()); break; // COM_PING
337
482
  case 0x1f: this._send(okPacket()); break; // COM_RESET_CONNECTION
483
+ case 0x16: this._handleStmtPrepare(body); break; // COM_STMT_PREPARE
484
+ case 0x17: this._handleStmtExecute(body); break; // COM_STMT_EXECUTE
485
+ case 0x18: break; // COM_STMT_SEND_LONG_DATA (忽略)
486
+ case 0x19: this._handleStmtClose(body); break; // COM_STMT_CLOSE
487
+ case 0x1a: this._send(okPacket()); break; // COM_STMT_RESET
338
488
  case 0x0a: { // COM_PROCESS_INFO
339
489
  const seq = this.sequence;
340
490
  this.sequence = (this.sequence + 1) & 0xff;
@@ -474,10 +624,173 @@ class MysqlConnection {
474
624
  }
475
625
  }
476
626
 
477
- _handleQuery(sql) {
627
+ countPlaceholders(sql) {
628
+ let n = 0;
629
+ let inStr = null;
630
+ for (let i = 0; i < sql.length; i++) {
631
+ const c = sql[i];
632
+ if (inStr) {
633
+ if (c === '\\' && i + 1 < sql.length) { i++; continue; }
634
+ if (c === inStr) inStr = null;
635
+ continue;
636
+ }
637
+ if (c === "'" || c === '"' || c === '`') { inStr = c; continue; }
638
+ if (c === '?' && sql[i + 1] === '?') { i++; continue; }
639
+ if (c === '?') n++;
640
+ }
641
+ return n;
642
+ }
643
+
644
+ _handleStmtPrepare(body) {
645
+ try {
646
+ const sql = body.toString('utf8');
647
+ const numParams = this.countPlaceholders(sql);
648
+ let numColumns = 0;
649
+ try {
650
+ const stmt = parseSQL(sql);
651
+ if (stmt && stmt.type === 'select' && stmt.columns) numColumns = stmt.columns.length;
652
+ } catch (e) { /* 无法解析的 SQL 仍可 prepare */ }
653
+ const stmtId = ++this._stmtSeq;
654
+ this._stmts.set(stmtId, { sql, numParams, numColumns });
655
+
656
+ const pkt = Buffer.alloc(12);
657
+ let off = 0;
658
+ pkt[off++] = 0x00;
659
+ pkt.writeUInt32LE(stmtId, off); off += 4;
660
+ pkt.writeUInt16LE(numColumns, off); off += 2;
661
+ pkt.writeUInt16LE(numParams, off); off += 2;
662
+ pkt[off++] = 0x00;
663
+ pkt.writeUInt16LE(0, off);
664
+ this._send(pkt);
665
+
666
+ if (numParams > 0) {
667
+ for (let i = 0; i < numParams; i++) {
668
+ this._send(columnDefinition({ name: '?', type: MYSQL_TYPE_VAR_STRING }));
669
+ }
670
+ this._send(eofPacket());
671
+ }
672
+ if (numColumns > 0) {
673
+ for (let i = 0; i < numColumns; i++) {
674
+ this._send(columnDefinition({ name: 'col' + (i + 1), type: MYSQL_TYPE_VAR_STRING }));
675
+ }
676
+ this._send(eofPacket());
677
+ }
678
+ } catch (e) {
679
+ this._send(errPacket(toMysqlErrno(e), e.message));
680
+ }
681
+ }
682
+
683
+ _handleStmtClose(body) {
684
+ if (body.length >= 4) {
685
+ const stmtId = body.readUInt32LE(0);
686
+ this._stmts.delete(stmtId);
687
+ }
688
+ }
689
+
690
+ _handleStmtExecute(body) {
691
+ try {
692
+ if (body.length < 9) throw new Error('malformed COM_STMT_EXECUTE');
693
+ const stmtId = body.readUInt32LE(0);
694
+ const stmt = this._stmts.get(stmtId);
695
+ if (!stmt) {
696
+ this._send(errPacket(1243, 'Unknown prepared statement handler (' + stmtId + ') given to mysqld_stmt_execute'));
697
+ return;
698
+ }
699
+ const flags = body[4];
700
+ const iteration = body.readUInt32LE(5);
701
+ let values = null;
702
+ if (stmt.numParams > 0) {
703
+ let off = 9;
704
+ const numParams = stmt.numParams;
705
+ const nullBitmapLen = Math.ceil(numParams / 8);
706
+ if (body.length < off + nullBitmapLen) throw new Error('malformed COM_STMT_EXECUTE params');
707
+ const nullBitmap = body.slice(off, off + nullBitmapLen);
708
+ off += nullBitmapLen;
709
+ if (off >= body.length) throw new Error('malformed COM_STMT_EXECUTE params');
710
+ const newParamsBound = body[off];
711
+ off += 1;
712
+ let types = stmt.types;
713
+ if (newParamsBound & 0x01) {
714
+ if (body.length < off + numParams * 2) throw new Error('malformed COM_STMT_EXECUTE types');
715
+ types = [];
716
+ for (let i = 0; i < numParams; i++) types.push(body.readUInt16LE(off + i * 2));
717
+ stmt.types = types;
718
+ off += numParams * 2;
719
+ }
720
+ if (!types) throw new Error('Parameter types unknown for prepared statement');
721
+ values = [];
722
+ for (let i = 0; i < numParams; i++) {
723
+ const isNull = nullBitmap[Math.floor(i / 8)] & (1 << (i % 8));
724
+ if (isNull) { values.push(null); continue; }
725
+ const type = types[i] & 0xff;
726
+ const parsed = this._readParamValue(body, off, type);
727
+ values.push(parsed.value);
728
+ off = parsed.off;
729
+ }
730
+ }
731
+ this._handleQuery(stmt.sql, values !== null && values.length > 0 ? values : undefined);
732
+ } catch (e) {
733
+ this._send(errPacket(toMysqlErrno(e), e.message));
734
+ }
735
+ }
736
+
737
+ _readParamValue(buf, off, type) {
738
+ switch (type) {
739
+ case MYSQL_TYPE_NULL: return { value: null, off };
740
+ case 0x01: { const v = buf.readInt8(off); return { value: v, off: off + 1 }; } // TINY
741
+ case 0x02: { const v = buf.readInt16LE(off); return { value: v, off: off + 2 }; } // SHORT
742
+ case 0x03: { const v = buf.readInt32LE(off); return { value: v, off: off + 4 }; } // LONG
743
+ case 0x08: { const v = buf.readBigInt64LE(off); return { value: Number(v), off: off + 8 }; } // LONGLONG
744
+ case 0x04: { const v = buf.readFloatLE(off); return { value: v, off: off + 4 }; } // FLOAT
745
+ case MYSQL_TYPE_DOUBLE: { const v = buf.readDoubleLE(off); return { value: v, off: off + 8 }; }
746
+ case 0x0a: case 0x07: case 0x0b: case MYSQL_TYPE_DATETIME: { // DATE/TIMESTAMP/DATETIME/TIME
747
+ const len = buf[off];
748
+ off += 1;
749
+ if (len === 0) return { value: null, off };
750
+ let value;
751
+ if (type === 0x0b) { // TIME
752
+ let sign = 1;
753
+ let p = off;
754
+ if (buf[p] !== 0) sign = -1;
755
+ p += 1;
756
+ const days = buf.readUInt32LE(p); p += 4;
757
+ const hour = buf[p++];
758
+ const min = buf[p++];
759
+ const sec = buf[p++];
760
+ value = sign * (days * 24 + hour) + ':' + String(min).padStart(2, '0') + ':' + String(sec).padStart(2, '0');
761
+ } else {
762
+ let p = off;
763
+ const year = buf.readUInt16LE(p); p += 2;
764
+ const month = buf[p++];
765
+ const day = buf[p++];
766
+ let hour = 0, minute = 0, second = 0;
767
+ if (len >= 7) { hour = buf[p++]; minute = buf[p++]; second = buf[p++]; }
768
+ value = String(year).padStart(4, '0') + '-' + String(month).padStart(2, '0') + '-' + String(day).padStart(2, '0');
769
+ if (len >= 7) value += ' ' + String(hour).padStart(2, '0') + ':' + String(minute).padStart(2, '0') + ':' + String(second).padStart(2, '0');
770
+ }
771
+ off += len;
772
+ return { value, off };
773
+ }
774
+ case 0xfc: case 0xfd: case 0xfe: case 0xf9: case 0xf6: { // BLOB / VAR_STRING / STRING / ...
775
+ const { value, off: noff } = readLenenc(buf, off);
776
+ return { value, off: noff };
777
+ }
778
+ case 0xf0: { // NULL
779
+ return { value: null, off };
780
+ }
781
+ default: {
782
+ const { value, off: noff } = readLenenc(buf, off);
783
+ return { value, off: noff };
784
+ }
785
+ }
786
+ }
787
+
788
+ _handleQuery(sql, values) {
478
789
  (async () => {
479
790
  try {
480
- const statements = splitStatements(sql);
791
+ const hasParams = values !== undefined && values !== null && values.length > 0;
792
+ const processedSql = hasParams ? applyParams(sql, values) : sql;
793
+ const statements = splitStatements(processedSql);
481
794
  if (statements.length > 1 && !this.multiStatements) {
482
795
  throw new Error(`too many statements (${statements.length} > 1)`);
483
796
  }
@@ -533,18 +846,30 @@ class MysqlConnection {
533
846
  allowComments: this.server.allowComments,
534
847
  safety: this.server.safety,
535
848
  maxStatements: 1,
849
+ session: this.session,
536
850
  });
537
- if (r.type === 'select' || r.type === 'showTables' || r.type === 'showDatabases' || r.type === 'describe') {
851
+ if (r.type === 'select' || r.type === 'showTables' || r.type === 'showDatabases' || r.type === 'describe'
852
+ || r.type === 'showColumns' || r.type === 'showIndex' || r.type === 'showCreateTable'
853
+ || r.type === 'showVariables' || r.type === 'showStatus' || r.type === 'showGrants' || r.type === 'showWarnings') {
854
+ this.session.foundRows = (r.raw && r.raw.length !== undefined) ? r.raw.length : (r.rows ? r.rows.length : 0);
538
855
  let schema = null;
539
856
  if (r.table) {
540
857
  schema = engine.getTableSchema
541
858
  ? await engine.getTableSchema(r.table)
542
859
  : (engine._schemas ? engine._schemas[r.table] : null);
543
860
  }
544
- const { packets, sequence } = resultSetPacket(r, schema, this.sequence);
861
+ const { packets, sequence } = hasParams
862
+ ? binaryResultSetPacket(r, schema, this.sequence)
863
+ : resultSetPacket(r, schema, this.sequence);
545
864
  this.sequence = sequence;
546
865
  this.socket.write(Buffer.concat(packets));
547
866
  } else {
867
+ if (r.type === 'insert') {
868
+ this.session.lastInsertId = r.insertId;
869
+ this.session.rowCount = r.affectedRows;
870
+ } else if (r.type === 'update' || r.type === 'delete' || r.type === 'truncate') {
871
+ this.session.rowCount = r.affectedRows;
872
+ }
548
873
  this._send(okPacket(r.affectedRows || 0, r.insertId || 0));
549
874
  }
550
875
  }
@@ -630,6 +955,10 @@ class MysqlServer {
630
955
  }
631
956
  };
632
957
  collect(stmt);
958
+ const isInfoSchema = tables.some(t => t && String(t).toLowerCase().startsWith('information_schema.'));
959
+ if (isInfoSchema) {
960
+ return { engine: await this._getEngine(currentDb), sql };
961
+ }
633
962
  let db = null;
634
963
  for (const t of tables) {
635
964
  if (t && t.indexOf('.') !== -1) {