jsql-neo 4.2.0-beta.1 → 4.3.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.
package/README.md CHANGED
@@ -1,10 +1,7 @@
1
- # JSQL-NEO v4.1.1
1
+ # JSQL-NEO v4.0.1
2
2
 
3
3
  Rust-powered embedded database with **three engines + SQL + Redis-style storage** in one npm package.
4
4
 
5
- > **Docs & Help:** [https://help.vexify.top](https://help.vexify.top)
6
- > **GitHub:** [https://github.com/vexify-org/JSQL-neo](https://github.com/vexify-org/JSQL-neo)
7
-
8
5
  ## Engines
9
6
 
10
7
  | Engine | Entry | Use Case |
@@ -122,6 +119,39 @@ db.save();
122
119
  }
123
120
  ```
124
121
 
122
+ ## ORM Compatibility
123
+
124
+ The MySQL-compatible server mode accepts standard MySQL clients, so popular Node.js ORMs work without a plugin. Tested against a live `createMysqlServer()` instance:
125
+
126
+ | ORM | Version | Results |
127
+ |-----|---------|---------|
128
+ | [Sequelize](https://sequelize.org) | v6 | ✅ 10/10 — connect, authenticate, sync (CREATE TABLE), create, bulkCreate, find, count, update, destroy, MAX() |
129
+ | [Knex](https://knexjs.org) | v3 | ✅ 9/9 — schema builder, insert, select, where + orderBy, count, update, delete, raw `SELECT VERSION()` |
130
+ | [TypeORM](https://typeorm.io) | v0.3 | ✅ 8/8 — initialize, synchronize, save, findOne, find, count, update, delete |
131
+
132
+ ```js
133
+ // Sequelize
134
+ const { Sequelize, DataTypes } = require('sequelize');
135
+ const sequelize = new Sequelize('default', 'root', '', {
136
+ host: '127.0.0.1', port: 33306, dialect: 'mysql',
137
+ });
138
+
139
+ // Knex
140
+ const knex = require('knex')({
141
+ client: 'mysql2',
142
+ connection: { host: '127.0.0.1', port: 33306, user: 'root', database: 'default' },
143
+ });
144
+
145
+ // TypeORM
146
+ const ds = new DataSource({
147
+ type: 'mysql',
148
+ host: '127.0.0.1', port: 33306, username: 'root', database: 'default',
149
+ synchronize: true, entities: [User],
150
+ });
151
+ ```
152
+
153
+ Supported for ORM compatibility: prepared statements (`COM_STMT_PREPARE`/`EXECUTE`), binary protocol result sets, `SHOW COLUMNS` / `SHOW INDEX` / `SHOW CREATE TABLE` / `SHOW VARIABLES` / `SHOW GRANTS`, `information_schema` queries, `START TRANSACTION`, `TRUNCATE TABLE`, `SET` statements, scalar functions (`VERSION()`, `NOW()`, `CONCAT()`, `IFNULL()`, ...), and MySQL DDL forms (`int unsigned`, `auto_increment`, `ENGINE=InnoDB`, `DEFAULT CHARSET`).
154
+
125
155
  ## Features
126
156
 
127
157
  - Three engines: Native (N-API Rust), WASM (wasm-pack Rust), Pure JS (local JSON)
@@ -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';
@@ -62,6 +62,13 @@ function parseLenenc(buf, offset) {
62
62
  return { value: buf.readUInt32LE(offset + 1), size: 5 };
63
63
  }
64
64
 
65
+ function readLenenc(buf, offset) {
66
+ const { value, size } = parseLenenc(buf, offset);
67
+ if (value === null) return { value: null, off: offset + size };
68
+ const str = buf.slice(offset + size, offset + size + value).toString('utf8');
69
+ return { value: str, off: offset + size + value };
70
+ }
71
+
65
72
  class PacketBuilder {
66
73
  constructor() {
67
74
  this.bufs = [];
@@ -224,6 +231,100 @@ function resultSetPacket(result, tableSchema, baseSeq, rawRow) {
224
231
  return { packets, sequence };
225
232
  }
226
233
 
234
+ function binaryResultSetPacket(result, tableSchema, baseSeq) {
235
+ const packets = [];
236
+ let sequence = baseSeq || 0;
237
+ const push = (buf) => {
238
+ const header = Buffer.alloc(4);
239
+ header.writeUIntLE(buf.length, 0, 3);
240
+ header[3] = sequence;
241
+ sequence++;
242
+ packets.push(header);
243
+ packets.push(buf);
244
+ };
245
+
246
+ const inferType = (name, value) => {
247
+ if (tableSchema && tableSchema[name]) return columnTypeFromSchema(tableSchema[name]);
248
+ if (value === null || value === undefined) return MYSQL_TYPE_NULL;
249
+ if (typeof value === 'number') return Number.isInteger(value) ? MYSQL_TYPE_LONGLONG : MYSQL_TYPE_DOUBLE;
250
+ if (typeof value === 'boolean') return MYSQL_TYPE_TINY;
251
+ if (typeof value === 'object') return MYSQL_TYPE_JSON;
252
+ return MYSQL_TYPE_VAR_STRING;
253
+ };
254
+
255
+ const cols = result.columns || [];
256
+ push(encodeLenenc(cols.length));
257
+ const sampleRow = (result.rows && result.rows[0]) || null;
258
+ const types = [];
259
+ for (let ci = 0; ci < cols.length; ci++) {
260
+ let sample;
261
+ if (sampleRow !== null) {
262
+ if (Array.isArray(sampleRow)) sample = sampleRow[ci];
263
+ else sample = sampleRow[cols[ci]];
264
+ }
265
+ const t = inferType(cols[ci], sample);
266
+ types.push(t);
267
+ push(columnDefinition({ name: cols[ci], table: result.table || '', type: t, length: 1024 }));
268
+ }
269
+ push(eofPacket());
270
+
271
+ const encodeValue = (v, type) => {
272
+ if (v === null || v === undefined) return null;
273
+ if (type === MYSQL_TYPE_LONG || type === MYSQL_TYPE_LONGLONG || type === MYSQL_TYPE_DOUBLE) {
274
+ const num = Number(v);
275
+ if (type === MYSQL_TYPE_DOUBLE) {
276
+ const b = Buffer.alloc(8); b.writeDoubleLE(num, 0); return b;
277
+ }
278
+ if (Number.isInteger(num) && num <= 2147483647 && num >= -2147483648) {
279
+ const b = Buffer.alloc(4); b.writeInt32LE(num, 0); return b;
280
+ }
281
+ const b = Buffer.alloc(8); b.writeBigInt64LE(BigInt(Math.trunc(num)), 0); return b;
282
+ }
283
+ if (type === MYSQL_TYPE_TINY) {
284
+ const b = Buffer.alloc(1); b.writeInt8(v ? 1 : 0, 0); return b;
285
+ }
286
+ if (type === MYSQL_TYPE_DATE || type === MYSQL_TYPE_DATETIME) {
287
+ const s = String(v);
288
+ const m = s.match(/^(\d{4})-(\d{2})-(\d{2})(?:[ T](\d{2}):(\d{2}):(\d{2}))?/);
289
+ if (!m) return encodeLenencString(String(v));
290
+ const hasTime = !!m[4];
291
+ const buf = hasTime ? Buffer.alloc(8) : Buffer.alloc(5);
292
+ let off = 0;
293
+ buf[off++] = hasTime ? 7 : 4;
294
+ buf.writeUInt16LE(parseInt(m[1], 10), off); off += 2;
295
+ buf[off++] = parseInt(m[2], 10);
296
+ buf[off++] = parseInt(m[3], 10);
297
+ if (hasTime) {
298
+ buf[off++] = parseInt(m[4], 10);
299
+ buf[off++] = parseInt(m[5], 10);
300
+ buf[off++] = parseInt(m[6], 10);
301
+ }
302
+ return buf;
303
+ }
304
+ return encodeLenencString(typeof v === 'object' ? JSON.stringify(v) : v);
305
+ };
306
+
307
+ for (const row of result.rows || []) {
308
+ const parts = [Buffer.from([0x00])];
309
+ const nb = Buffer.alloc(Math.ceil(cols.length / 8));
310
+ const encoded = [];
311
+ for (let i = 0; i < cols.length; i++) {
312
+ const v = Array.isArray(row) ? row[i] : row[cols[i]];
313
+ if (v === null || v === undefined) {
314
+ nb[Math.floor(i / 8)] |= (1 << (i % 8));
315
+ encoded.push(null);
316
+ } else {
317
+ encoded.push(encodeValue(v, types[i]));
318
+ }
319
+ }
320
+ parts.push(nb);
321
+ for (const e of encoded) if (e !== null) parts.push(e);
322
+ push(Buffer.concat(parts));
323
+ }
324
+ push(eofPacket());
325
+ return { packets, sequence };
326
+ }
327
+
227
328
  class MysqlConnection {
228
329
  constructor(socket, server) {
229
330
  this.socket = socket;
@@ -236,6 +337,8 @@ class MysqlConnection {
236
337
  this.user = null;
237
338
  this.authFails = 0;
238
339
  this.multiStatements = false;
340
+ this._stmts = new Map();
341
+ this._stmtSeq = 0;
239
342
  if (server.handshakeTimeout > 0) {
240
343
  this._authTimer = setTimeout(() => {
241
344
  if (!this.authenticated) {
@@ -335,6 +438,11 @@ class MysqlConnection {
335
438
  case 0x11: this._handleChangeUser(body, cmdSeq); break; // COM_CHANGE_USER
336
439
  case 0x0e: this._send(okPacket()); break; // COM_PING
337
440
  case 0x1f: this._send(okPacket()); break; // COM_RESET_CONNECTION
441
+ case 0x16: this._handleStmtPrepare(body); break; // COM_STMT_PREPARE
442
+ case 0x17: this._handleStmtExecute(body); break; // COM_STMT_EXECUTE
443
+ case 0x18: break; // COM_STMT_SEND_LONG_DATA (忽略)
444
+ case 0x19: this._handleStmtClose(body); break; // COM_STMT_CLOSE
445
+ case 0x1a: this._send(okPacket()); break; // COM_STMT_RESET
338
446
  case 0x0a: { // COM_PROCESS_INFO
339
447
  const seq = this.sequence;
340
448
  this.sequence = (this.sequence + 1) & 0xff;
@@ -474,10 +582,173 @@ class MysqlConnection {
474
582
  }
475
583
  }
476
584
 
477
- _handleQuery(sql) {
585
+ countPlaceholders(sql) {
586
+ let n = 0;
587
+ let inStr = null;
588
+ for (let i = 0; i < sql.length; i++) {
589
+ const c = sql[i];
590
+ if (inStr) {
591
+ if (c === '\\' && i + 1 < sql.length) { i++; continue; }
592
+ if (c === inStr) inStr = null;
593
+ continue;
594
+ }
595
+ if (c === "'" || c === '"' || c === '`') { inStr = c; continue; }
596
+ if (c === '?' && sql[i + 1] === '?') { i++; continue; }
597
+ if (c === '?') n++;
598
+ }
599
+ return n;
600
+ }
601
+
602
+ _handleStmtPrepare(body) {
603
+ try {
604
+ const sql = body.toString('utf8');
605
+ const numParams = this.countPlaceholders(sql);
606
+ let numColumns = 0;
607
+ try {
608
+ const stmt = parseSQL(sql);
609
+ if (stmt && stmt.type === 'select' && stmt.columns) numColumns = stmt.columns.length;
610
+ } catch (e) { /* 无法解析的 SQL 仍可 prepare */ }
611
+ const stmtId = ++this._stmtSeq;
612
+ this._stmts.set(stmtId, { sql, numParams, numColumns });
613
+
614
+ const pkt = Buffer.alloc(12);
615
+ let off = 0;
616
+ pkt[off++] = 0x00;
617
+ pkt.writeUInt32LE(stmtId, off); off += 4;
618
+ pkt.writeUInt16LE(numColumns, off); off += 2;
619
+ pkt.writeUInt16LE(numParams, off); off += 2;
620
+ pkt[off++] = 0x00;
621
+ pkt.writeUInt16LE(0, off);
622
+ this._send(pkt);
623
+
624
+ if (numParams > 0) {
625
+ for (let i = 0; i < numParams; i++) {
626
+ this._send(columnDefinition({ name: '?', type: MYSQL_TYPE_VAR_STRING }));
627
+ }
628
+ this._send(eofPacket());
629
+ }
630
+ if (numColumns > 0) {
631
+ for (let i = 0; i < numColumns; i++) {
632
+ this._send(columnDefinition({ name: 'col' + (i + 1), type: MYSQL_TYPE_VAR_STRING }));
633
+ }
634
+ this._send(eofPacket());
635
+ }
636
+ } catch (e) {
637
+ this._send(errPacket(toMysqlErrno(e), e.message));
638
+ }
639
+ }
640
+
641
+ _handleStmtClose(body) {
642
+ if (body.length >= 4) {
643
+ const stmtId = body.readUInt32LE(0);
644
+ this._stmts.delete(stmtId);
645
+ }
646
+ }
647
+
648
+ _handleStmtExecute(body) {
649
+ try {
650
+ if (body.length < 9) throw new Error('malformed COM_STMT_EXECUTE');
651
+ const stmtId = body.readUInt32LE(0);
652
+ const stmt = this._stmts.get(stmtId);
653
+ if (!stmt) {
654
+ this._send(errPacket(1243, 'Unknown prepared statement handler (' + stmtId + ') given to mysqld_stmt_execute'));
655
+ return;
656
+ }
657
+ const flags = body[4];
658
+ const iteration = body.readUInt32LE(5);
659
+ let values = null;
660
+ if (stmt.numParams > 0) {
661
+ let off = 9;
662
+ const numParams = stmt.numParams;
663
+ const nullBitmapLen = Math.ceil(numParams / 8);
664
+ if (body.length < off + nullBitmapLen) throw new Error('malformed COM_STMT_EXECUTE params');
665
+ const nullBitmap = body.slice(off, off + nullBitmapLen);
666
+ off += nullBitmapLen;
667
+ if (off >= body.length) throw new Error('malformed COM_STMT_EXECUTE params');
668
+ const newParamsBound = body[off];
669
+ off += 1;
670
+ let types = stmt.types;
671
+ if (newParamsBound & 0x01) {
672
+ if (body.length < off + numParams * 2) throw new Error('malformed COM_STMT_EXECUTE types');
673
+ types = [];
674
+ for (let i = 0; i < numParams; i++) types.push(body.readUInt16LE(off + i * 2));
675
+ stmt.types = types;
676
+ off += numParams * 2;
677
+ }
678
+ if (!types) throw new Error('Parameter types unknown for prepared statement');
679
+ values = [];
680
+ for (let i = 0; i < numParams; i++) {
681
+ const isNull = nullBitmap[Math.floor(i / 8)] & (1 << (i % 8));
682
+ if (isNull) { values.push(null); continue; }
683
+ const type = types[i] & 0xff;
684
+ const parsed = this._readParamValue(body, off, type);
685
+ values.push(parsed.value);
686
+ off = parsed.off;
687
+ }
688
+ }
689
+ this._handleQuery(stmt.sql, values !== null && values.length > 0 ? values : undefined);
690
+ } catch (e) {
691
+ this._send(errPacket(toMysqlErrno(e), e.message));
692
+ }
693
+ }
694
+
695
+ _readParamValue(buf, off, type) {
696
+ switch (type) {
697
+ case MYSQL_TYPE_NULL: return { value: null, off };
698
+ case 0x01: { const v = buf.readInt8(off); return { value: v, off: off + 1 }; } // TINY
699
+ case 0x02: { const v = buf.readInt16LE(off); return { value: v, off: off + 2 }; } // SHORT
700
+ case 0x03: { const v = buf.readInt32LE(off); return { value: v, off: off + 4 }; } // LONG
701
+ case 0x08: { const v = buf.readBigInt64LE(off); return { value: Number(v), off: off + 8 }; } // LONGLONG
702
+ case 0x04: { const v = buf.readFloatLE(off); return { value: v, off: off + 4 }; } // FLOAT
703
+ case MYSQL_TYPE_DOUBLE: { const v = buf.readDoubleLE(off); return { value: v, off: off + 8 }; }
704
+ case 0x0a: case 0x07: case 0x0b: case MYSQL_TYPE_DATETIME: { // DATE/TIMESTAMP/DATETIME/TIME
705
+ const len = buf[off];
706
+ off += 1;
707
+ if (len === 0) return { value: null, off };
708
+ let value;
709
+ if (type === 0x0b) { // TIME
710
+ let sign = 1;
711
+ let p = off;
712
+ if (buf[p] !== 0) sign = -1;
713
+ p += 1;
714
+ const days = buf.readUInt32LE(p); p += 4;
715
+ const hour = buf[p++];
716
+ const min = buf[p++];
717
+ const sec = buf[p++];
718
+ value = sign * (days * 24 + hour) + ':' + String(min).padStart(2, '0') + ':' + String(sec).padStart(2, '0');
719
+ } else {
720
+ let p = off;
721
+ const year = buf.readUInt16LE(p); p += 2;
722
+ const month = buf[p++];
723
+ const day = buf[p++];
724
+ let hour = 0, minute = 0, second = 0;
725
+ if (len >= 7) { hour = buf[p++]; minute = buf[p++]; second = buf[p++]; }
726
+ value = String(year).padStart(4, '0') + '-' + String(month).padStart(2, '0') + '-' + String(day).padStart(2, '0');
727
+ if (len >= 7) value += ' ' + String(hour).padStart(2, '0') + ':' + String(minute).padStart(2, '0') + ':' + String(second).padStart(2, '0');
728
+ }
729
+ off += len;
730
+ return { value, off };
731
+ }
732
+ case 0xfc: case 0xfd: case 0xfe: case 0xf9: case 0xf6: { // BLOB / VAR_STRING / STRING / ...
733
+ const { value, off: noff } = readLenenc(buf, off);
734
+ return { value, off: noff };
735
+ }
736
+ case 0xf0: { // NULL
737
+ return { value: null, off };
738
+ }
739
+ default: {
740
+ const { value, off: noff } = readLenenc(buf, off);
741
+ return { value, off: noff };
742
+ }
743
+ }
744
+ }
745
+
746
+ _handleQuery(sql, values) {
478
747
  (async () => {
479
748
  try {
480
- const statements = splitStatements(sql);
749
+ const hasParams = values !== undefined && values !== null && values.length > 0;
750
+ const processedSql = hasParams ? applyParams(sql, values) : sql;
751
+ const statements = splitStatements(processedSql);
481
752
  if (statements.length > 1 && !this.multiStatements) {
482
753
  throw new Error(`too many statements (${statements.length} > 1)`);
483
754
  }
@@ -534,14 +805,18 @@ class MysqlConnection {
534
805
  safety: this.server.safety,
535
806
  maxStatements: 1,
536
807
  });
537
- if (r.type === 'select' || r.type === 'showTables' || r.type === 'showDatabases' || r.type === 'describe') {
808
+ if (r.type === 'select' || r.type === 'showTables' || r.type === 'showDatabases' || r.type === 'describe'
809
+ || r.type === 'showColumns' || r.type === 'showIndex' || r.type === 'showCreateTable'
810
+ || r.type === 'showVariables' || r.type === 'showStatus' || r.type === 'showGrants' || r.type === 'showWarnings') {
538
811
  let schema = null;
539
812
  if (r.table) {
540
813
  schema = engine.getTableSchema
541
814
  ? await engine.getTableSchema(r.table)
542
815
  : (engine._schemas ? engine._schemas[r.table] : null);
543
816
  }
544
- const { packets, sequence } = resultSetPacket(r, schema, this.sequence);
817
+ const { packets, sequence } = hasParams
818
+ ? binaryResultSetPacket(r, schema, this.sequence)
819
+ : resultSetPacket(r, schema, this.sequence);
545
820
  this.sequence = sequence;
546
821
  this.socket.write(Buffer.concat(packets));
547
822
  } else {
@@ -630,6 +905,10 @@ class MysqlServer {
630
905
  }
631
906
  };
632
907
  collect(stmt);
908
+ const isInfoSchema = tables.some(t => t && String(t).toLowerCase().startsWith('information_schema.'));
909
+ if (isInfoSchema) {
910
+ return { engine: await this._getEngine(currentDb), sql };
911
+ }
633
912
  let db = null;
634
913
  for (const t of tables) {
635
914
  if (t && t.indexOf('.') !== -1) {