jsql-neo 4.2.0 → 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,9 +1,6 @@
1
- # JSQL-NEO v4.2.0
1
+ # JSQL-NEO v4.0.1
2
2
 
3
- Rust-powered embedded database with **three engines + SQL + MySQL-compatible server + Redis-style storage** in one npm package.
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)
3
+ Rust-powered embedded database with **three engines + SQL + Redis-style storage** in one npm package.
7
4
 
8
5
  ## Engines
9
6
 
@@ -33,54 +30,7 @@ await jsql.executeSQL(db, 'DELETE FROM users WHERE id = 2');
33
30
  await db.stop();
34
31
  ```
35
32
 
36
- Supported SQL: `CREATE/DROP TABLE` (incl. `IF [NOT] EXISTS`), `CREATE/DROP DATABASE`, `USE` / `SHOW DATABASES` / `SHOW TABLES [FROM db]`, `INSERT` (multi-row, `ON DUPLICATE KEY UPDATE`), `SELECT` (`WHERE` / `ORDER BY` / `LIMIT` / `OFFSET` / `GROUP BY` / `HAVING` / aggregates `COUNT`/`SUM`/`AVG`/`MIN`/`MAX` / `DISTINCT` / `JOIN` / `UNION [ALL]` / `CASE WHEN` / subqueries `IN (…)` & scalar `(SELECT …)` / `BETWEEN`/`LIKE`/`IS NULL`), `UPDATE`, `DELETE`, `BEGIN`/`COMMIT`/`ROLLBACK`, prepared statements with `?` placeholders.
37
-
38
- Type names accept MySQL aliases: `INTEGER/INT/BIGINT/TINYINT/SMALLINT`, `STRING/TEXT/VARCHAR(n)/CHAR(n)`, `FLOAT/DOUBLE/REAL/NUMERIC/DECIMAL`, `BOOLEAN/BOOL`, `DATE/DATETIME/TIMESTAMP`, `JSON/OBJECT/ARRAY`.
39
-
40
- ## MySQL-Compatible Server (multi-database)
41
-
42
- A native MySQL protocol server on TCP 3306 — any `mysql` / `mysql2` / GUI client connects directly.
43
-
44
- ```bash
45
- npx jsql server start --port 3306 --data-dir ./data # 后台常驻
46
- npx jsql server status # 状态
47
- npx jsql server stop # 停止
48
- ```
49
-
50
- **Multi-database & 一键建表:** each database is an independent directory (`<dataDir>/<db>`).
51
-
52
- ```sql
53
- -- 随意连接即可直接建表(自动落到默认库 default)
54
- CREATE TABLE users (id INTEGER PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50));
55
-
56
- -- 一条 SQL 跨库建库+建表(库不存在自动创建)
57
- CREATE TABLE shop.orders (id INTEGER PRIMARY KEY, amount FLOAT, `user` STRING);
58
-
59
- -- 连接时指定尚不存在的库会自动创建:mysql -uroot -D mydb
60
- -- 查询/增删改也能带库前缀:INSERT INTO shop.orders ..., SELECT * FROM shop.orders, SHOW TABLES FROM shop;
61
- ```
62
-
63
- - 连接指定未存在的库 → **自动建库**,无需先 `CREATE DATABASE`
64
- - 未 `USE` 任何库 → 落到默认库,可直接 `CREATE TABLE`
65
- - 全量多库 DDL/DML 支持 `db.table` 点号语法 + `SHOW TABLES FROM db`
66
- - **多用户 + 库级权限**: 每个用户可配置可访问的数据库白名单(`{ user: { password, databases: [...] } }`)
67
-
68
- ```json
69
- { "port": 3306, "dataDir": "/var/lib/jsql",
70
- "auth": { "admin": { "password": "secret", "databases": ["shop", "blog"] }, "reader": "readpass" } }
71
- ```
72
-
73
- ## Transactions
74
-
75
- ```js
76
- db.begin(); // 默认 REPEATABLE_READ:begin 时保存行快照
77
- db.insert('t', { id: 2 });
78
- db.rollback(); // 删除新增、还原更新、恢复自增 —— 回滚生效
79
- db.begin(); db.insert('t', { id: 3 }); db.commit();
80
- // 也支持 SQL:BEGIN ... COMMIT / ROLLBACK
81
- ```
82
-
83
- `begin(isolationLevel)` 可选 `'REPEATABLE_READ'`(快照回滚,默认)或 `'READ_COMMITTED'`(仅回滚自增)。
33
+ Supported: `CREATE/DROP TABLE`, `INSERT` (multi-row, `ON DUPLICATE KEY UPDATE`), `SELECT` (`WHERE`/`ORDER BY`/`LIMIT`/`OFFSET`/`GROUP BY`/`HAVING`/aggregates), `UPDATE`, `DELETE`, prepared statements with `?` placeholders.
84
34
 
85
35
  ## Redis-Style Storage Modes
86
36
 
@@ -154,58 +104,62 @@ db.save();
154
104
  | `updateById(table, id, data)` | ✅ | ✅ | ✅ | O(1) PK update |
155
105
  | `removeById(table, id)` | ✅ | ✅ | ✅ | O(1) PK delete |
156
106
  | `dropTable(name)` | ✅ | ✅ | ✅ | Remove table |
157
- | `insertMany(table, items)` | ✅ | ✅ | ✅ | Batch insert |
158
- | `update(table, query, updates)` | ✅ | ✅ | ✅ | Batch update by query |
159
- | `delete(table, query)` | ✅ | ✅ | ✅ | Batch delete by query |
160
- | `truncate(table)` | ✅ | ✅ | ✅ | Delete all rows |
161
- | `begin()/commit()/rollback()` | ✅ | ✅ | ✅ | Transactions (snapshot rollback) |
162
107
  | `executeSQL(db, sql, params?)` | ✅ | ✅ | ✅ | Run SQL statements |
163
108
 
164
109
  ## Schema Field Options
165
110
 
166
111
  ```js
