jsql-neo 6.3.6 → 6.3.10

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
@@ -9,7 +9,23 @@ const path = require('path');
9
9
  const os = require('os');
10
10
  const crypto = require('crypto');
11
11
  const Table = require('./table');
12
- const { createError } = require('./errors');
12
+ const { createError, JSQL_Error, ErrorCodes } = require('./errors');
13
+
14
+ /**
15
+ * 构造带补充说明的错误。
16
+ *
17
+ * 为什么不能直接用 createError:它的消息来自错误码目录的模板,
18
+ * 只把 %s 换成参数 —— ER_NO_TRANSACTION 的模板里没有 %s,
19
+ * 自定义说明会被整段丢弃,用户只能看到一句 "No transaction in progress",
20
+ * 不知道该怎么做。这里沿用同一个 MySQL 错误码(保证兼容),
21
+ * 但在标准消息后面补上有用的提示。
22
+ */
23
+ function errorWithHint(codeKey, hint) {
24
+ const t = ErrorCodes[codeKey];
25
+ if (!t) return new JSQL_Error('UNKNOWN', hint);
26
+ const msg = hint ? `${t.msg}: ${hint}` : t.msg;
27
+ return new JSQL_Error(t.code, msg, { codeKey });
28
+ }
13
29
  const JSQLFormat = require('./jsql_format');
