jsql-neo 4.0.2 → 4.1.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/bin/jsql +69 -0
- package/bin/jsql-server +90 -0
- package/bin/jsql-server-control.js +177 -0
- package/lib/database.js +98 -9
- package/lib/errors.js +6 -0
- package/lib/jsql_format.js +33 -14
- package/lib/mod.js +43 -67
- package/lib/mysql_compat.js +134 -17
- package/lib/mysql_server.js +315 -22
- package/lib/native_client.js +95 -32
- package/lib/sql.js +646 -122
- package/lib/table.js +114 -12
- package/lib/wasm_client.js +59 -6
- package/native/jsql-neo-native.node +0 -0
- package/nativesrc/jsql-neo-core/Cargo.lock +455 -0
- package/nativesrc/jsql-neo-core/src/engine/hybrid.rs +12 -6
- package/nativesrc/jsql-neo-core/src/engine/memory.rs +28 -7
- package/nativesrc/jsql-neo-core/src/engine/mod.rs +22 -1
- package/nativesrc/jsql-neo-core/src/engine/table.rs +53 -23
- package/nativesrc/jsql-neo-core/src/storage/mod.rs +2 -1
- package/nativesrc/jsql-neo-core/src/storage/wal.rs +1 -0
- package/nativesrc/jsql-neo-native/build.rs +3 -0
- package/nativesrc/jsql-neo-native/jsql-neo-native.node +0 -0
- package/nativesrc/jsql-neo-native/src/lib.rs +75 -87
- package/nativesrc/jsql-neo-wasm/Cargo.lock +465 -0
- package/nativesrc/jsql-neo-wasm/Cargo.toml +15 -0
- package/nativesrc/jsql-neo-wasm/src/lib.rs +423 -0
- package/package.json +8 -1
- package/wasm/jsql_neo_wasm.d.ts +2 -0
- package/wasm/jsql_neo_wasm.js +5 -0
- package/wasm/jsql_neo_wasm_bg.wasm +0 -0
- package/wasm/jsql_neo_wasm_bg.wasm.d.ts +1 -0
package/lib/mysql_server.js
CHANGED
|
@@ -6,7 +6,9 @@
|
|
|
6
6
|
|
|
7
7
|
const net = require('net');
|
|
8
8
|
const crypto = require('crypto');
|
|
9
|
-
const
|
|
9
|
+
const path = require('path');
|
|
10
|
+
const fs = require('fs');
|
|
11
|
+
const { executeSQL, parseSQL, splitStatements } = require('./sql');
|
|
10
12
|
const Database = require('./database');
|
|
11
13
|
|
|
12
14
|
const SERVER_VERSION = '8.0.0-jsql-neo';
|
|
@@ -162,7 +164,7 @@ function columnTypeFromSchema(def) {
|
|
|
162
164
|
return MYSQL_TYPE_VAR_STRING;
|
|
163
165
|
}
|
|
164
166
|
|
|
165
|
-
function resultSetPacket(result, tableSchema, baseSeq) {
|
|
167
|
+
function resultSetPacket(result, tableSchema, baseSeq, rawRow) {
|
|
166
168
|
const packets = [];
|
|
167
169
|
let sequence = baseSeq || 0;
|
|
168
170
|
const push = (buf) => {
|
|
@@ -174,12 +176,28 @@ function resultSetPacket(result, tableSchema, baseSeq) {
|
|
|
174
176
|
packets.push(buf);
|
|
175
177
|
};
|
|
176
178
|
|
|
179
|
+
const inferType = (name, value) => {
|
|
180
|
+
if (tableSchema && tableSchema[name]) return columnTypeFromSchema(tableSchema[name]);
|
|
181
|
+
if (value === null || value === undefined) return MYSQL_TYPE_NULL;
|
|
182
|
+
if (typeof value === 'number') return Number.isInteger(value) ? MYSQL_TYPE_LONGLONG : MYSQL_TYPE_DOUBLE;
|
|
183
|
+
if (typeof value === 'boolean') return MYSQL_TYPE_TINY;
|
|
184
|
+
if (typeof value === 'object') return MYSQL_TYPE_JSON;
|
|
185
|
+
return MYSQL_TYPE_VAR_STRING;
|
|
186
|
+
};
|
|
187
|
+
|
|
177
188
|
push(encodeLenenc((result.columns || []).length));
|
|
178
|
-
|
|
189
|
+
const sampleRow = (result.rows && result.rows[0]) || null;
|
|
190
|
+
for (let ci = 0; ci < (result.columns || []).length; ci++) {
|
|
191
|
+
const name = result.columns[ci];
|
|
192
|
+
let sample;
|
|
193
|
+
if (sampleRow !== null) {
|
|
194
|
+
if (Array.isArray(sampleRow)) sample = sampleRow[ci];
|
|
195
|
+
else sample = sampleRow[name];
|
|
196
|
+
}
|
|
179
197
|
const col = {
|
|
180
198
|
name,
|
|
181
199
|
table: result.table || '',
|
|
182
|
-
type:
|
|
200
|
+
type: inferType(name, sample),
|
|
183
201
|
length: 1024,
|
|
184
202
|
};
|
|
185
203
|
push(columnDefinition(col));
|
|
@@ -192,6 +210,8 @@ function resultSetPacket(result, tableSchema, baseSeq) {
|
|
|
192
210
|
const v = row[i];
|
|
193
211
|
if (v === null || v === undefined) {
|
|
194
212
|
parts.push(Buffer.from([0xfb]));
|
|
213
|
+
} else if (typeof v === 'boolean') {
|
|
214
|
+
parts.push(encodeLenencString(v ? '1' : '0'));
|
|
195
215
|
} else if (typeof v === 'object') {
|
|
196
216
|
parts.push(encodeLenencString(JSON.stringify(v)));
|
|
197
217
|
} else {
|
|
@@ -227,9 +247,22 @@ class MysqlConnection {
|
|
|
227
247
|
}
|
|
228
248
|
this.socket.on('data', chunk => this._onData(chunk));
|
|
229
249
|
this.socket.on('error', () => {});
|
|
250
|
+
this._armIdleTimer();
|
|
230
251
|
this._send(handshakePacket(this.connectionId, this.seed));
|
|
231
252
|
}
|
|
232
253
|
|
|
254
|
+
_armIdleTimer() {
|
|
255
|
+
if (this._idleTimer) { clearTimeout(this._idleTimer); this._idleTimer = null; }
|
|
256
|
+
const ttl = this.server.idleTimeout;
|
|
257
|
+
if (!this.authenticated) return;
|
|
258
|
+
if (ttl <= 0) return;
|
|
259
|
+
this._idleTimer = setTimeout(() => {
|
|
260
|
+
this.server._onSecurityEvent({ type: 'idle-timeout', user: this.user, remote: this.socket.remoteAddress });
|
|
261
|
+
this.socket.destroy();
|
|
262
|
+
}, ttl);
|
|
263
|
+
this._idleTimer.unref();
|
|
264
|
+
}
|
|
265
|
+
|
|
233
266
|
_send(payload) {
|
|
234
267
|
if (!this.socket.writable) return;
|
|
235
268
|
const header = Buffer.alloc(4);
|
|
@@ -241,6 +274,7 @@ class MysqlConnection {
|
|
|
241
274
|
|
|
242
275
|
_onData(chunk) {
|
|
243
276
|
this.buffer = Buffer.concat([this.buffer, chunk]);
|
|
277
|
+
this._armIdleTimer();
|
|
244
278
|
if (this.buffer.length > this.server.maxPacketSize + 4) {
|
|
245
279
|
this._malicious('packet exceeds maxPacketSize (' + this.server.maxPacketSize + ')');
|
|
246
280
|
return;
|
|
@@ -280,12 +314,13 @@ class MysqlConnection {
|
|
|
280
314
|
case 0x01: this.socket.end(); break; // COM_QUIT
|
|
281
315
|
case 0x02: { // COM_INIT_DB
|
|
282
316
|
const db = body.toString('utf8');
|
|
283
|
-
if (
|
|
317
|
+
if (!this.server._safeDbName(db)) {
|
|
284
318
|
this._malicious('COM_INIT_DB with invalid database name');
|
|
285
319
|
return;
|
|
286
320
|
}
|
|
287
|
-
this.
|
|
288
|
-
|
|
321
|
+
this.server._switchDb(this, db)
|
|
322
|
+
.then(() => this._send(okPacket()))
|
|
323
|
+
.catch(e => this._send(errPacket(e.code || 1049, e.message)));
|
|
289
324
|
break;
|
|
290
325
|
}
|
|
291
326
|
case 0x03: {
|
|
@@ -297,6 +332,7 @@ class MysqlConnection {
|
|
|
297
332
|
this._handleQuery(sql);
|
|
298
333
|
break;
|
|
299
334
|
}
|
|
335
|
+
case 0x11: this._handleChangeUser(body, cmdSeq); break; // COM_CHANGE_USER
|
|
300
336
|
case 0x0e: this._send(okPacket()); break; // COM_PING
|
|
301
337
|
case 0x1f: this._send(okPacket()); break; // COM_RESET_CONNECTION
|
|
302
338
|
case 0x0a: { // COM_PROCESS_INFO
|
|
@@ -318,7 +354,56 @@ class MysqlConnection {
|
|
|
318
354
|
}
|
|
319
355
|
}
|
|
320
356
|
|
|
321
|
-
|
|
357
|
+
_handleChangeUser(body, cmdSeq) {
|
|
358
|
+
(async () => {
|
|
359
|
+
try {
|
|
360
|
+
let pos = 0;
|
|
361
|
+
const userEnd = body.indexOf(0, pos);
|
|
362
|
+
if (userEnd === -1) throw new Error('malformed COM_CHANGE_USER');
|
|
363
|
+
const user = body.slice(pos, userEnd).toString('utf8');
|
|
364
|
+
pos = userEnd + 1;
|
|
365
|
+
const lenenc = parseLenenc(body, pos);
|
|
366
|
+
let authResponse = Buffer.alloc(0);
|
|
367
|
+
if (lenenc.value !== null) {
|
|
368
|
+
authResponse = body.slice(pos + lenenc.size, pos + lenenc.size + lenenc.value);
|
|
369
|
+
pos += lenenc.size + lenenc.value;
|
|
370
|
+
} else {
|
|
371
|
+
pos += lenenc.size;
|
|
372
|
+
}
|
|
373
|
+
let db = null;
|
|
374
|
+
if (pos < body.length) {
|
|
375
|
+
const dbEnd = body.indexOf(0, pos);
|
|
376
|
+
if (dbEnd !== -1) {
|
|
377
|
+
db = body.slice(pos, dbEnd).toString('utf8');
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
const valid = this.server._checkAuth(user, authResponse, this.seed);
|
|
381
|
+
if (!valid) {
|
|
382
|
+
this._send(errPacket(1045, `Access denied for user '${user}'`));
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
if (db) {
|
|
386
|
+
if (!this.server._canAccessDb(user, db)) {
|
|
387
|
+
this._send(errPacket(1044, `Access denied for user '${user}' to database '${db}'`));
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
try {
|
|
391
|
+
await this.server._getDatabase(db);
|
|
392
|
+
} catch (e) {
|
|
393
|
+
this._send(errPacket(1049, `Unknown database '${db}'`));
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
this.user = user;
|
|
398
|
+
this.currentDb = db;
|
|
399
|
+
this._send(okPacket());
|
|
400
|
+
} catch (e) {
|
|
401
|
+
this._send(errPacket(toMysqlErrno(e), e.message));
|
|
402
|
+
}
|
|
403
|
+
})();
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
async _handleAuth(payload) {
|
|
322
407
|
try {
|
|
323
408
|
const caps = payload.readUInt32LE(0);
|
|
324
409
|
let pos = 32; // 4 caps + 4 maxpacket + 1 charset + 23 reserved
|
|
@@ -365,6 +450,22 @@ class MysqlConnection {
|
|
|
365
450
|
this.multiStatements = !!(caps & CLIENT_MULTI_STATEMENTS);
|
|
366
451
|
this.authenticated = true;
|
|
367
452
|
if (this._authTimer) { clearTimeout(this._authTimer); this._authTimer = null; }
|
|
453
|
+
this._armIdleTimer();
|
|
454
|
+
if (db) {
|
|
455
|
+
if (!this.server._canAccessDb(this.user, db)) {
|
|
456
|
+
this._send(errPacket(1044, `Access denied for user '${this.user}' to database '${db}'`));
|
|
457
|
+
this.socket.end();
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
try {
|
|
461
|
+
await this.server._getDatabase(db);
|
|
462
|
+
this.currentDb = db;
|
|
463
|
+
} catch (e) {
|
|
464
|
+
this._send(errPacket(1049, `Unknown database '${db}'`));
|
|
465
|
+
this.socket.end();
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
}
|
|
368
469
|
this._send(okPacket());
|
|
369
470
|
} catch (e) {
|
|
370
471
|
this._send(errPacket(1105, 'auth failed: ' + e.message));
|
|
@@ -375,17 +476,64 @@ class MysqlConnection {
|
|
|
375
476
|
_handleQuery(sql) {
|
|
376
477
|
(async () => {
|
|
377
478
|
try {
|
|
378
|
-
const
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
479
|
+
const statements = splitStatements(sql);
|
|
480
|
+
if (statements.length > 1 && !this.multiStatements) {
|
|
481
|
+
throw new Error(`too many statements (${statements.length} > 1)`);
|
|
482
|
+
}
|
|
483
|
+
for (const stmtSql of statements) {
|
|
484
|
+
const stmt = parseSQL(stmtSql);
|
|
485
|
+
switch (stmt.type) {
|
|
486
|
+
case 'use': {
|
|
487
|
+
if (!this.server._canAccessDb(this.user, stmt.database)) {
|
|
488
|
+
this._send(errPacket(1044, `Access denied for user '${this.user}' to database '${stmt.database}'`));
|
|
489
|
+
continue;
|
|
490
|
+
}
|
|
491
|
+
try {
|
|
492
|
+
await this.server._getDatabase(stmt.database);
|
|
493
|
+
this.currentDb = stmt.database;
|
|
494
|
+
this._send(okPacket());
|
|
495
|
+
} catch (e) {
|
|
496
|
+
this._send(errPacket(1049, `Unknown database '${stmt.database}'`));
|
|
497
|
+
}
|
|
498
|
+
continue;
|
|
499
|
+
}
|
|
500
|
+
case 'showDatabases': {
|
|
501
|
+
const dbs = await this.server.listDatabases();
|
|
502
|
+
const allowed = this.server._userDbList(this.user);
|
|
503
|
+
const names = allowed === null ? dbs : dbs.filter(n => allowed.includes(n));
|
|
504
|
+
const { packets, sequence } = resultSetPacket({
|
|
505
|
+
type: 'showDatabases', columns: ['Database'], rows: names.map(n => [n]),
|
|
506
|
+
}, null, this.sequence);
|
|
507
|
+
this.sequence = sequence;
|
|
508
|
+
this.socket.write(Buffer.concat(packets));
|
|
509
|
+
continue;
|
|
510
|
+
}
|
|
511
|
+
case 'createDatabase': {
|
|
512
|
+
if (!this.server._canAccessDb(this.user, stmt.database)) {
|
|
513
|
+
this._send(errPacket(1044, `Access denied for user '${this.user}' to database '${stmt.database}'`));
|
|
514
|
+
continue;
|
|
515
|
+
}
|
|
516
|
+
await this.server.createDatabase(stmt.database, { ifNotExists: stmt.ifNotExists });
|
|
517
|
+
this._send(okPacket());
|
|
518
|
+
continue;
|
|
519
|
+
}
|
|
520
|
+
case 'dropDatabase': {
|
|
521
|
+
await this.server.dropDatabase(stmt.database, { ifExists: stmt.ifExists });
|
|
522
|
+
this._send(okPacket());
|
|
523
|
+
continue;
|
|
524
|
+
}
|
|
525
|
+
default:
|
|
526
|
+
break;
|
|
527
|
+
}
|
|
528
|
+
const engine = await this.server._getEngine(this.currentDb);
|
|
529
|
+
const r = await executeSQL(engine, stmtSql, {
|
|
530
|
+
allowComments: this.server.allowComments,
|
|
531
|
+
safety: this.server.safety,
|
|
532
|
+
maxStatements: 1,
|
|
533
|
+
});
|
|
385
534
|
if (r.type === 'select' || r.type === 'showTables' || r.type === 'showDatabases' || r.type === 'describe') {
|
|
386
535
|
let schema = null;
|
|
387
536
|
if (r.table) {
|
|
388
|
-
const engine = this.server._engine;
|
|
389
537
|
schema = engine.getTableSchema
|
|
390
538
|
? await engine.getTableSchema(r.table)
|
|
391
539
|
: (engine._schemas ? engine._schemas[r.table] : null);
|
|
@@ -413,13 +561,16 @@ class MysqlServer {
|
|
|
413
561
|
this.password = options.password || null;
|
|
414
562
|
this.auth = options.auth || null;
|
|
415
563
|
this.safety = options.safety !== false;
|
|
416
|
-
this.allowComments = options.allowComments
|
|
564
|
+
this.allowComments = options.allowComments === true;
|
|
417
565
|
this.maxPacketSize = options.maxPacketSize || 1024 * 1024;
|
|
418
566
|
this.handshakeTimeout = options.handshakeTimeout != null ? options.handshakeTimeout : 10000;
|
|
567
|
+
this.idleTimeout = options.idleTimeout != null ? options.idleTimeout : 300000;
|
|
419
568
|
this.maxAuthFails = options.maxAuthFails || 3;
|
|
420
569
|
this.maxConnections = options.maxConnections || 128;
|
|
421
570
|
this._engine = null;
|
|
422
571
|
this._ownEngine = false;
|
|
572
|
+
this._databases = new Map();
|
|
573
|
+
this._dbDir = options.dataDir ? path.resolve(options.dataDir) : null;
|
|
423
574
|
this._connectionCounter = 0;
|
|
424
575
|
this._sockets = new Set();
|
|
425
576
|
this._securityHandler = typeof options.onSecurityEvent === 'function' ? options.onSecurityEvent : null;
|
|
@@ -431,7 +582,13 @@ class MysqlServer {
|
|
|
431
582
|
}
|
|
432
583
|
}
|
|
433
584
|
|
|
434
|
-
async _getEngine() {
|
|
585
|
+
async _getEngine(dbName) {
|
|
586
|
+
if (dbName) return this._getDatabase(dbName);
|
|
587
|
+
if (this._dbDir) {
|
|
588
|
+
const err = new Error(`No database selected`);
|
|
589
|
+
err.code = 1046;
|
|
590
|
+
throw err;
|
|
591
|
+
}
|
|
435
592
|
if (this._engine) return this._engine;
|
|
436
593
|
if (this.options.engine || this.options.database) {
|
|
437
594
|
this._engine = this.options.engine || this.options.database;
|
|
@@ -444,10 +601,133 @@ class MysqlServer {
|
|
|
444
601
|
return this._engine;
|
|
445
602
|
}
|
|
446
603
|
|
|
604
|
+
_safeDbName(name) {
|
|
605
|
+
if (typeof name !== 'string' || name.length === 0 || name.length > 64) return false;
|
|
606
|
+
if (!/^[a-zA-Z0-9_$.\-]+$/.test(name)) return false;
|
|
607
|
+
if (name === '.' || name === '..' || name.includes('..')) return false;
|
|
608
|
+
return true;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
_dbPath(name) {
|
|
612
|
+
if (!this._dbDir) return null;
|
|
613
|
+
if (!this._safeDbName(name)) return null;
|
|
614
|
+
return path.join(this._dbDir, name);
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
async _getDatabase(name) {
|
|
618
|
+
if (!this._safeDbName(name)) {
|
|
619
|
+
const err = new Error(`Unknown database '${name}'`);
|
|
620
|
+
err.code = 1049;
|
|
621
|
+
throw err;
|
|
622
|
+
}
|
|
623
|
+
if (this._databases.has(name)) return this._databases.get(name);
|
|
624
|
+
const dir = this._dbPath(name);
|
|
625
|
+
if (dir && !fs.existsSync(dir)) {
|
|
626
|
+
const err = new Error(`Unknown database '${name}'`);
|
|
627
|
+
err.code = 1049;
|
|
628
|
+
throw err;
|
|
629
|
+
}
|
|
630
|
+
const engine = dir ? new Database(dir, { mode: 'hybrid' }) : new Database(':memory:');
|
|
631
|
+
if (typeof engine.start === 'function') await engine.start();
|
|
632
|
+
this._databases.set(name, engine);
|
|
633
|
+
return engine;
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
async listDatabases() {
|
|
637
|
+
if (!this._dbDir) return ['jsql'];
|
|
638
|
+
let names = [];
|
|
639
|
+
if (fs.existsSync(this._dbDir)) {
|
|
640
|
+
names = fs.readdirSync(this._dbDir).filter(n => {
|
|
641
|
+
const p = path.join(this._dbDir, n);
|
|
642
|
+
return fs.statSync(p).isDirectory() && this._safeDbName(n);
|
|
643
|
+
});
|
|
644
|
+
}
|
|
645
|
+
for (const n of this._databases.keys()) {
|
|
646
|
+
if (!names.includes(n)) names.push(n);
|
|
647
|
+
}
|
|
648
|
+
return names.sort();
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
async createDatabase(name, opts = {}) {
|
|
652
|
+
if (!this._safeDbName(name)) {
|
|
653
|
+
const err = new Error(`Invalid database name '${name}'`);
|
|
654
|
+
err.code = 1105;
|
|
655
|
+
throw err;
|
|
656
|
+
}
|
|
657
|
+
if (this._databases.has(name)) {
|
|
658
|
+
if (opts.ifNotExists) return this._databases.get(name);
|
|
659
|
+
const err = new Error(`Can't create database '${name}'; database exists`);
|
|
660
|
+
err.code = 1007;
|
|
661
|
+
throw err;
|
|
662
|
+
}
|
|
663
|
+
const dir = this._dbPath(name);
|
|
664
|
+
if (dir && fs.existsSync(dir)) {
|
|
665
|
+
if (opts.ifNotExists) return this._getDatabase(name);
|
|
666
|
+
const err = new Error(`Can't create database '${name}'; database exists`);
|
|
667
|
+
err.code = 1007;
|
|
668
|
+
throw err;
|
|
669
|
+
}
|
|
670
|
+
if (dir) fs.mkdirSync(dir, { recursive: true });
|
|
671
|
+
const engine = dir ? new Database(dir, { mode: 'hybrid' }) : new Database(':memory:');
|
|
672
|
+
if (typeof engine.start === 'function') await engine.start();
|
|
673
|
+
this._databases.set(name, engine);
|
|
674
|
+
return engine;
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
async dropDatabase(name, opts = {}) {
|
|
678
|
+
if (!this._safeDbName(name)) {
|
|
679
|
+
const err = new Error(`Unknown database '${name}'`);
|
|
680
|
+
err.code = 1049;
|
|
681
|
+
throw err;
|
|
682
|
+
}
|
|
683
|
+
const engine = this._databases.get(name);
|
|
684
|
+
if (!engine) {
|
|
685
|
+
if (opts.ifExists) return;
|
|
686
|
+
const err = new Error(`Can't drop database '${name}'; database doesn't exist`);
|
|
687
|
+
err.code = 1008;
|
|
688
|
+
throw err;
|
|
689
|
+
}
|
|
690
|
+
if (typeof engine.stop === 'function') {
|
|
691
|
+
try { await engine.stop(); } catch (e) {}
|
|
692
|
+
}
|
|
693
|
+
this._databases.delete(name);
|
|
694
|
+
const dir = this._dbPath(name);
|
|
695
|
+
if (dir && fs.existsSync(dir)) {
|
|
696
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
_userDbList(user) {
|
|
701
|
+
if (!this.auth) return null;
|
|
702
|
+
const entry = this.auth[user];
|
|
703
|
+
if (entry && typeof entry === 'object') {
|
|
704
|
+
if (Array.isArray(entry.databases)) return entry.databases;
|
|
705
|
+
return null;
|
|
706
|
+
}
|
|
707
|
+
return [];
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
_canAccessDb(user, dbName) {
|
|
711
|
+
const list = this._userDbList(user);
|
|
712
|
+
if (list === null) return true;
|
|
713
|
+
return list.includes(dbName);
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
async _switchDb(conn, dbName) {
|
|
717
|
+
if (!this._canAccessDb(conn.user, dbName)) {
|
|
718
|
+
const err = new Error(`Access denied for user '${conn.user}' to database '${dbName}'`);
|
|
719
|
+
err.code = 1044;
|
|
720
|
+
throw err;
|
|
721
|
+
}
|
|
722
|
+
await this._getDatabase(dbName);
|
|
723
|
+
conn.currentDb = dbName;
|
|
724
|
+
}
|
|
725
|
+
|
|
447
726
|
_checkAuth(user, authResponse, seed) {
|
|
448
727
|
if (this.auth) {
|
|
449
728
|
if (!Object.prototype.hasOwnProperty.call(this.auth, user)) return false;
|
|
450
|
-
const
|
|
729
|
+
const entry = this.auth[user];
|
|
730
|
+
const pwd = entry && typeof entry === 'object' ? entry.password : entry;
|
|
451
731
|
if (!pwd) return authResponse.length === 0;
|
|
452
732
|
const pwdHash1 = crypto.createHash('sha1').update(pwd).digest();
|
|
453
733
|
const pwdHash2 = crypto.createHash('sha1').update(pwdHash1).digest();
|
|
@@ -456,7 +736,11 @@ class MysqlServer {
|
|
|
456
736
|
for (let i = 0; i < 20; i++) expected[i] = pwdHash1[i] ^ seedHash[i];
|
|
457
737
|
return authResponse.length === 20 && crypto.timingSafeEqual(expected, authResponse);
|
|
458
738
|
}
|
|
459
|
-
if (this.user === null)
|
|
739
|
+
if (this.user === null) {
|
|
740
|
+
// 未配置认证:默认拒绝所有登录(防配置遗漏导致认证旁路)。
|
|
741
|
+
// 仅当显式设置 noAuth: true 时允许无认证连接(仅限本地开发)。
|
|
742
|
+
return this.options.noAuth === true;
|
|
743
|
+
}
|
|
460
744
|
if (user !== this.user) return false;
|
|
461
745
|
if (!this.password) return authResponse.length === 0;
|
|
462
746
|
const pwdHash1 = crypto.createHash('sha1').update(this.password).digest();
|
|
@@ -468,7 +752,8 @@ class MysqlServer {
|
|
|
468
752
|
}
|
|
469
753
|
|
|
470
754
|
listen(cb) {
|
|
471
|
-
this.
|
|
755
|
+
const ready = this._dbDir ? Promise.resolve() : this._getEngine();
|
|
756
|
+
ready.then(() => {
|
|
472
757
|
this._server = net.createServer(socket => {
|
|
473
758
|
if (this._sockets.size >= this.maxConnections) {
|
|
474
759
|
this._onSecurityEvent({ type: 'max-connections', remote: socket.remoteAddress });
|
|
@@ -494,8 +779,16 @@ class MysqlServer {
|
|
|
494
779
|
close(cb) {
|
|
495
780
|
const done = () => {
|
|
496
781
|
for (const s of this._sockets) s.destroy();
|
|
782
|
+
const stops = [];
|
|
497
783
|
if (this._ownEngine && this._engine && typeof this._engine.stop === 'function') {
|
|
498
|
-
this._engine.stop()
|
|
784
|
+
stops.push(this._engine.stop());
|
|
785
|
+
}
|
|
786
|
+
for (const engine of this._databases.values()) {
|
|
787
|
+
if (engine && typeof engine.stop === 'function') stops.push(engine.stop());
|
|
788
|
+
}
|
|
789
|
+
this._databases.clear();
|
|
790
|
+
if (stops.length > 0) {
|
|
791
|
+
Promise.allSettled(stops).then(() => cb && cb());
|
|
499
792
|
} else if (cb) cb();
|
|
500
793
|
};
|
|
501
794
|
if (this._server) {
|
package/lib/native_client.js
CHANGED
|
@@ -11,16 +11,9 @@ function encodeBatch(rows) {
|
|
|
11
11
|
if (rows.length === 0) return new Uint8Array(0);
|
|
12
12
|
const fieldNames = Object.keys(rows[0]);
|
|
13
13
|
const nFields = fieldNames.length;
|
|
14
|
-
const est = 100 + rows.length *
|
|
15
|
-
|
|
14
|
+
const est = 100 + rows.length * 120;
|
|
15
|
+
const buf = Buffer.allocUnsafe(est);
|
|
16
16
|
let off = 0;
|
|
17
|
-
const ensure = (n) => {
|
|
18
|
-
if (off + n > buf.length) {
|
|
19
|
-
const nb = Buffer.allocUnsafe(Math.max(buf.length * 2, off + n));
|
|
20
|
-
buf.copy(nb, 0, 0, off);
|
|
21
|
-
buf = nb;
|
|
22
|
-
}
|
|
23
|
-
};
|
|
24
17
|
|
|
25
18
|
off = buf.writeUInt8(nFields, off);
|
|
26
19
|
for (let fi = 0; fi < nFields; fi++) {
|
|
@@ -41,12 +34,19 @@ function encodeBatch(rows) {
|
|
|
41
34
|
if (Number.isInteger(v)) { if (v >= -2147483648 && v <= 2147483647) { off = buf.writeUInt8(INT32_TAG, off); off = buf.writeInt32LE(v, off); } else { off = buf.writeUInt8(INT64_TAG, off); off = buf.writeBigInt64LE(BigInt(v), off); } }
|
|
42
35
|
else { off = buf.writeUInt8(FLOAT_TAG, off); off = buf.writeDoubleLE(v, off); }
|
|
43
36
|
} else if (typeof v === 'string') {
|
|
44
|
-
const sl = Buffer.byteLength(v, 'utf8');
|
|
45
|
-
ensure(5 + sl);
|
|
46
37
|
off = buf.writeUInt8(STR_TAG, off);
|
|
38
|
+
const sl = Buffer.byteLength(v, 'utf8');
|
|
47
39
|
off = buf.writeUInt32LE(sl, off);
|
|
48
40
|
off += buf.write(v, off, sl, 'utf8');
|
|
49
|
-
} else
|
|
41
|
+
} else if (typeof v === 'boolean') {
|
|
42
|
+
off = buf.writeUInt8(BOOL_TAG, off); off = buf.writeUInt8(v ? 1 : 0, off);
|
|
43
|
+
} else {
|
|
44
|
+
const s = typeof v === 'object' ? JSON.stringify(v) : String(v);
|
|
45
|
+
off = buf.writeUInt8(STR_TAG, off);
|
|
46
|
+
const sl = Buffer.byteLength(s, 'utf8');
|
|
47
|
+
off = buf.writeUInt32LE(sl, off);
|
|
48
|
+
off += buf.write(s, off, sl, 'utf8');
|
|
49
|
+
}
|
|
50
50
|
}
|
|
51
51
|
if (nFields >= 2) {
|
|
52
52
|
const v = row[fieldNames[1]];
|
|
@@ -55,12 +55,19 @@ function encodeBatch(rows) {
|
|
|
55
55
|
if (Number.isInteger(v)) { if (v >= -2147483648 && v <= 2147483647) { off = buf.writeUInt8(INT32_TAG, off); off = buf.writeInt32LE(v, off); } else { off = buf.writeUInt8(INT64_TAG, off); off = buf.writeBigInt64LE(BigInt(v), off); } }
|
|
56
56
|
else { off = buf.writeUInt8(FLOAT_TAG, off); off = buf.writeDoubleLE(v, off); }
|
|
57
57
|
} else if (typeof v === 'string') {
|
|
58
|
-
const sl = Buffer.byteLength(v, 'utf8');
|
|
59
|
-
ensure(5 + sl);
|
|
60
58
|
off = buf.writeUInt8(STR_TAG, off);
|
|
59
|
+
const sl = Buffer.byteLength(v, 'utf8');
|
|
61
60
|
off = buf.writeUInt32LE(sl, off);
|
|
62
61
|
off += buf.write(v, off, sl, 'utf8');
|
|
63
|
-
} else
|
|
62
|
+
} else if (typeof v === 'boolean') {
|
|
63
|
+
off = buf.writeUInt8(BOOL_TAG, off); off = buf.writeUInt8(v ? 1 : 0, off);
|
|
64
|
+
} else {
|
|
65
|
+
const s = typeof v === 'object' ? JSON.stringify(v) : String(v);
|
|
66
|
+
off = buf.writeUInt8(STR_TAG, off);
|
|
67
|
+
const sl = Buffer.byteLength(s, 'utf8');
|
|
68
|
+
off = buf.writeUInt32LE(sl, off);
|
|
69
|
+
off += buf.write(s, off, sl, 'utf8');
|
|
70
|
+
}
|
|
64
71
|
}
|
|
65
72
|
if (nFields >= 3) {
|
|
66
73
|
const v = row[fieldNames[2]];
|
|
@@ -69,12 +76,19 @@ function encodeBatch(rows) {
|
|
|
69
76
|
if (Number.isInteger(v)) { if (v >= -2147483648 && v <= 2147483647) { off = buf.writeUInt8(INT32_TAG, off); off = buf.writeInt32LE(v, off); } else { off = buf.writeUInt8(INT64_TAG, off); off = buf.writeBigInt64LE(BigInt(v), off); } }
|
|
70
77
|
else { off = buf.writeUInt8(FLOAT_TAG, off); off = buf.writeDoubleLE(v, off); }
|
|
71
78
|
} else if (typeof v === 'string') {
|
|
72
|
-
const sl = Buffer.byteLength(v, 'utf8');
|
|
73
|
-
ensure(5 + sl);
|
|
74
79
|
off = buf.writeUInt8(STR_TAG, off);
|
|
80
|
+
const sl = Buffer.byteLength(v, 'utf8');
|
|
75
81
|
off = buf.writeUInt32LE(sl, off);
|
|
76
82
|
off += buf.write(v, off, sl, 'utf8');
|
|
77
|
-
} else
|
|
83
|
+
} else if (typeof v === 'boolean') {
|
|
84
|
+
off = buf.writeUInt8(BOOL_TAG, off); off = buf.writeUInt8(v ? 1 : 0, off);
|
|
85
|
+
} else {
|
|
86
|
+
const s = typeof v === 'object' ? JSON.stringify(v) : String(v);
|
|
87
|
+
off = buf.writeUInt8(STR_TAG, off);
|
|
88
|
+
const sl = Buffer.byteLength(s, 'utf8');
|
|
89
|
+
off = buf.writeUInt32LE(sl, off);
|
|
90
|
+
off += buf.write(s, off, sl, 'utf8');
|
|
91
|
+
}
|
|
78
92
|
}
|
|
79
93
|
if (nFields >= 4) {
|
|
80
94
|
const v = row[fieldNames[3]];
|
|
@@ -83,12 +97,19 @@ function encodeBatch(rows) {
|
|
|
83
97
|
if (Number.isInteger(v)) { if (v >= -2147483648 && v <= 2147483647) { off = buf.writeUInt8(INT32_TAG, off); off = buf.writeInt32LE(v, off); } else { off = buf.writeUInt8(INT64_TAG, off); off = buf.writeBigInt64LE(BigInt(v), off); } }
|
|
84
98
|
else { off = buf.writeUInt8(FLOAT_TAG, off); off = buf.writeDoubleLE(v, off); }
|
|
85
99
|
} else if (typeof v === 'string') {
|
|
86
|
-
const sl = Buffer.byteLength(v, 'utf8');
|
|
87
|
-
ensure(5 + sl);
|
|
88
100
|
off = buf.writeUInt8(STR_TAG, off);
|
|
101
|
+
const sl = Buffer.byteLength(v, 'utf8');
|
|
89
102
|
off = buf.writeUInt32LE(sl, off);
|
|
90
103
|
off += buf.write(v, off, sl, 'utf8');
|
|
91
|
-
} else
|
|
104
|
+
} else if (typeof v === 'boolean') {
|
|
105
|
+
off = buf.writeUInt8(BOOL_TAG, off); off = buf.writeUInt8(v ? 1 : 0, off);
|
|
106
|
+
} else {
|
|
107
|
+
const s = typeof v === 'object' ? JSON.stringify(v) : String(v);
|
|
108
|
+
off = buf.writeUInt8(STR_TAG, off);
|
|
109
|
+
const sl = Buffer.byteLength(s, 'utf8');
|
|
110
|
+
off = buf.writeUInt32LE(sl, off);
|
|
111
|
+
off += buf.write(s, off, sl, 'utf8');
|
|
112
|
+
}
|
|
92
113
|
}
|
|
93
114
|
if (nFields >= 5) {
|
|
94
115
|
const v = row[fieldNames[4]];
|
|
@@ -97,12 +118,19 @@ function encodeBatch(rows) {
|
|
|
97
118
|
if (Number.isInteger(v)) { if (v >= -2147483648 && v <= 2147483647) { off = buf.writeUInt8(INT32_TAG, off); off = buf.writeInt32LE(v, off); } else { off = buf.writeUInt8(INT64_TAG, off); off = buf.writeBigInt64LE(BigInt(v), off); } }
|
|
98
119
|
else { off = buf.writeUInt8(FLOAT_TAG, off); off = buf.writeDoubleLE(v, off); }
|
|
99
120
|
} else if (typeof v === 'string') {
|
|
100
|
-
const sl = Buffer.byteLength(v, 'utf8');
|
|
101
|
-
ensure(5 + sl);
|
|
102
121
|
off = buf.writeUInt8(STR_TAG, off);
|
|
122
|
+
const sl = Buffer.byteLength(v, 'utf8');
|
|
103
123
|
off = buf.writeUInt32LE(sl, off);
|
|
104
124
|
off += buf.write(v, off, sl, 'utf8');
|
|
105
|
-
} else
|
|
125
|
+
} else if (typeof v === 'boolean') {
|
|
126
|
+
off = buf.writeUInt8(BOOL_TAG, off); off = buf.writeUInt8(v ? 1 : 0, off);
|
|
127
|
+
} else {
|
|
128
|
+
const s = typeof v === 'object' ? JSON.stringify(v) : String(v);
|
|
129
|
+
off = buf.writeUInt8(STR_TAG, off);
|
|
130
|
+
const sl = Buffer.byteLength(s, 'utf8');
|
|
131
|
+
off = buf.writeUInt32LE(sl, off);
|
|
132
|
+
off += buf.write(s, off, sl, 'utf8');
|
|
133
|
+
}
|
|
106
134
|
}
|
|
107
135
|
for (let fi = 5; fi < nFields; fi++) {
|
|
108
136
|
const v = row[fieldNames[fi]];
|
|
@@ -111,12 +139,19 @@ function encodeBatch(rows) {
|
|
|
111
139
|
if (Number.isInteger(v)) { if (v >= -2147483648 && v <= 2147483647) { off = buf.writeUInt8(INT32_TAG, off); off = buf.writeInt32LE(v, off); } else { off = buf.writeUInt8(INT64_TAG, off); off = buf.writeBigInt64LE(BigInt(v), off); } }
|
|
112
140
|
else { off = buf.writeUInt8(FLOAT_TAG, off); off = buf.writeDoubleLE(v, off); }
|
|
113
141
|
} else if (typeof v === 'string') {
|
|
114
|
-
const sl = Buffer.byteLength(v, 'utf8');
|
|
115
|
-
ensure(5 + sl);
|
|
116
142
|
off = buf.writeUInt8(STR_TAG, off);
|
|
143
|
+
const sl = Buffer.byteLength(v, 'utf8');
|
|
117
144
|
off = buf.writeUInt32LE(sl, off);
|
|
118
145
|
off += buf.write(v, off, sl, 'utf8');
|
|
119
|
-
} else
|
|
146
|
+
} else if (typeof v === 'boolean') {
|
|
147
|
+
off = buf.writeUInt8(BOOL_TAG, off); off = buf.writeUInt8(v ? 1 : 0, off);
|
|
148
|
+
} else {
|
|
149
|
+
const s = typeof v === 'object' ? JSON.stringify(v) : String(v);
|
|
150
|
+
off = buf.writeUInt8(STR_TAG, off);
|
|
151
|
+
const sl = Buffer.byteLength(s, 'utf8');
|
|
152
|
+
off = buf.writeUInt32LE(sl, off);
|
|
153
|
+
off += buf.write(s, off, sl, 'utf8');
|
|
154
|
+
}
|
|
120
155
|
}
|
|
121
156
|
}
|
|
122
157
|
|
|
@@ -241,6 +276,10 @@ class JSQL {
|
|
|
241
276
|
this._tableNames = new Set(r.tables);
|
|
242
277
|
if (r.schemas) this._schemas = r.schemas;
|
|
243
278
|
}
|
|
279
|
+
} else {
|
|
280
|
+
JSON.parse(native.jsqlOpen('', 'memory'));
|
|
281
|
+
}
|
|
282
|
+
if (this._mode !== 'memory') {
|
|
244
283
|
if (this._flushInterval > 0) {
|
|
245
284
|
this._diskFlushTimer = setInterval(() => {
|
|
246
285
|
try { native.jsqlFlushDirty(); } catch (e) { /* ignore */ }
|
|
@@ -341,11 +380,16 @@ class JSQL {
|
|
|
341
380
|
if (!this._runHooks('beforeInsert', [table, filtered])) return [];
|
|
342
381
|
if (arr.length > 1) {
|
|
343
382
|
await this._flush();
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
383
|
+
let result;
|
|
384
|
+
for (let i = 0; i < arr.length; i += this._flushThreshold) {
|
|
385
|
+
const chunk = arr.slice(i, i + this._flushThreshold);
|
|
386
|
+
const r = await this._insertBatch(table, chunk);
|
|
387
|
+
if (r && r.error) throw new Error(r.error);
|
|
388
|
+
if (!result) result = r;
|
|
389
|
+
}
|
|
390
|
+
this._emit('insert', { table, count: arr.length, ids: result });
|
|
391
|
+
this._runHooks('afterInsert', [table, filtered, result]);
|
|
392
|
+
return result;
|
|
349
393
|
}
|
|
350
394
|
if (!this._buffer[table]) this._buffer[table] = [];
|
|
351
395
|
this._buffer[table].push(arr[0]);
|
|
@@ -473,6 +517,25 @@ class JSQL {
|
|
|
473
517
|
this._runHooks('onStop', []);
|
|
474
518
|
this._emit('stop', {});
|
|
475
519
|
}
|
|
520
|
+
|
|
521
|
+
async beginTx() {
|
|
522
|
+
this._flushOpsNow();
|
|
523
|
+
const r = JSON.parse(native.jsqlBeginTx());
|
|
524
|
+
if (r && r.ok === false) throw new Error(r.error || 'begin transaction failed');
|
|
525
|
+
return r.txId;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
async commitTx(txId) {
|
|
529
|
+
const r = JSON.parse(native.jsqlCommitTx(String(txId)));
|
|
530
|
+
if (r && r.ok === false) throw new Error(r.error || 'commit transaction failed');
|
|
531
|
+
return true;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
async rollbackTx(txId) {
|
|
535
|
+
const r = JSON.parse(native.jsqlRollbackTx(String(txId)));
|
|
536
|
+
if (r && r.ok === false) throw new Error(r.error || 'rollback transaction failed');
|
|
537
|
+
return true;
|
|
538
|
+
}
|
|
476
539
|
}
|
|
477
540
|
|
|
478
541
|
module.exports = { JSQL };
|