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/README.md +78 -8239
- package/index.js +1 -1
- package/lib/cache.js +33 -27
- package/lib/client.js +11 -2
- package/lib/database.js +370 -56
- package/lib/date-types.js +51 -13
- package/lib/errors.js +4 -1
- package/lib/jsql_format.js +30 -21
- package/lib/mod.js +27 -6
- package/lib/mongo_server.js +143 -26
- package/lib/mysql_compat.js +57 -11
- package/lib/mysql_server.js +104 -4
- package/lib/nedb_compat.js +9 -6
- package/lib/pg_server.js +4 -11
- package/lib/plugin.js +20 -2
- package/lib/prepared.js +10 -0
- package/lib/query.js +9 -2
- package/lib/redis_server.js +88 -16
- package/lib/sql.js +856 -81
- package/lib/sqlite_compat.js +2 -1
- package/lib/sqlite_worker.js +8 -2
- package/lib/table.js +68 -26
- package/lib/tui.js +1 -1
- package/lib/web_ui.js +24 -8
- package/native/jsql-neo-native.node +0 -0
- package/package.json +10 -3
- package/test/alter-table.test.js +289 -0
- package/test/identifier-keywords.test.js +140 -0
- package/test/index-ddl.test.js +203 -0
- package/test/mongo-group.test.js +105 -0
- package/test/null-semantics.test.js +126 -0
- package/test/recursive-cte.test.js +123 -0
- package/test/savepoint.test.js +388 -0
- package/test/sql-parser.test.js +30 -5
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
|
-
|
|
346
|
-
|
|
347
|
-
|
|
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 === '
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
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 },
|
|
1469
|
-
return
|
|
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
|
-
|
|
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(
|
|
1513
|
-
const nv =
|
|
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,
|
|
1519
|
-
if (pk) this._walWrite({ op: 'update', table: tableName, pk, id, row: { ...
|
|
1520
|
-
this._emit('update', { table: tableName, id, data });
|
|
1521
|
-
this._runHooks('afterUpdate', [tableName, id,
|
|
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.
|
|
1919
|
-
|
|
1920
|
-
|
|
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
|
-
|
|
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
|
// ============================================================
|