jsql-neo 6.5.20 → 6.5.22
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 +1 -1
- package/bin/jsql +9 -2
- package/bin/yaggs-fallback.js +108 -0
- package/index.js +1 -1
- package/lib/database.js +17 -1
- package/lib/mysql_server.js +22 -8
- package/lib/native_client.js +38 -8
- package/lib/pg_server.js +5 -1
- package/lib/query.js +7 -4
- package/lib/sql.js +202 -47
- package/lib/table.js +13 -5
- package/lib/tui.js +28 -6
- package/lib/wasm_client.js +62 -10
- package/lib/xql/src/compiler.js +129 -25
- package/lib/xql/src/tokenizer.js +10 -2
- package/native/jsql-neo-native.node +0 -0
- package/nativesrc/jsql-neo-core/src/engine/hybrid.rs +14 -3
- package/nativesrc/jsql-neo-core/src/engine/table.rs +64 -4
- package/nativesrc/jsql-neo-wasm/src/lib.rs +39 -14
- package/package.json +7 -5
- package/postinstall.js +42 -18
- package/test/cli-fallback.test.js +55 -0
- package/test/issue-34-35-36.test.js +142 -0
- package/test/issue-39.test.js +160 -0
- package/test/issue-40.test.js +264 -0
- package/test/issue-g4.test.js +82 -0
- package/test/native-add-column.test.js +74 -0
- package/test/sql-parser.test.js +9 -1
- package/wasm/browser.mjs +19 -1
- package/wasm/browser_bg.mjs +150 -97
- package/wasm/jsql_neo_wasm.d.ts +5 -0
- package/wasm/jsql_neo_wasm.js +28 -0
- package/wasm/jsql_neo_wasm_bg.wasm +0 -0
- package/wasm/jsql_neo_wasm_bg.wasm.d.ts +1 -0
- package/nativesrc/jsql-neo-native/jsql-neo-native.node +0 -0
package/README.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
> **One engine to rule them all** — a Rust-powered embedded database that speaks your language:
|
|
4
4
|
> MySQL, PostgreSQL, MongoDB, Redis, SQL, TypeScript, **and the browser**.
|
|
5
5
|
|
|
6
|
-
> **v6.5.
|
|
6
|
+
> **v6.5.22** — stable release · [github.com/vexify-org/JSQL-neo](https://github.com/vexify-org/JSQL-neo) · [jsql.vexify.top](https://jsql.vexify.top)
|
|
7
7
|
|
|
8
8
|

|
|
9
9
|

|
package/bin/jsql
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
const path = require('path');
|
|
3
3
|
const fs = require('fs');
|
|
4
|
-
|
|
4
|
+
// @vexify-org/yaggs 是可选依赖:缺失时(离线安装/私有源不可达/lockfile 漂移)
|
|
5
|
+
// 用内置 shim 兜底,保证 --help/--version 及核心命令仍能启动(#24)。
|
|
6
|
+
let yaggs;
|
|
7
|
+
try { yaggs = require('@vexify-org/yaggs'); }
|
|
8
|
+
catch (e) { yaggs = null; }
|
|
5
9
|
const { ModuleManager } = require('../lib/mod');
|
|
6
10
|
const Database = require('../lib/database');
|
|
7
11
|
const migrate = require('../lib/migrate');
|
|
@@ -29,7 +33,10 @@ const manager = new ModuleManager();
|
|
|
29
33
|
|
|
30
34
|
const RAW_ARGV = process.argv.slice(2);
|
|
31
35
|
|
|
32
|
-
const
|
|
36
|
+
const _pkg = require('../package.json');
|
|
37
|
+
const cli = (yaggs || require('./yaggs-fallback').buildYaggsFallback(_pkg))({
|
|
38
|
+
pkg: _pkg,
|
|
39
|
+
})
|
|
33
40
|
.usage('jsql <command> [options]')
|
|
34
41
|
.option('help', { alias: ['h'], type: 'boolean', description: 'Show help' })
|
|
35
42
|
.command('mod', 'Module management', (sub) => {
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Minimal @vexify-org/yaggs-compatible CLI shim.
|
|
5
|
+
*
|
|
6
|
+
* Used when the optional dependency @vexify-org/yaggs is not installed
|
|
7
|
+
* (offline install, private registry unreachable, lockfile drift, …).
|
|
8
|
+
* Without this shim, bin/jsql crashed on every invocation — even --help /
|
|
9
|
+
* --version — because the top-level `require('@vexify-org/yaggs')` threw.
|
|
10
|
+
*
|
|
11
|
+
* The shim mirrors the fluent API surface bin/jsql relies on:
|
|
12
|
+
* .usage(str).option(name, cfg).command(name, desc, builder, handler)
|
|
13
|
+
* .middleware(fn).run(argv)
|
|
14
|
+
*
|
|
15
|
+
* It captures every command handler registered via the fluent chain and
|
|
16
|
+
* dispatches to it with a minimal `--key value` / `--flag` argument parse.
|
|
17
|
+
* This keeps --help/--version and the core commands (serve, export, import,
|
|
18
|
+
* mod, plugin, …) working even when the optional dependency is absent.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
function buildYaggsFallback(pkg) {
|
|
22
|
+
// 与 @vexify-org/yaggs 的调用形态一致:buildYaggsFallback(pkg)({ pkg }) 返回 CLI 对象。
|
|
23
|
+
return function makeCli(opts) {
|
|
24
|
+
const realPkg = (opts && opts.pkg) || pkg;
|
|
25
|
+
const commands = new Map();
|
|
26
|
+
let usageStr = '';
|
|
27
|
+
const self = {
|
|
28
|
+
usage(u) { usageStr = u; return self; },
|
|
29
|
+
option() { return self; },
|
|
30
|
+
positional() { return self; },
|
|
31
|
+
middleware() { return self; },
|
|
32
|
+
command(name, description, builder, handler) {
|
|
33
|
+
// 兼容两种签名:command(name, descObj) 或 command(name, desc, builder, handler)
|
|
34
|
+
let h = handler;
|
|
35
|
+
let desc = description;
|
|
36
|
+
if (description && typeof description === 'object') {
|
|
37
|
+
desc = description.description;
|
|
38
|
+
if (description.handler) h = description.handler;
|
|
39
|
+
}
|
|
40
|
+
commands.set(name, { desc: desc || '', handler: h, builder: builder || (() => {}) });
|
|
41
|
+
return self;
|
|
42
|
+
},
|
|
43
|
+
async run(argv) {
|
|
44
|
+
const args = (argv || []).slice();
|
|
45
|
+
const first = args[0];
|
|
46
|
+
|
|
47
|
+
if (!first || first === '--help' || first === '-h' || first === 'help') {
|
|
48
|
+
console.log(usageStr || `jsql ${realPkg.version} — embedded SQL CLI`);
|
|
49
|
+
console.log('Usage: jsql <command> [options]');
|
|
50
|
+
if (commands.size) {
|
|
51
|
+
console.log('');
|
|
52
|
+
console.log('Commands:');
|
|
53
|
+
for (const [name, c] of commands) {
|
|
54
|
+
console.log(` ${name.padEnd(12)} ${c.desc}`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
console.log('');
|
|
58
|
+
console.log('Note: optional dependency @vexify-org/yaggs is not installed; full argument parsing is unavailable.');
|
|
59
|
+
console.log(' Install it with `npm install @vexify-org/yaggs` to restore rich argument parsing.');
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (first === '--version' || first === '-v' || first === 'version') {
|
|
64
|
+
console.log(realPkg.version);
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const cmd = commands.get(first);
|
|
69
|
+
if (!cmd) {
|
|
70
|
+
console.error(`Unknown command: ${first}`);
|
|
71
|
+
console.error('Run `jsql --help` for the list of commands.');
|
|
72
|
+
process.exitCode = 1;
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// 极简参数解析:--key value / --key=value / --flag,以及位置参数。
|
|
77
|
+
const parsed = { _: [] };
|
|
78
|
+
for (let i = 1; i < args.length; i++) {
|
|
79
|
+
const a = args[i];
|
|
80
|
+
if (a.startsWith('--')) {
|
|
81
|
+
const eq = a.indexOf('=');
|
|
82
|
+
if (eq !== -1) {
|
|
83
|
+
parsed[a.slice(2, eq)] = a.slice(eq + 1);
|
|
84
|
+
} else {
|
|
85
|
+
const nxt = args[i + 1];
|
|
86
|
+
if (nxt !== undefined && !nxt.startsWith('-')) { parsed[a.slice(2)] = nxt; i++; }
|
|
87
|
+
else parsed[a.slice(2)] = true;
|
|
88
|
+
}
|
|
89
|
+
} else if (a.startsWith('-') && a.length > 1) {
|
|
90
|
+
const key = a.slice(1);
|
|
91
|
+
const nxt = args[i + 1];
|
|
92
|
+
if (nxt !== undefined && !nxt.startsWith('-')) { parsed[key] = nxt; i++; }
|
|
93
|
+
else parsed[key] = true;
|
|
94
|
+
} else {
|
|
95
|
+
parsed._.push(a);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (cmd.handler) return cmd.handler(parsed);
|
|
100
|
+
console.error(`Command '${first}' has no handler.`);
|
|
101
|
+
process.exitCode = 1;
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
return self;
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
module.exports = { buildYaggsFallback };
|
package/index.js
CHANGED
package/lib/database.js
CHANGED
|
@@ -1948,7 +1948,23 @@ class Database {
|
|
|
1948
1948
|
const clash = table._rows.find(r => r !== row && r[field] === nv);
|
|
1949
1949
|
if (clash) throw createError('ER_DUP_ENTRY', `Duplicate entry '${nv}' for key '${field}'`);
|
|
1950
1950
|
}
|
|
1951
|
-
|
|
1951
|
+
// 外键:改主键时按 ON UPDATE 传播到引用它的子表;改自身外键列时校验引用存在。
|
|
1952
|
+
// 此前这里直接 Object.assign 绕过 Table.updateById,ON UPDATE CASCADE / RESTRICT
|
|
1953
|
+
// 与 FK 校验全部失效(外键形同虚设)。
|
|
1954
|
+
if (pk && Object.prototype.hasOwnProperty.call(validated, pk) && validated[pk] !== row[pk]) {
|
|
1955
|
+
table._propagatePKUpdate(row[pk], validated[pk]);
|
|
1956
|
+
}
|
|
1957
|
+
if (table._foreignKeys && table._foreignKeys.length) {
|
|
1958
|
+
table._validateForeignKeys({ ...row, ...validated });
|
|
1959
|
+
}
|
|
1960
|
+
// 改主键/唯一值时索引必须同步:否则 _pkIndex 还指向旧值,
|
|
1961
|
+
// 后续 FK 校验 findOne({id:新值}) 查不到刚更新的父行。
|
|
1962
|
+
{
|
|
1963
|
+
const rowIndex = table._rows.indexOf(row);
|
|
1964
|
+
if (rowIndex !== -1) table._removeFromIndexes(rowIndex, row);
|
|
1965
|
+
Object.assign(row, validated);
|
|
1966
|
+
if (rowIndex !== -1) table._addToIndexes(rowIndex, row);
|
|
1967
|
+
}
|
|
1952
1968
|
if (pk) this._walWrite({ op: 'update', table: tableName, pk, id, row: { ...validated } });
|
|
1953
1969
|
this._emit('update', { table: tableName, id, data: validated });
|
|
1954
1970
|
await this._runHooks('afterUpdate', [tableName, id, validated, { ok: true }]);
|
package/lib/mysql_server.js
CHANGED
|
@@ -281,7 +281,8 @@ function columnDefinition(column) {
|
|
|
281
281
|
|
|
282
282
|
function columnTypeFromSchema(def) {
|
|
283
283
|
const type = def && def.type ? String(def.type).toLowerCase() : 'string';
|
|
284
|
-
if (type === 'integer' || type === 'int'
|
|
284
|
+
if (type === 'integer' || type === 'int') return MYSQL_TYPE_LONG;
|
|
285
|
+
if (type === 'bigint') return MYSQL_TYPE_LONGLONG;
|
|
285
286
|
if (type === 'number' || type === 'float' || type === 'double' || type === 'real') return MYSQL_TYPE_DOUBLE;
|
|
286
287
|
if (type === 'boolean' || type === 'bool') return MYSQL_TYPE_TINY;
|
|
287
288
|
if (type === 'date') return MYSQL_TYPE_DATE;
|
|
@@ -394,15 +395,26 @@ function binaryResultSetPacket(result, tableSchema, baseSeq) {
|
|
|
394
395
|
if (v === null || v === undefined) return null;
|
|
395
396
|
if (type === MYSQL_TYPE_LONG || type === MYSQL_TYPE_LONGLONG || type === MYSQL_TYPE_DOUBLE) {
|
|
396
397
|
const num = Number(v);
|
|
398
|
+
if (!Number.isFinite(num)) return encodeLenencString(String(v));
|
|
397
399
|
if (type === MYSQL_TYPE_DOUBLE) {
|
|
398
|
-
if (!Number.isFinite(num)) return encodeLenencString(String(v));
|
|
399
400
|
const b = Buffer.alloc(8); b.writeDoubleLE(num, 0); return b;
|
|
400
401
|
}
|
|
401
|
-
if (Number.isInteger(num) && num <= 2147483647 && num >= -2147483648) {
|
|
402
|
-
const b = Buffer.alloc(4); b.writeInt32LE(num, 0); return b;
|
|
403
|
-
}
|
|
404
402
|
if (!Number.isFinite(num)) return encodeLenencString(String(v));
|
|
405
|
-
|
|
403
|
+
if (type === MYSQL_TYPE_LONG) {
|
|
404
|
+
// 4 字节 LONG:总是写 4 字节(#32)。超出 int32 范围时钳位,
|
|
405
|
+
// 不能写 8 字节,否则二进制结果集的行布局被破坏(下一列错位)。
|
|
406
|
+
let n = Math.trunc(num);
|
|
407
|
+
if (n > 2147483647) n = 2147483647;
|
|
408
|
+
if (n < -2147483648) n = -2147483648;
|
|
409
|
+
const b = Buffer.alloc(4); b.writeInt32LE(n, 0); return b;
|
|
410
|
+
}
|
|
411
|
+
// MYSQL_TYPE_LONGLONG:始终写 8 字节,超出 int64 范围时钳位(保持行布局)。
|
|
412
|
+
let bi = BigInt(Math.trunc(num));
|
|
413
|
+
const INT64_MAX = 9223372036854775807n;
|
|
414
|
+
const INT64_MIN = -9223372036854775808n;
|
|
415
|
+
if (bi > INT64_MAX) bi = INT64_MAX;
|
|
416
|
+
if (bi < INT64_MIN) bi = INT64_MIN;
|
|
417
|
+
const b = Buffer.alloc(8); b.writeBigInt64LE(bi, 0); return b;
|
|
406
418
|
}
|
|
407
419
|
if (type === MYSQL_TYPE_TINY) {
|
|
408
420
|
const b = Buffer.alloc(1); b.writeInt8(v ? 1 : 0, 0); return b;
|
|
@@ -1102,8 +1114,10 @@ class MysqlServer {
|
|
|
1102
1114
|
if (t && t.indexOf('.') !== -1) {
|
|
1103
1115
|
const re = new RegExp('\\b' + t.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '\\b', 'g');
|
|
1104
1116
|
s = s.replace(re, (match, offset) => {
|
|
1105
|
-
//
|
|
1106
|
-
|
|
1117
|
+
// 如果匹配位置在字符串字面量或注释中,不替换。
|
|
1118
|
+
// _maskStringsAndComments 把字符串/注释内的字符置为空格,
|
|
1119
|
+
// 因此 masked[offset] === ' ' 表示该位置在字面量/注释里(#31)。
|
|
1120
|
+
if (masked[offset] === ' ') return match;
|
|
1107
1121
|
return t.split('.')[1];
|
|
1108
1122
|
});
|
|
1109
1123
|
}
|
package/lib/native_client.js
CHANGED
|
@@ -57,6 +57,19 @@ function _trackInstance(inst) {
|
|
|
57
57
|
}
|
|
58
58
|
|
|
59
59
|
const INT64_TAG = 1;
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* 把可能是字符串/数字的 limit / offset / id 规范成非负整数,供原生 usize/u64 使用。
|
|
63
|
+
* 负值(或 NaN/Infinity)会在绑定层直接抛错,而不是让 Rust 侧 debug panic 或
|
|
64
|
+
* release 静默空结果(#35)。返回 Math.trunc(n)(对非负浮点向下取整,与 usize 语义一致)。
|
|
65
|
+
*/
|
|
66
|
+
function toNonNeg(name, v, def) {
|
|
67
|
+
if (v === undefined || v === null) return def;
|
|
68
|
+
const n = Number(v);
|
|
69
|
+
if (!Number.isFinite(n)) throw new Error(`jsql-neo: ${name} must be a finite number, got ${v}`);
|
|
70
|
+
if (n < 0) throw new Error(`jsql-neo: ${name} must be non-negative, got ${v}`);
|
|
71
|
+
return Math.trunc(n);
|
|
72
|
+
}
|
|
60
73
|
const FLOAT_TAG = 2;
|
|
61
74
|
const STR_TAG = 3;
|
|
62
75
|
const BOOL_TAG = 4;
|
|
@@ -539,7 +552,7 @@ class JSQL {
|
|
|
539
552
|
if (!schema || !pk) return id;
|
|
540
553
|
const def = schema[pk];
|
|
541
554
|
let t = null;
|
|
542
|
-
if (typeof def === 'string') t = def.toLowerCase();
|
|
555
|
+
if (typeof def === 'string') t = def.trim().split(/\s+/)[0].toLowerCase();
|
|
543
556
|
else if (def && typeof def === 'object' && def.type) t = String(def.type).toLowerCase();
|
|
544
557
|
if (['integer', 'int', 'bigint'].includes(t)) return Number(id);
|
|
545
558
|
return id;
|
|
@@ -552,8 +565,11 @@ class JSQL {
|
|
|
552
565
|
_isAutoIntPk(table) {
|
|
553
566
|
if (!this._pkAutoIncrement(table)) return false;
|
|
554
567
|
const def = this._schemas[table][this._pkOf(table)];
|
|
555
|
-
|
|
556
|
-
|
|
568
|
+
// 简写字符串 schema(如 'integer primary key auto_increment')取首个 token 作为类型;
|
|
569
|
+
// 对象 schema 直接读 def.type。二者都必须精确匹配整数类型,否则负 id 校验不生效(issue #35)。
|
|
570
|
+
let t = '';
|
|
571
|
+
if (typeof def === 'string') t = def.trim().split(/\s+/)[0].toLowerCase();
|
|
572
|
+
else if (def && def.type) t = String(def.type).toLowerCase();
|
|
557
573
|
return ['integer', 'int', 'bigint'].includes(t);
|
|
558
574
|
}
|
|
559
575
|
|
|
@@ -561,8 +577,11 @@ class JSQL {
|
|
|
561
577
|
const pk = this._pkOf(table);
|
|
562
578
|
if (pk) {
|
|
563
579
|
if (this._isAutoIntPk(table)) {
|
|
564
|
-
const n =
|
|
580
|
+
const n = toNonNeg('id', id, NaN);
|
|
565
581
|
if (Number.isFinite(n)) {
|
|
582
|
+
// 负 id 会被原生层 `i64 -> u64` 静默回绕成 usize::MAX,导致
|
|
583
|
+
// findById 误报 not found(行明明存在却取不到)。在此显式拒绝。
|
|
584
|
+
if (n < 0) throw new Error('id must be a non-negative number');
|
|
566
585
|
const r = safeParse(this._n('jsqlFindById', table, n));
|
|
567
586
|
if (r && r.error) return r;
|
|
568
587
|
return r || null;
|
|
@@ -574,15 +593,18 @@ class JSQL {
|
|
|
574
593
|
if (r && r.error) return r;
|
|
575
594
|
return Array.isArray(r) && r.length > 0 ? r[0] : null;
|
|
576
595
|
}
|
|
577
|
-
return safeParse(this._n('jsqlFindById', table,
|
|
596
|
+
return safeParse(this._n('jsqlFindById', table, toNonNeg('id', id, NaN)));
|
|
578
597
|
}
|
|
579
598
|
|
|
580
599
|
_seqIdByPk(table, id) {
|
|
581
600
|
const pk = this._pkOf(table);
|
|
582
601
|
if (pk) {
|
|
583
602
|
if (this._isAutoIntPk(table)) {
|
|
584
|
-
const n =
|
|
603
|
+
const n = toNonNeg('id', id, NaN);
|
|
585
604
|
if (Number.isFinite(n)) {
|
|
605
|
+
// 负 id 在原生层回绕为 usize::MAX 后静默 not found,update/remove
|
|
606
|
+
// 会无提示地什么都不做 —— 这里显式拒绝(见 issue #35)。
|
|
607
|
+
if (n < 0) throw new Error('id must be a non-negative number');
|
|
586
608
|
const r = safeParse(this._n('jsqlFindById', table, n));
|
|
587
609
|
if (r && !r.error && r.id !== undefined) return r.id;
|
|
588
610
|
}
|
|
@@ -824,7 +846,10 @@ class JSQL {
|
|
|
824
846
|
let r;
|
|
825
847
|
if (this._isAutoIntPk(table) && ids.every(id => Number.isFinite(Number(id)))) {
|
|
826
848
|
// 自增主键:内部 id 即主键值,一次批量调用即可,避免 N 次往返。
|
|
827
|
-
|
|
849
|
+
// #35:负 id 在原生层 `i64 -> u64` 回绕成 usize::MAX,静默 not found;
|
|
850
|
+
// NaN/Infinity 同样不允许。绑定层先校验,绝不把非法值传给原生模块。
|
|
851
|
+
const nums = ids.map(id => toNonNeg('id', id, 0));
|
|
852
|
+
r = safeParse(this._n('jsqlFindByIds', table, JSON.stringify(nums)));
|
|
828
853
|
} else if (this._pkOf(table)) {
|
|
829
854
|
r = ids.map(id => this._findRawById(table, id)).filter(x => x !== null && !(x && x.error));
|
|
830
855
|
} else {
|
|
@@ -846,7 +871,12 @@ class JSQL {
|
|
|
846
871
|
if (!(await this._runHooks('beforeFind', [table, { filter, opts }]))) return [];
|
|
847
872
|
const filterStr = filter ? JSON.stringify(filter) : '';
|
|
848
873
|
const { limit = 100, offset = 0 } = opts;
|
|
849
|
-
|
|
874
|
+
// #35:原生层把 limit/offset 按 `as usize` 无符号转换,负数会静默回绕
|
|
875
|
+
// (release 下空结果 / debug 下 usize::MAX+1 溢出 panic)。绑定层先校验,
|
|
876
|
+
// 与 WASM 端的 usize 签名保持一致,绝不把负值传给原生模块。
|
|
877
|
+
const lim = toNonNeg('limit', limit, 100);
|
|
878
|
+
const off = toNonNeg('offset', offset, 0);
|
|
879
|
+
const r = safeParse(this._n('jsqlFind', table, filterStr, lim, off));
|
|
850
880
|
if (r && r.error) throw new Error(r.error);
|
|
851
881
|
await this._runHooks('afterFind', [table, { filter, opts }, r]);
|
|
852
882
|
const schema = this._schemas[table];
|
package/lib/pg_server.js
CHANGED
|
@@ -711,7 +711,11 @@ class PgConnection {
|
|
|
711
711
|
const tag = `SELECT ${(res.rows || []).length}`;
|
|
712
712
|
this._send(buildCommandComplete(tag));
|
|
713
713
|
} else if (res.type === 'insert' || res.type === 'update' || res.type === 'delete') {
|
|
714
|
-
|
|
714
|
+
// PG CommandComplete tag:INSERT 是 "INSERT <oid> <count>",UPDATE/DELETE 是
|
|
715
|
+
// "UPDATE <count>" / "DELETE <count>"(#33)。缺了 OID 段会让客户端把 count
|
|
716
|
+
// 解析到 OID 槽位、rowCount 变 NaN。
|
|
717
|
+
const count = res.affectedRows != null ? res.affectedRows : 0;
|
|
718
|
+
const tag = res.type === 'insert' ? `INSERT 0 ${count}` : `${String(res.type).toUpperCase()} ${count}`;
|
|
715
719
|
this._send(buildCommandComplete(tag));
|
|
716
720
|
} else if (res.type === 'begin' || res.type === 'commit' || res.type === 'rollback') {
|
|
717
721
|
this._inTransaction = res.type === 'begin';
|
package/lib/query.js
CHANGED
|
@@ -227,9 +227,11 @@ class Query {
|
|
|
227
227
|
const startTime = Date.now();
|
|
228
228
|
if (this._explain) return this._getExplain();
|
|
229
229
|
|
|
230
|
-
//
|
|
230
|
+
// 缓存命中: 返回每行的浅拷贝,与未命中路径一致,避免调用方修改污染缓存
|
|
231
231
|
if (this._cache && this._cache.result && (Date.now() - this._cache.timestamp < this._cache.ttl)) {
|
|
232
|
-
|
|
232
|
+
// #37:命中返回副本,而不是内部数组引用——调用方对返回值做变更
|
|
233
|
+
// (push/splice/改字段)会污染缓存;未命中路径返回的是新数组,行为应一致。
|
|
234
|
+
return this._cache.result.map(row => (row && typeof row === 'object' ? { ...row } : row));
|
|
233
235
|
}
|
|
234
236
|
|
|
235
237
|
// P1-4: 软删除模式下 QueryBuilder 也不得泄露已删除行
|
|
@@ -327,9 +329,10 @@ class Query {
|
|
|
327
329
|
rows = copy;
|
|
328
330
|
}
|
|
329
331
|
|
|
330
|
-
//
|
|
332
|
+
// 缓存存储:#37 存副本,未命中路径返回的 rows 直接给调用方随意变更,
|
|
333
|
+
// 不能把同一个数组塞进缓存(否则调用方 push/改字段会污染后续命中)。
|
|
331
334
|
if (this._cache) {
|
|
332
|
-
this._cache.result = rows;
|
|
335
|
+
this._cache.result = rows.map(row => (row && typeof row === 'object' ? { ...row } : row));
|
|
333
336
|
this._cache.timestamp = Date.now();
|
|
334
337
|
}
|
|
335
338
|
|