167
112
  {
168
- type: 'string' | 'integer' | 'float' | 'boolean' | 'date' | 'object' | 'array',
169
- primaryKey: true, // PK field (auto-indexed) — `primary: true` 也接受
113
+ type: 'string' | 'integer' | 'float' | 'boolean',
114
+ primaryKey: true, // PK field (auto-indexed)
170
115
  autoIncrement: true, // Auto-generate integer PK
171
- unique: true, // Unique constraint (auto-indexed)
172
116
  length: 32, // Max string length
173
117
  default: 'value', // Default value
174
- nullable: true, // Allow null
175
- required: true, // Not-null constraint
176
- foreignKey: { table, field, onDelete }, // FK constraint
177
- check: (val) => bool, // CHECK constraint
178
- computed: (row) => val // Computed field
118
+ nullable: true // Allow null
179
119
  }
180
120
  ```
181
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
+
182
155
  ## Features
183
156
 
184
- **Engines & Storage**
185
- - Three engines with one shared API: Native (N-API Rust, fastest), WASM (wasm-pack, zero native deps), Pure JS (`Database`)
186
- - Redis-style storage modes: `memory` / `hybrid` (async flush + LRU eviction + lazy reload) / `disk` (fast flush, memory as cache)
187
- - Atomic per-table files (`<dir>/<table>.jsql.json` + `meta.json`, tmp + rename)
188
- - WAL + snapshot crash recovery
189
- - O(1) primary-key hash index (`FxHashMap` / `Map`) + B-Tree range indexes
190
-
191
- **SQL**
192
- - Full statement coverage: DDL / DML / SELECT (aggregates, `GROUP BY`/`HAVING`, `JOIN`, `UNION`, `CASE WHEN`, subqueries) / transactions
193
- - Prepared statements with `?` placeholders
194
- - MySQL type-name aliases + `db.table` cross-database syntax + `SHOW TABLES FROM db`
195
-
196
- **MySQL Server & CLI**
197
- - MySQL protocol server (TCP 3306), compatible with `mysql`/`mysql2`/GUI clients
198
- - Multi-database (one dir per DB), auto-create on connect / on `db.table` usage, default-DB fallback for one-click `CREATE TABLE`
199
- - Multi-user authentication with per-database access lists (`1044`/`1049` enforcement)
200
- - `jsql server start/stop/status` background daemon with graceful shutdown
201
-
202
- **Data API**
203
- - Typed schema with constraints: PK / auto-increment / unique / required / length / default / FK / CHECK / computed fields
204
- - Batch insert / update / delete, cursor-based pagination, upsert
205
- - Transactions with snapshot rollback (default `REPEATABLE_READ`)
206
- - CSV import/export, JSON schema validation, soft delete, versioning, views, triggers, attachable databases
207
- - Hooks & change events (`beforeInsert`/`afterUpdate`/…) for plugins and audit
208
-
209
- **Security**
210
- - SQL injection hardening: comments/`LOAD_FILE`/dangerous statements blocked by default, ReDoS-safe regex
211
- - Rate-limited auth (max auth fails), oversized-row protection, safe name/path validation (`..` blocked)
157
+ - Three engines: Native (N-API Rust), WASM (wasm-pack Rust), Pure JS (local JSON)
158
+ - SQL engine with prepared statements
159
+ - Redis-style hybrid/disk storage: memory-first + async flush + LRU eviction + lazy reload
160
+ - O(1) primary key hash index (`FxHashMap` / `Map`)
161
+ - B-Tree indexing for range queries
162
+ - WAL + snapshot crash recovery (server engine)
163
+ - Batch insert / update / delete
164
+ - Cursor-based pagination
165
+ - Transaction support (server engine)
@@ -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) {