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.
@@ -6,7 +6,9 @@
6
6
 
7
7
  const net = require('net');
8
8
  const crypto = require('crypto');
9
- const { executeSQL } = require('./sql');
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
- for (const name of result.columns || []) {
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: tableSchema && tableSchema[name] ? columnTypeFromSchema(tableSchema[name]) : MYSQL_TYPE_VAR_STRING,
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 (!/^[a-zA-Z0-9_$.\-]+$/.test(db)) {
317
+ if (!this.server._safeDbName(db)) {
284
318
  this._malicious('COM_INIT_DB with invalid database name');
285
319
  return;
286
320
  }
287
- this.currentDb = db;
288
- this._send(okPacket());
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
- _handleAuth(payload) {
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 results = await executeSQL(this.server._engine, sql, {
379
- allowComments: this.server.allowComments,
380
- safety: this.server.safety,
381
- maxStatements: this.multiStatements ? null : 1,
382
- });
383
- const list = Array.isArray(results) ? results : [results];
384
- for (const r of list) {
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 !== false;
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 pwd = this.auth[user];
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) return true;
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._getEngine().then(() => {
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().then(() => cb && cb()).catch(() => cb && cb());
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) {
@@ -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 * 160;
15
- let buf = Buffer.allocUnsafe(est);
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 { off = buf.writeUInt8(BOOL_TAG, off); off = buf.writeUInt8(v ? 1 : 0, off); }
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 { off = buf.writeUInt8(BOOL_TAG, off); off = buf.writeUInt8(v ? 1 : 0, off); }
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 { off = buf.writeUInt8(BOOL_TAG, off); off = buf.writeUInt8(v ? 1 : 0, off); }
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 { off = buf.writeUInt8(BOOL_TAG, off); off = buf.writeUInt8(v ? 1 : 0, off); }
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 { off = buf.writeUInt8(BOOL_TAG, off); off = buf.writeUInt8(v ? 1 : 0, off); }
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 { off = buf.writeUInt8(BOOL_TAG, off); off = buf.writeUInt8(v ? 1 : 0, off); }
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
- const r = await this._insertBatch(table, arr);
345
- if (r && r.error) throw new Error(r.error);
346
- this._emit('insert', { table, count: arr.length, ids: r });
347
- this._runHooks('afterInsert', [table, filtered, r]);
348
- return r;
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 };