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 +48 -94
- package/lib/mysql_server.js +284 -5
- package/lib/sql.js +425 -53
- package/lib/table.js +4 -2
- package/package.json +10 -1
- package/wasm/browser.mjs +404 -0
- package/wasm/browser_bg.mjs +462 -0
package/README.md
CHANGED
|
@@ -1,9 +1,6 @@
|
|
|
1
|
-
# JSQL-NEO v4.
|
|
1
|
+
# JSQL-NEO v4.0.1
|
|
2
2
|
|
|
3
|
-
Rust-powered embedded database with **three engines + SQL +
|
|
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
|
|
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'
|
|
169
|
-
primaryKey: true, // PK field (auto-indexed)
|
|
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
|
|
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
|
-
|
|
185
|
-
-
|
|
186
|
-
- Redis-style storage
|
|
187
|
-
-
|
|
188
|
-
-
|
|
189
|
-
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
-
|
|
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)
|
package/lib/mysql_server.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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 } =
|
|
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) {
|