14
30
  const {
15
31
  createHookRegistry, applyPlugin, buildPluginContext, registerHook,
@@ -341,22 +357,27 @@ class Database {
341
357
  _acquireLock() {
342
358
  if (!this._fileLockEnabled) return;
343
359
  try {
344
- // 检查锁文件是否存在
345
- if (fs.existsSync(this._lockPath)) {
346
- const lockTime = fs.statSync(this._lockPath).mtimeMs;
347
- // 锁超过 30 秒视为过期
348
- if (Date.now() - lockTime > 30000) {
349
- fs.unlinkSync(this._lockPath);
350
- } else {
351
- throw createError('ER_LOCK_WAIT_TIMEOUT', 'Database is locked by another process');
352
- }
353
- }
354
- fs.writeFileSync(this._lockPath, String(process.pid), 'utf8');
355
- this._lockFd = true;
360
+ // 原子创建锁文件:'wx' 模式在文件存在时立即抛错,无需 TOCTOU 两步检查
361
+ this._lockFd = fs.openSync(this._lockPath, 'wx');
362
+ fs.writeSync(this._lockFd, String(process.pid), null, 'utf8');
363
+ fs.fsyncSync(this._lockFd);
356
364
  } catch (e) {
357
- if (e.code === 'ER_LOCK_WAIT_TIMEOUT') throw e;
365
+ if (e.code === 'EEXIST') {
366
+ try {
367
+ const lockTime = fs.statSync(this._lockPath).mtimeMs;
368
+ if (Date.now() - lockTime > 30000) {
369
+ fs.unlinkSync(this._lockPath);
370
+ this._lockFd = fs.openSync(this._lockPath, 'wx');
371
+ fs.writeSync(this._lockFd, String(process.pid), null, 'utf8');
372
+ fs.fsyncSync(this._lockFd);
373
+ return;
374
+ }
375
+ } catch (_) { /* ignore */ }
376
+ throw createError('ER_LOCK_WAIT_TIMEOUT', 'Database is locked by another process');
377
+ }
358
378
  // 权限不足等,降级为无锁模式
359
379
  this._fileLockEnabled = false;
380
+ this._lockFd = null;
360
381
  }
361
382
  }
362
383
 
@@ -606,6 +627,14 @@ class Database {
606
627
  }
607
628
  return true;
608
629
 
630
+ case 'renameTable':
631
+ // 重命名也要能回放:否则崩溃恢复后表名会退回去,
632
+ // 而后续的 insert/update 记录全都打在新名字上,全部丢失。
633
+ if (op.from && op.to && this._tables[op.from] && !this._tables[op.to]) {
634
+ this.renameTable(op.from, op.to);
635
+ }
636
+ return true;
637
+
609
638
  case 'insert':
610
639
  case 'update':
611
640
  case 'remove':
@@ -705,6 +734,89 @@ class Database {
705
734
  this._runHooks('afterDropTable', [name]);
706
735
  }
707
736
 
737
+ /**
738
+ * 重命名表。
739
+ *
740
+ * 单独写成引擎方法而不是在 SQL 层拼:表名不只存在 _tables 里 ——
741
+ * _tableNames、db.<表名> 这个取值属性、_versions、_meta.tables(目录模式)
742
+ * 各自都存了一份。漏掉任何一个都会留下一张「半改名」的表:
743
+ * 查得到数据却列不出来,或者反过来。
744
+ */
745
+ renameTable(oldName, newName, { ifExists = false } = {}) {
746
+ if (oldName === newName) {
747
+ if (!this._tables[oldName]) {
748
+ if (ifExists) return false;
749
+ throw createError('ER_NO_SUCH_TABLE', oldName);
750
+ }
751
+ return true; // 改成同名 = 无事发生,不算错
752
+ }
753
+ if (!this._tables[oldName]) {
754
+ if (ifExists) return false;
755
+ throw createError('ER_NO_SUCH_TABLE', oldName);
756
+ }
757
+ if (this._tables[newName] || (this._dirMode && this._meta.tables[newName])) {
758
+ throw createError('ER_TABLE_EXISTS_ERROR', newName);
759
+ }
760
+ if (!newName || newName.length > 64 || newName === '.' || newName === '..' ||
761
+ /[\x00-\x1f/\\]|\.\./.test(newName)) {
762
+ throw createError('ER_BAD_TABLE_NAME', newName);
763
+ }
764
+ if (!this._runHooks('beforeRenameTable', [oldName, newName])) return false;
765
+
766
+ const table = this._tables[oldName];
767
+
768
+ // 目录模式:先把物理文件挪过去。放在改动内存状态之前 ——
769
+ // 文件挪失败就整体放弃,不会留下内存与磁盘不一致的状态。
770
+ if (this._dirMode && this._meta.tables[oldName]) {
771
+ const meta = this._meta.tables[oldName];
772
+ const oldFile = meta.file;
773
+ const newFile = encodeURIComponent(newName) + '.jsql';
774
+ if (oldFile && oldFile !== newFile) {
775
+ const from = path.join(this._filePath, oldFile);
776
+ const to = path.join(this._filePath, newFile);
777
+ if (fs.existsSync(from)) {
778
+ try { fs.renameSync(from, to); }
779
+ catch (e) { throw createError('ER_FILE_NOT_FOUND', `${oldFile} → ${newFile}: ${e.message}`); }
780
+ }
781
+ meta.file = newFile;
782
+ }
783
+ }
784
+
785
+ delete this._tables[oldName];
786
+ table._name = newName;
787
+ this._tables[newName] = table;
788
+
789
+ this._tableNames.delete(oldName);
790
+ this._tableNames.add(newName);
791
+
792
+ delete this[oldName];
793
+ Object.defineProperty(this, newName, {
794
+ get: () => this._ensureTable(newName) || this._tables[newName],
795
+ enumerable: true,
796
+ configurable: true
797
+ });
798
+
799
+ if (this._versioning && this._versions[oldName]) {
800
+ this._versions[newName] = this._versions[oldName];
801
+ delete this._versions[oldName];
802
+ }
803
+ if (this._dirMode && this._meta.tables[oldName]) {
804
+ const meta = this._meta.tables[oldName];
805
+ delete this._meta.tables[oldName];
806
+ this._meta.tables[newName] = meta;
807
+ this._dirtyTables.delete(oldName);
808
+ this._dirtyTables.add(newName);
809
+ this._saveMeta();
810
+ }
811
+
812
+ this._dirtyTables.delete(oldName);
813
+ this._markDirty(newName);
814
+ this._walWrite({ op: 'renameTable', from: oldName, to: newName });
815
+ this._emit('renameTable', { from: oldName, to: newName });
816
+ this._runHooks('afterRenameTable', [oldName, newName]);
817
+ return true;
818
+ }
819
+
708
820
  hasTable(name) {
709
821
  return !!this._tables[name] || (this._dirMode && !!this._meta.tables[name]);
710
822
  }
@@ -1085,6 +1197,83 @@ class Database {
1085
1197
  // 事务(支持隔离级别)
1086
1198
  // ============================================================
1087
1199
 
1200
+ // ============================================================
1201
+ // 事务(支持隔离级别)与保存点
1202
+ // ============================================================
1203
+
1204
+ /**
1205
+ * 抓一份「所有表」的快照。
1206
+ *
1207
+ * 形状与 beginTx 的 _txSnapshot 一致(表名 → {rows, schema}),
1208
+ * 让「事务开始」「保存点」「两种回滚」共用同一套还原逻辑。
1209
+ *
1210
+ * 必须是深拷贝:行里可能含嵌套对象/数组,浅拷贝的话回滚之后
1211
+ * 调用方手里的引用仍指向已被改动的数据。
1212
+ */
1213
+ _captureTxSnapshot() {
1214
+ const snap = {};
1215
+ for (const [name, table] of Object.entries(this._tables)) {
1216
+ snap[name] = {
1217
+ rows: JSON.parse(JSON.stringify(table.toJSON())),
1218
+ schema: table._schema,
1219
+ // 自增计数器也要存:否则回滚掉刚插入的行之后,
1220
+ // 下一次插入会跳过已用过的号(用户看到 id 从 2 开始会困惑)。
1221
+ // 原有 rollback() 一直是这么做的,保存点保持一致。
1222
+ autoIncrement: table._autoIncrement
1223
+ };
1224
+ }
1225
+ return snap;
1226
+ }
1227
+
1228
+ /**
1229
+ * 把数据还原到某个快照。
1230
+ *
1231
+ * 还原时**再深拷贝一次**是关键:同一个保存点可以被 ROLLBACK TO 多次,
1232
+ * 若把快照里的数组直接交给表,后续写入会改坏快照本身,
1233
+ * 第二次回滚拿到的就不是最初的状态了。
1234
+ *
1235
+ * 快照里没有的表要删掉 —— 否则「保存点之后才建的表」会残留,
1236
+ * 回滚完却多出一张当时并不存在的表。
1237
+ */
1238
+ _restoreTxSnapshot(snap) {
1239
+ if (!snap) return;
1240
+ for (const name of Object.keys(this._tables)) {
1241
+ if (!snap[name]) delete this._tables[name];
1242
+ }
1243
+ for (const [name, entry] of Object.entries(snap)) {
1244
+ const rows = JSON.parse(JSON.stringify(entry.rows));
1245
+ let table = this._tables[name];
1246
+ if (!table) {
1247
+ table = new Table(name, entry.schema, this);
1248
+ this._tables[name] = table;
1249
+ }
1250
+ table._loadRows(rows);
1251
+ // _loadRows 只会把计数器往上抬(取现有行的最大值),
1252
+ // 所以「回滚掉刚插入的行」之后还得显式退回去。
1253
+ if (entry.autoIncrement !== undefined) {
1254
+ table._autoIncrement = entry.autoIncrement;
1255
+ }
1256
+ }
1257
+ this._dirty = true;
1258
+ }
1259
+
1260
+ /**
1261
+ * 是否有进行中的事务。
1262
+ *
1263
+ * 引擎里有两条事务实现路径:begin/commit/rollback 与
1264
+ * beginTx/commitTx/rollbackTx。SQL 层走后者(因为 Database.beginTx 存在),
1265
+ * 直接调 API 走前者 —— 保存点必须对两条都有效,所以这里两条都算。
1266
+ */
1267
+ hasActiveTx() {
1268
+ return (this._txId !== undefined && !!this._txSnapshot) || !!this._transaction;
1269
+ }
1270
+
1271
+ /** 保存点栈(懒建),随事务结束清空 */
1272
+ _savepointStackRef() {
1273
+ if (!this._savepointStack) this._savepointStack = [];
1274
+ return this._savepointStack;
1275
+ }
1276
+
1088
1277
  /**
1089
1278
  * 开始事务
1090
1279
  * @param {string} isolationLevel - 覆盖默认隔离级别(可选)
@@ -1095,7 +1284,7 @@ class Database {
1095
1284
  const level = isolationLevel || this._isolationLevel;
1096
1285
 
1097
1286
  this._transaction = {
1098
- rows: {},
1287
+ snapshot: null,
1099
1288
  autoIncrements: {},
1100
1289
  level,
1101
1290
  startedAt: Date.now()
@@ -1103,10 +1292,7 @@ class Database {
1103
1292
 
1104
1293
  if (level === 'REPEATABLE_READ') {
1105
1294
  // 快照:保存当前所有数据(深拷贝,保证嵌套对象可回滚)
1106
- for (const [name, table] of Object.entries(this._tables)) {
1107
- this._transaction.rows[name] = JSON.parse(JSON.stringify(table._rows));
1108
- this._transaction.autoIncrements[name] = table._autoIncrement;
1109
- }
1295
+ this._transaction.snapshot = this._captureTxSnapshot();
1110
1296
  } else {
1111
1297
  // READ_COMMITTED:只保存 autoIncrement
1112
1298
  for (const [name, table] of Object.entries(this._tables)) {
@@ -1114,35 +1300,120 @@ class Database {
1114
1300
  }
1115
1301
  }
1116
1302
 
1303
+ this._savepointStack = [];
1117
1304
  this._walWrite({ op: 'begin', level });
1118
1305
  }
1119
1306
 
1120
1307
  commit() {
1121
1308
  if (!this._transaction) throw createError('ER_NO_TRANSACTION');
1122
1309
  this._walWrite({ op: 'commit', at: this._transaction.startedAt });
1310
+ // 提交即释放全部保存点 —— 它们随事务一起结束
1123
1311
  this._transaction = null;
1124
- this._checkpoint();
1312
+ this._savepointStack = [];
1313
+ // 不在此处调用 _checkpoint():_checkpoint() 清空 WAL,但数据可能还未保存。
1314
+ // save() 内部会在保存完成后自动调用 _checkpoint()。
1125
1315
  this._markDirtyLegacy();
1126
1316
  }
1127
1317
 
1128
1318
  rollback() {
1129
1319
  if (!this._transaction) throw createError('ER_NO_TRANSACTION');
1130
1320
 
1131
- for (const [name, table] of Object.entries(this._tables)) {
1132
- if (this._transaction.rows[name]) {
1133
- table._rows = this._transaction.rows[name];
1134
- table._autoIncrement = this._transaction.autoIncrements[name];
1135
- table._rebuildAllBTrees();
1136
- for (const field of Object.keys(table._indexes)) {
1137
- table.createIndex(field);
1138
- }
1139
- } else if (this._transaction.autoIncrements[name] !== undefined) {
1140
- table._autoIncrement = this._transaction.autoIncrements[name];
1321
+ if (this._transaction.snapshot) {
1322
+ this._restoreTxSnapshot(this._transaction.snapshot);
1323
+ } else {
1324
+ // READ_COMMITTED:没有行快照,只能退回自增值
1325
+ for (const [name, table] of Object.entries(this._tables)) {
1326
+ const v = this._transaction.autoIncrements[name];
1327
+ if (v !== undefined) table._autoIncrement = v;
1141
1328
  }
1142
1329
  }
1143
1330
 
1144
1331
  this._walWrite({ op: 'rollback', at: this._transaction.startedAt });
1145
1332
  this._transaction = null;
1333
+ this._savepointStack = [];
1334
+ this._markDirtyLegacy();
1335
+ }
1336
+
1337
+ // ============================================================
1338
+ // 保存点(SAVEPOINT / ROLLBACK TO / RELEASE)
1339
+ // ============================================================
1340
+
1341
+ /**
1342
+ * 建立一个保存点。
1343
+ *
1344
+ * 语义遵循 SQL 标准:
1345
+ * - 只在事务内有效,COMMIT / ROLLBACK 时全部释放
1346
+ * - 同名保存点覆盖旧的(Postgres 行为),它之后的保存点一并作废
1347
+ * - ROLLBACK TO 之后该保存点**仍然存在**,可以再回滚一次
1348
+ *
1349
+ * 保存点一律做全量行快照,不看隔离级别 —— 保存点的全部意义就是精确还原,
1350
+ * READ_COMMITTED 下若只记自增值,「回滚成功」就是假的。
1351
+ */
1352
+ savepoint(name) {
1353
+ if (!this.hasActiveTx()) {
1354
+ throw errorWithHint('ER_NO_TRANSACTION',
1355
+ `SAVEPOINT '${name}' requires an active transaction (run BEGIN first)`);
1356
+ }
1357
+ const sp = String(name == null ? '' : name).trim();
1358
+ if (!sp) throw errorWithHint('ER_PARSE_ERROR', 'Savepoint name cannot be empty');
1359
+
1360
+ const list = this._savepointStackRef();
1361
+ // 同名覆盖:丢掉它以及它之后的所有保存点
1362
+ const dup = list.findIndex(s => s.name.toLowerCase() === sp.toLowerCase());
1363
+ if (dup >= 0) list.length = dup;
1364
+
1365
+ list.push({ name: sp, snap: this._captureTxSnapshot(), at: Date.now() });
1366
+ return list.length;
1367
+ }
1368
+
1369
+ /**
1370
+ * 回滚到某个保存点,事务本身继续有效。
1371
+ * 该保存点保留(可再次回滚到它),它之后建立的保存点全部丢弃。
1372
+ */
1373
+ rollbackTo(name) {
1374
+ const list = this._requireSavepoints(name, 'ROLLBACK TO SAVEPOINT');
1375
+ const idx = this._findSavepoint(list, name);
1376
+ this._restoreTxSnapshot(list[idx].snap);
1377
+ list.length = idx + 1;
1378
+ this._walWrite({ op: 'rollbackTo', name });
1379
+ this._markDirtyLegacy();
1380
+ return list.length;
1381
+ }
1382
+
1383
+ /** 释放保存点:它和它之后的都作废,但不还原数据 */
1384
+ releaseSavepoint(name) {
1385
+ const list = this._requireSavepoints(name, 'RELEASE SAVEPOINT');
1386
+ const idx = this._findSavepoint(list, name);
1387
+ list.length = idx;
1388
+ return list.length;
1389
+ }
1390
+
1391
+ /** 当前事务里的保存点名(由外到内) */
1392
+ listSavepoints() {
1393
+ if (!this.hasActiveTx()) return [];
1394
+ return this._savepointStackRef().map(s => s.name);
1395
+ }
1396
+
1397
+ _requireSavepoints(name, op) {
1398
+ if (!this.hasActiveTx()) {
1399
+ throw errorWithHint('ER_NO_TRANSACTION',
1400
+ `${op} '${name}' requires an active transaction (run BEGIN first)`);
1401
+ }
1402
+ return this._savepointStackRef();
1403
+ }
1404
+
1405
+ _findSavepoint(list, name) {
1406
+ const sp = String(name == null ? '' : name).toLowerCase();
1407
+ const idx = list.findIndex(s => s.name.toLowerCase() === sp);
1408
+ if (idx < 0) {
1409
+ const known = list.map(s => s.name).join(', ') || '(none)';
1410
+ // 用 MySQL 的错误码 1305(SAVEPOINT xxx does not exist),
1411
+ // 后面补上已定义的保存点,省得用户回头翻代码
1412
+ const base = ErrorCodes.ER_SP_DOES_NOT_EXIST.msg.replace('%s', String(name));
1413
+ throw new JSQL_Error(ErrorCodes.ER_SP_DOES_NOT_EXIST.code,
1414
+ `${base} (defined savepoints: ${known})`, { codeKey: 'ER_SP_DOES_NOT_EXIST' });
1415
+ }
1416
+ return idx;
1146
1417
  }
1147
1418
 
1148
1419
  inTransaction() {
@@ -1385,6 +1656,13 @@ class Database {
1385
1656
  if (Database._signalDBs) Database._signalDBs.delete(db);
1386
1657
  }
1387
1658
 
1659
+ /** 公开别名 */
1660
+ static removeSignalHandler(db) {
1661
+ if (Database._signalDBs) {
1662
+ Database._signalDBs.delete(db);
1663
+ }
1664
+ }
1665
+
1388
1666
  insert(tableName, data) {
1389
1667
  const table = this._ensureTable(tableName);
1390
1668
  if (!table) throw createError('ER_NO_SUCH_TABLE', tableName);
@@ -1435,6 +1713,8 @@ class Database {
1435
1713
  let rows = table._applyFilterOptimized(table._rows, filter || {});
1436
1714
  if (limit > 0) rows = rows.slice(offset, offset + limit);
1437
1715
  else if (offset > 0) rows = rows.slice(offset);
1716
+ // P1-11: 返回浅拷贝,防止调用方修改影响内部数据
1717
+ rows = rows.map(r => ({ ...r }));
1438
1718
  this._touchTable(tableName);
1439
1719
  this._runHooks('afterFind', [tableName, { filter, opts }, rows]);
1440
1720
  return rows;
@@ -1464,16 +1744,19 @@ class Database {
1464
1744
  if (!table) throw createError('ER_NO_SUCH_TABLE', tableName);
1465
1745
  if (!this._runHooks('beforeFind', [tableName, { id }])) return null;
1466
1746
  const row = this._resolveId(table, id);
1747
+ // P1-11: 返回浅拷贝,防止调用方修改影响内部数据
1748
+ const result = row ? { ...row } : null;
1467
1749
  this._touchTable(tableName);
1468
- this._runHooks('afterFind', [tableName, { id }, row]);
1469
- return row;
1750
+ this._runHooks('afterFind', [tableName, { id }, result]);
1751
+ return result;
1470
1752
  }
1471
1753
 
1472
1754
  findByIds(tableName, ids) {
1473
1755
  const table = this._ensureTable(tableName);
1474
1756
  if (!table) throw createError('ER_NO_SUCH_TABLE', tableName);
1475
1757
  if (!this._runHooks('beforeFind', [tableName, { ids }])) return null;
1476
- const rows = ids.map(id => this._resolveId(table, id)).filter(r => r !== null);
1758
+ // P1-11: 返回浅拷贝
1759
+ const rows = ids.map(id => this._resolveId(table, id)).filter(r => r !== null).map(r => ({ ...r }));
1477
1760
  this._touchTable(tableName);
1478
1761
  this._runHooks('afterFind', [tableName, { ids }, rows]);
1479
1762
  return rows;
@@ -1497,6 +1780,8 @@ class Database {
1497
1780
  const table = this._ensureTable(tableName);
1498
1781
  if (!table) throw createError('ER_NO_SUCH_TABLE', tableName);
1499
1782
  if (!this._runHooks('beforeUpdate', [tableName, id, data])) return;
1783
+ // P1-9: updateById 绕过类型校验——先校验再更新
1784
+ const validated = table._validateDataTypes({ ...data }, true);
1500
1785
  const row = this._resolveId(table, id);
1501
1786
  if (!row) return { ok: false, error: 'not found' };
1502
1787
  // 改主键/唯一列时要先确认新值不与其它行冲突。
@@ -1509,16 +1794,16 @@ class Database {
1509
1794
  const pk = this._pkOf(table);
1510
1795
  const uniq = (table._cachedUniqueFields || []).filter(f => f !== pk);
1511
1796
  for (const field of [pk, ...uniq]) {
1512
- if (!field || !Object.prototype.hasOwnProperty.call(data, field)) continue;
1513
- const nv = data[field];
1797
+ if (!field || !Object.prototype.hasOwnProperty.call(validated, field)) continue;
1798
+ const nv = validated[field];
1514
1799
  if (nv === row[field]) continue; // 值没变,不算冲突
1515
1800
  const clash = table._rows.find(r => r !== row && r[field] === nv);
1516
1801
  if (clash) throw createError('ER_DUP_ENTRY', `Duplicate entry '${nv}' for key '${field}'`);
1517
1802
  }
1518
- Object.assign(row, data);
1519
- if (pk) this._walWrite({ op: 'update', table: tableName, pk, id, row: { ...data } });
1520
- this._emit('update', { table: tableName, id, data });
1521
- this._runHooks('afterUpdate', [tableName, id, data, { ok: true }]);
1803
+ Object.assign(row, validated);
1804
+ if (pk) this._walWrite({ op: 'update', table: tableName, pk, id, row: { ...validated } });
1805
+ this._emit('update', { table: tableName, id, data: validated });
1806
+ this._runHooks('afterUpdate', [tableName, id, validated, { ok: true }]);
1522
1807
  this._markDirty(tableName);
1523
1808
  return { ok: true };
1524
1809
  }
@@ -1627,6 +1912,10 @@ class Database {
1627
1912
  const arr = Array.isArray(items) ? items : [items];
1628
1913
  if (!this._runHooks('beforeInsert', [tableName, arr])) return [];
1629
1914
  const inserted = table.insertMany(arr);
1915
+ // P1-6: insertMany 不写 WAL——崩溃恢复时丢失这些操作
1916
+ if (table._primaryKey && inserted.length > 0) {
1917
+ this._walWrite({ op: 'insert', table: tableName, rows: inserted });
1918
+ }
1630
1919
  this._emit('insert', { table: tableName, count: arr.length });
1631
1920
  this._runHooks('afterInsert', [tableName, arr, inserted]);
1632
1921
  this._markDirty(tableName);
@@ -1638,6 +1927,14 @@ class Database {
1638
1927
  if (!table) throw createError('ER_NO_SUCH_TABLE', tableName);
1639
1928
  if (!this._runHooks('beforeUpdate', [tableName, query, updates])) return 0;
1640
1929
  const count = table.update(query, updates);
1930
+ // P1-6: update 不写 WAL——崩溃恢复时丢失这些操作
1931
+ if (table._primaryKey && count > 0) {
1932
+ const updatedRows = table.find(query);
1933
+ for (const row of updatedRows) {
1934
+ const id = row[table._primaryKey];
1935
+ if (id !== undefined) this._walWrite({ op: 'update', table: tableName, pk: table._primaryKey, id, row: { ...row } });
1936
+ }
1937
+ }
1641
1938
  this._emit('update', { table: tableName, query, count });
1642
1939
  this._runHooks('afterUpdate', [tableName, query, updates, count]);
1643
1940
  this._markDirty(tableName);
@@ -1648,6 +1945,14 @@ class Database {
1648
1945
  const table = this._ensureTable(tableName);
1649
1946
  if (!table) throw createError('ER_NO_SUCH_TABLE', tableName);
1650
1947
  if (!this._runHooks('beforeDelete', [tableName, query])) return 0;
1948
+ // P1-6: delete 不写 WAL——崩溃恢复时丢失这些操作
1949
+ if (table._primaryKey) {
1950
+ const toDelete = table.find(query);
1951
+ for (const row of toDelete) {
1952
+ const id = row[table._primaryKey];
1953
+ if (id !== undefined) this._walWrite({ op: 'remove', table: tableName, pk: table._primaryKey, id });
1954
+ }
1955
+ }
1651
1956
  const count = table.remove(query);
1652
1957
  this._emit('delete', { table: tableName, query, count });
1653
1958
  this._runHooks('afterDelete', [tableName, query, count]);
@@ -1658,6 +1963,14 @@ class Database {
1658
1963
  truncate(tableName) {
1659
1964
  const table = this._ensureTable(tableName);
1660
1965
  if (!table) throw createError('ER_NO_SUCH_TABLE', tableName);
1966
+ // P1-6: truncate 不写 WAL——崩溃恢复时丢失这些操作
1967
+ if (table._primaryKey) {
1968
+ const toDelete = table.find({});
1969
+ for (const row of toDelete) {
1970
+ const id = row[table._primaryKey];
1971
+ if (id !== undefined) this._walWrite({ op: 'remove', table: tableName, pk: table._primaryKey, id });
1972
+ }
1973
+ }
1661
1974
  const count = table.truncate();
1662
1975
  this._emit('delete', { table: tableName, count });
1663
1976
  this._markDirty(tableName);
@@ -1834,6 +2147,12 @@ class Database {
1834
2147
  return;
1835
2148
  }
1836
2149
 
2150
+ // P1-10: WAL-only 恢复 —— 主库文件不存在时跳过读取,直接恢复 WAL
2151
+ if (!fs.existsSync(this._filePath)) {
2152
+ this._recoverFromWAL();
2153
+ return;
2154
+ }
2155
+
1837
2156
  const json = this._readFile(this._filePath);
1838
2157
  const data = JSON.parse(json);
1839
2158
 
@@ -1892,6 +2211,11 @@ class Database {
1892
2211
  }
1893
2212
  if (this._jsqlFormat) this._jsqlFormat._close();
1894
2213
  this._releaseLock();
2214
+ // Bug 3: 清理静态信号处理器,防止关闭后还在响应信号
2215
+ if (this._sigInstalled) {
2216
+ this._sigInstalled = false;
2217
+ Database.removeSignalHandler(this);
2218
+ }
1895
2219
  this._tables = {};
1896
2220
  }
1897
2221
 
@@ -1915,10 +2239,10 @@ class Database {
1915
2239
  }
1916
2240
 
1917
2241
  async beginTx() {
1918
- this._txSnapshot = {};
1919
- for (const [name, table] of Object.entries(this._tables)) {
1920
- this._txSnapshot[name] = { rows: JSON.parse(JSON.stringify(table.toJSON())), schema: table._schema };
1921
- }
2242
+ this._savepointStack = [];
2243
+ // 与保存点共用同一套快照函数,两边形状必须一致,
2244
+ // 否则 rollbackTx 还原不了保存点存下来的快照(反之亦然)。
2245
+ this._txSnapshot = this._captureTxSnapshot();
1922
2246
  this._txId = (this._txId || 0) + 1;
1923
2247
  return this._txId;
1924
2248
  }
@@ -1926,6 +2250,7 @@ class Database {
1926
2250
  async commitTx() {
1927
2251
  this._txSnapshot = null;
1928
2252
  this._txId = undefined;
2253
+ this._savepointStack = []; // 提交即释放全部保存点
1929
2254
  if (this._dirMode) {
1930
2255
  try { this._flushDirty(); } catch (e) {}
1931
2256
  } else if (!this._memoryMode) {
@@ -1937,20 +2262,9 @@ class Database {
1937
2262
  const snap = this._txSnapshot;
1938
2263
  this._txSnapshot = null;
1939
2264
  this._txId = undefined;
2265
+ this._savepointStack = [];
1940
2266
  if (!snap) return;
1941
- for (const name of Object.keys(this._tables)) {
1942
- if (!snap[name]) delete this._tables[name];
1943
- }
1944
- for (const [name, entry] of Object.entries(snap)) {
1945
- if (this._tables[name]) {
1946
- this._tables[name]._loadRows(entry.rows);
1947
- } else {
1948
- const table = new Table(name, entry.schema, this);
1949
- table._loadRows(entry.rows);
1950
- this._tables[name] = table;
1951
- }
1952
- }
1953
- this._dirty = true;
2267
+ this._restoreTxSnapshot(snap);
1954
2268
  }
1955
2269
 
1956
2270
  // ============================================================