jsql-neo 4.5.0 → 4.5.1

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/lib/database.js CHANGED
@@ -1142,9 +1142,46 @@ class Database {
1142
1142
  async start() {
1143
1143
  this._runHooks('onStart', []);
1144
1144
  this._emit('start', {});
1145
+ if (this._dirMode && !this._sigInstalled) {
1146
+ this._sigInstalled = true;
1147
+ Database._installSignalHandler(this);
1148
+ }
1145
1149
  return this;
1146
1150
  }
1147
1151
 
1152
+ /**
1153
+ * 进程退出时兜底刷盘:拦截 SIGINT/SIGTERM,同步落盘脏表后再退出。
1154
+ * 使用静态共享 handler,避免多个数据库实例重复注册/互相覆盖。
1155
+ * @private
1156
+ */
1157
+ static _installSignalHandler(db) {
1158
+ if (!Database._signalHandlerInstalled) {
1159
+ Database._signalHandlerInstalled = true;
1160
+ Database._signalDBs = new Set();
1161
+ const handler = (sig) => {
1162
+ for (const d of Database._signalDBs) {
1163
+ try {
1164
+ if (d._dirMode) {
1165
+ if (d._flushTimer) { clearTimeout(d._flushTimer); d._flushTimer = null; }
1166
+ d._flushDirty();
1167
+ d._saveMeta();
1168
+ }
1169
+ } catch (e) { /* 兜底失败不阻塞退出 */ }
1170
+ }
1171
+ process.exit(128 + (sig === 'SIGINT' ? 2 : 15));
1172
+ };
1173
+ process.on('SIGINT', handler);
1174
+ process.on('SIGTERM', handler);
1175
+ Database._signalHandler = handler;
1176
+ }
1177
+ Database._signalDBs.add(db);
1178
+ }
1179
+
1180
+ /** 从进程退出兜底集合移除(stop 时调用) */
1181
+ static _uninstallSignalHandler(db) {
1182
+ if (Database._signalDBs) Database._signalDBs.delete(db);
1183
+ }
1184
+
1148
1185
  insert(tableName, data) {
1149
1186
  const table = this._ensureTable(tableName);
1150
1187
  if (!table) throw createError('ER_NO_SUCH_TABLE', tableName);
@@ -1333,11 +1370,16 @@ class Database {
1333
1370
  this._runHooks('onStop', []);
1334
1371
  this._emit('stop', {});
1335
1372
  if (this._dirMode) {
1373
+ if (this._flushTimer) { clearTimeout(this._flushTimer); this._flushTimer = null; }
1336
1374
  try {
1337
1375
  this._flushDirty();
1338
1376
  this._saveMeta();
1339
1377
  } catch (e) {}
1340
1378
  }
1379
+ if (this._sigInstalled) {
1380
+ this._sigInstalled = false;
1381
+ Database._uninstallSignalHandler(this);
1382
+ }
1341
1383
  return this;
1342
1384
  }
1343
1385
 
@@ -104,8 +104,19 @@ function handshakePacket(connectionId, seed) {
104
104
  return b.build();
105
105
  }
106
106
 
107
- function okPacket(affectedRows = 0, insertId = 0, status = SERVER_STATUS_AUTOCOMMIT) {
108
- const b = new PacketBuilder();
107
+ /**
108
+ * 生成握手 seed:使用可打印 ASCII(33-126),避开 0x00。
109
+ * 老客户端按 C 字符串读取 seed,若含 \0 会提前截断导致握手失败/断连。
110
+ */
111
+ function genSeed(len = 20) {
112
+ const out = Buffer.alloc(len);
113
+ for (let i = 0; i < len; i++) {
114
+ out[i] = 33 + Math.floor(Math.random() * 94);
115
+ }
116
+ return out;
117
+ }
118
+
119
+ function okPacket(affectedRows = 0, insertId = 0, status = SERVER_STATUS_AUTOCOMMIT) { const b = new PacketBuilder();
109
120
  b.byte(0x00);
110
121
  b.raw(encodeLenenc(affectedRows));
111
122
  b.raw(encodeLenenc(insertId));
@@ -364,7 +375,7 @@ class MysqlConnection {
364
375
  this.socket = socket;
365
376
  this.server = server;
366
377
  this.connectionId = ++server._connectionCounter;
367
- this.seed = crypto.randomBytes(20);
378
+ this.seed = genSeed(20);
368
379
  this.buffer = Buffer.alloc(0);
369
380
  this.sequence = 0;
370
381
  this.authenticated = false;
@@ -257,14 +257,23 @@ class JSQL {
257
257
  _runHooks(hookName, args) {
258
258
  const hooks = this._hooks[hookName];
259
259
  if (!hooks || hooks.length === 0) return true;
260
- for (const fn of hooks) {
261
- const r = fn(...args);
262
- if (r === false) return false;
263
- if (r !== undefined && args.length > 0) {
264
- args[0] = r;
260
+ if (this._inHook) {
261
+ // 防止插件 hook 内重入 native 调用导致 N-API 崩溃(段错误)
262
+ throw new Error('ER_PLUGIN_REENTRY: plugin hook "' + hookName + '" re-entered native call; plugin must not call engine methods inside its own hook');
263
+ }
264
+ this._inHook = true;
265
+ try {
266
+ for (const fn of hooks) {
267
+ const r = fn(...args);
268
+ if (r === false) return false;
269
+ if (r !== undefined && args.length > 0) {
270
+ args[0] = r;
271
+ }
265
272
  }
273
+ return true;
274
+ } finally {
275
+ this._inHook = false;
266
276
  }
267
- return true;
268
277
  }
269
278
 
270
279
  async start() {
package/lib/sql.js CHANGED
@@ -284,16 +284,28 @@ class Parser {
284
284
  if (t.value === 'PRIMARY') {
285
285
  this.expectKeyword('PRIMARY'); this.expectKeyword('KEY');
286
286
  this.expect('op', '(');
287
- const pkCol = this.parseTableName();
287
+ const pkCols = [this.parseTableName()];
288
+ while (this.peek().type === 'op' && this.peek().value === ',') {
289
+ this.next();
290
+ pkCols.push(this.parseTableName());
291
+ }
288
292
  this.expect('op', ')');
289
- if (schema[pkCol]) schema[pkCol].primaryKey = true;
293
+ for (const pkCol of pkCols) {
294
+ if (schema[pkCol]) schema[pkCol].primaryKey = true;
295
+ }
290
296
  hasPk = true;
291
297
  } else {
292
298
  this.expectKeyword('UNIQUE');
293
299
  this.expect('op', '(');
294
- const uCol = this.parseTableName();
300
+ const uCols = [this.parseTableName()];
301
+ while (this.peek().type === 'op' && this.peek().value === ',') {
302
+ this.next();
303
+ uCols.push(this.parseTableName());
304
+ }
295
305
  this.expect('op', ')');
296
- if (schema[uCol]) schema[uCol].unique = true;
306
+ for (const uCol of uCols) {
307
+ if (schema[uCol]) schema[uCol].unique = true;
308
+ }
297
309
  }
298
310
  } else if (t.type === 'keyword' && t.value === 'CONSTRAINT') {
299
311
  this.expectKeyword('CONSTRAINT');
@@ -1012,7 +1024,7 @@ class Parser {
1012
1024
  if (t.type === 'sysvar') return { type: 'sysvar', name: t.value };
1013
1025
  if (t.type === 'keyword' && t.value === 'NULL') return { type: 'value', value: null };
1014
1026
  if (t.type === 'keyword' && t.value === 'CASE') return this.parseCase();
1015
- if (t.type === 'keyword' && this.peek().type === 'op' && this.peek().value === '(') {
1027
+ if (t.type === 'keyword' && !['SUM', 'AVG', 'MIN', 'MAX', 'COUNT'].includes(t.value) && this.peek().type === 'op' && this.peek().value === '(') {
1016
1028
  const name = t.value;
1017
1029
  this.next();
1018
1030
  const args = [];
@@ -1608,7 +1620,10 @@ class SQLExecutor {
1608
1620
  if (explicit.length > 0) {
1609
1621
  const all = (await this.engine.find(statement.name, {}, { limit: 1e9, offset: 0 })).map(r => normalizeRow(r, schema));
1610
1622
  const pkMap = new Map();
1611
- for (const row of all) pkMap.set(keyOf(row), row.id);
1623
+ for (const row of all) {
1624
+ const keys = pkCols.map(c => row[c]).filter(v => v !== undefined && v !== null);
1625
+ if (keys.length === pkCols.length) pkMap.set(keyOf(row), keys);
1626
+ }
1612
1627
  const conflicts = [];
1613
1628
  const fresh = [];
1614
1629
  for (const d of dataRows) {
@@ -1626,7 +1641,13 @@ class SQLExecutor {
1626
1641
  for (const { d, existingId } of conflicts) {
1627
1642
  const data = {};
1628
1643
  for (const [col, val] of statement.onDuplicate) data[col] = val;
1629
- this.engine.updateById(statement.name, existingId, data);
1644
+ if (this.engine.updateById && existingId.length === 1) {
1645
+ this.engine.updateById(statement.name, existingId[0], data);
1646
+ } else if (this.engine.update) {
1647
+ const filter = {};
1648
+ pkCols.forEach((c, i) => { filter[c] = existingId[i]; });
1649
+ this.engine.update(statement.name, filter, data);
1650
+ }
1630
1651
  updated++;
1631
1652
  }
1632
1653
  await this.engine.flush();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jsql-neo",
3
- "version": "4.5.0",
3
+ "version": "4.5.1",
4
4
  "description": "JSQL-NEO — Rust-powered embedded database with WASM, REST API, B-Tree indexes, WAL, crash recovery",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",