jsql-neo 6.3.5 → 6.3.6
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 +8 -8
- package/index.js +1 -1
- package/lib/native_client.js +30 -0
- package/lib/sql.js +5 -1
- package/lib/wasm_client.js +35 -0
- package/package.json +1 -1
- package/test/dml-constraints.test.js +41 -1
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. The browser. **And it fits in one npm package.**
|
|
5
5
|
|
|
6
|
-
> **v6.3.
|
|
6
|
+
> **v6.3.6** — official release build · [github.com/vexify-org/JSQL-neo](https://github.com/vexify-org/JSQL-neo)
|
|
7
7
|
|
|
8
8
|

|
|
9
9
|

|
|
@@ -1470,7 +1470,7 @@ jsql mod --engine wasm # switch engine (restart required)
|
|
|
1470
1470
|
|
|
1471
1471
|
```bash
|
|
1472
1472
|
$ jsql version
|
|
1473
|
-
jsql-neo v6.3.
|
|
1473
|
+
jsql-neo v6.3.6
|
|
1474
1474
|
engine: native (napi) | wasm | js
|
|
1475
1475
|
node: v22.0.0 platform: linux x64
|
|
1476
1476
|
```
|
|
@@ -1491,7 +1491,7 @@ jsql tui --memory -q # memory mode, quiet
|
|
|
1491
1491
|
jsql tui --prompt 'db> ' --no-color
|
|
1492
1492
|
```
|
|
1493
1493
|
|
|
1494
|
-
The status bar shows: `db=<name> dialect=<d> mode=<tui|batch> ver=6.3.
|
|
1494
|
+
The status bar shows: `db=<name> dialect=<d> mode=<tui|batch> ver=6.3.6`.
|
|
1495
1495
|
|
|
1496
1496
|
### Keyboard shortcuts
|
|
1497
1497
|
|
|
@@ -4175,7 +4175,7 @@ Data dir: /root/.jsql-neo/data
|
|
|
4175
4175
|
|
|
4176
4176
|
```bash
|
|
4177
4177
|
$ jsql version
|
|
4178
|
-
jsql-neo v6.3.
|
|
4178
|
+
jsql-neo v6.3.6
|
|
4179
4179
|
engine: native (napi) | wasm | js
|
|
4180
4180
|
node: v22.0.0
|
|
4181
4181
|
platform: linux x64
|
|
@@ -5275,7 +5275,7 @@ Apache License
|
|
|
5275
5275
|
|
|
5276
5276
|
*JSQL-NEO — One engine to rule them all. MySQL. PostgreSQL. MongoDB. Redis. SQL. TypeScript. The browser.*
|
|
5277
5277
|
|
|
5278
|
-
*文档版本:v6.3.
|
|
5278
|
+
*文档版本:v6.3.6 · 最后更新:2026-09-27*
|
|
5279
5279
|
|
|
5280
5280
|
---
|
|
5281
5281
|
|
|
@@ -7047,7 +7047,7 @@ Usage: jsql version
|
|
|
7047
7047
|
|
|
7048
7048
|
输出版本与环境信息:
|
|
7049
7049
|
|
|
7050
|
-
jsql-neo v6.3.
|
|
7050
|
+
jsql-neo v6.3.6
|
|
7051
7051
|
engine: native (napi) | wasm | js
|
|
7052
7052
|
node: v22.0.0
|
|
7053
7053
|
platform: linux x64
|
|
@@ -7703,7 +7703,7 @@ CI(GitHub Actions)矩阵:`node 20/22` × `linux/macos/windows` × `native/
|
|
|
7703
7703
|
|
|
7704
7704
|
*JSQL-NEO — One engine to rule them all. MySQL. PostgreSQL. MongoDB. Redis. SQL. TypeScript. The browser.*
|
|
7705
7705
|
|
|
7706
|
-
*文档版本:v6.3.
|
|
7706
|
+
*文档版本:v6.3.6 · 共 19 个附录 · 最后更新:2026-09-27*
|
|
7707
7707
|
|
|
7708
7708
|
---
|
|
7709
7709
|
|
|
@@ -8310,4 +8310,4 @@ npm test
|
|
|
8310
8310
|
|
|
8311
8311
|
*JSQL-NEO — One engine to rule them all. MySQL. PostgreSQL. MongoDB. Redis. SQL. TypeScript. The browser.*
|
|
8312
8312
|
|
|
8313
|
-
*文档版本:v6.3.
|
|
8313
|
+
*文档版本:v6.3.6 · 附录 A–Z · 全文 6000+ 行 · 最后更新:2026-09-27*
|
package/index.js
CHANGED
package/lib/native_client.js
CHANGED
|
@@ -707,11 +707,41 @@ class JSQL {
|
|
|
707
707
|
if (!this._runHooks('beforeUpdate', [table, id, data])) return;
|
|
708
708
|
const seq = this._seqIdByPk(table, id);
|
|
709
709
|
if (seq === null) return;
|
|
710
|
+
// 改主键 / UNIQUE 列前先查冲突(同步版,与 lib/database.js 的修复对齐)。
|
|
711
|
+
// 此前直接写入,导致 UPDATE t SET id = 1 WHERE id = 2 静默产生两行同主键。
|
|
712
|
+
this._assertNoKeyConflictSync(table, id, data);
|
|
710
713
|
if (!this._opBuffer.update[table]) this._opBuffer.update[table] = [];
|
|
711
714
|
this._opBuffer.update[table].push([seq, data]);
|
|
712
715
|
this._scheduleOpFlush();
|
|
713
716
|
}
|
|
714
717
|
|
|
718
|
+
/** UPDATE 后的主键 / UNIQUE 值是否与其它行冲突(同步检查) */
|
|
719
|
+
_assertNoKeyConflictSync(table, id, data) {
|
|
720
|
+
const schema = this._schemas && this._schemas[table];
|
|
721
|
+
if (!schema) return;
|
|
722
|
+
const fields = Object.keys(schema).filter(f => schema[f] && (schema[f].primaryKey || schema[f].unique));
|
|
723
|
+
if (!fields.length) return;
|
|
724
|
+
const raw = this._n('jsqlFind', table, '{}', 1000000, 0);
|
|
725
|
+
const all = safeParse(raw);
|
|
726
|
+
if (!Array.isArray(all)) return;
|
|
727
|
+
const pk = this._pkOf(table) || 'id';
|
|
728
|
+
// jsqlFind 返回的行结构是 { id, fields:{...} } —— 值在 fields 里,不在顶层。
|
|
729
|
+
const valOf = (r, f) => (r && r.fields && typeof r.fields === 'object' ? r.fields[f] : r ? r[f] : undefined);
|
|
730
|
+
const idOf = (r) => (r && r.fields ? valOf(r, pk) : r ? r[pk] : undefined);
|
|
731
|
+
for (const f of fields) {
|
|
732
|
+
if (!Object.prototype.hasOwnProperty.call(data, f)) continue;
|
|
733
|
+
const nv = data[f];
|
|
734
|
+
const self = all.find(r => String(idOf(r)) === String(id));
|
|
735
|
+
if (self && valOf(self, f) === nv) continue; // 值没变
|
|
736
|
+
if (all.some(r => r !== self && valOf(r, f) === nv)) {
|
|
737
|
+
const e = new Error(`Duplicate entry '${nv}' for key '${f}'`);
|
|
738
|
+
e.code = 'ER_DUP_ENTRY';
|
|
739
|
+
e.errno = 1062;
|
|
740
|
+
throw e;
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
|
|
715
745
|
removeById(table, id) {
|
|
716
746
|
if (!this._runHooks('beforeDelete', [table, id])) return;
|
|
717
747
|
const seq = this._seqIdByPk(table, id);
|
package/lib/sql.js
CHANGED
|
@@ -4156,7 +4156,11 @@ class SQLExecutor {
|
|
|
4156
4156
|
for (const [col, val] of statement.assignments) {
|
|
4157
4157
|
data[col] = typeof val === 'object' && val !== null && val.type ? resolveOperand(val, row, this.ctx) : val;
|
|
4158
4158
|
}
|
|
4159
|
-
|
|
4159
|
+
// 必须 await:wasm / native client 的 updateById 是异步的,
|
|
4160
|
+
// 不等就会把「主键/唯一约束冲突」的异常变成未处理的 Promise 拒绝 ——
|
|
4161
|
+
// 表现为 UPDATE 静默成功、产生两行同主键,而调用方拿不到任何错误。
|
|
4162
|
+
// lib/database.js 的实现是同步的,await 同步值同样安全。
|
|
4163
|
+
await this.engine.updateById(statement.table, id, data);
|
|
4160
4164
|
count++;
|
|
4161
4165
|
}
|
|
4162
4166
|
}
|
package/lib/wasm_client.js
CHANGED
|
@@ -664,6 +664,12 @@ class JSQL {
|
|
|
664
664
|
await this._flush();
|
|
665
665
|
const seq = this._seqIdByPk(table, id);
|
|
666
666
|
if (seq === null) return null;
|
|
667
|
+
// 改主键 / UNIQUE 列前先查冲突。
|
|
668
|
+
// 此前直接调 jsql_update_by_id,导致
|
|
669
|
+
// UPDATE t SET id = 1 WHERE id = 2 (t 已有 id=1)
|
|
670
|
+
// 静默产生两行同主键 —— 主键索引只留一个,索引与实际行数组不一致。
|
|
671
|
+
// MySQL / SQLite 都会报 UNIQUE constraint failed。
|
|
672
|
+
await this._assertNoKeyConflict(table, id, data, seq);
|
|
667
673
|
const r = safeJsonParse(wasmBindings.jsql_update_by_id(table, BigInt(seq), JSON.stringify(data)));
|
|
668
674
|
if (r && r.ok === false) throw new Error(r.error || 'update failed');
|
|
669
675
|
this._emit('update', { table, id, data });
|
|
@@ -671,6 +677,35 @@ class JSQL {
|
|
|
671
677
|
return r;
|
|
672
678
|
}
|
|
673
679
|
|
|
680
|
+
/**
|
|
681
|
+
* 校验 UPDATE 后的主键 / UNIQUE 值不与其它行冲突。
|
|
682
|
+
* 逐行比对(未建索引也正确);值未变化时跳过。
|
|
683
|
+
*/
|
|
684
|
+
async _assertNoKeyConflict(table, id, data, selfSeq) {
|
|
685
|
+
const schema = this._schemas[table];
|
|
686
|
+
if (!schema) return;
|
|
687
|
+
const fields = Object.keys(schema).filter(f => schema[f] && (schema[f].primaryKey || schema[f].unique));
|
|
688
|
+
if (!fields.length) return;
|
|
689
|
+
let rows;
|
|
690
|
+
try {
|
|
691
|
+
rows = await this.find(table, {}, { limit: 1e9, offset: 0 });
|
|
692
|
+
} catch (_) {
|
|
693
|
+
return; // 读不到就不拦,交给底层报错
|
|
694
|
+
}
|
|
695
|
+
for (const f of fields) {
|
|
696
|
+
if (!Object.prototype.hasOwnProperty.call(data, f)) continue;
|
|
697
|
+
const nv = data[f];
|
|
698
|
+
const self = rows.find(r => r && String(r.id) === String(id));
|
|
699
|
+
if (self && self[f] === nv) continue; // 值没变
|
|
700
|
+
if (rows.some(r => r && r !== self && r[f] === nv)) {
|
|
701
|
+
const e = new Error(`Duplicate entry '${nv}' for key '${f}'`);
|
|
702
|
+
e.code = 'ER_DUP_ENTRY';
|
|
703
|
+
e.errno = 1062;
|
|
704
|
+
throw e;
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
|
|
674
709
|
async removeById(table, id) {
|
|
675
710
|
if (!this._runHooks('beforeDelete', [table, id])) return;
|
|
676
711
|
await this._flush();
|
package/package.json
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
const path = require('path');
|
|
14
14
|
|
|
15
15
|
const ROOT = path.join(__dirname, '..');
|
|
16
|
-
const { Database } = require(path.join(ROOT, 'index.js'));
|
|
16
|
+
const { Database, JSQL, NativeJSQL } = require(path.join(ROOT, 'index.js'));
|
|
17
17
|
const { executeSQL } = require(path.join(ROOT, 'lib/sql.js'));
|
|
18
18
|
|
|
19
19
|
const OPTS = { safety: false };
|
|
@@ -130,6 +130,46 @@ async function t(name, fn) {
|
|
|
130
130
|
return eq(await rows(`SELECT id, v FROM t ORDER BY id`), [[1, 99], [2, 20]]);
|
|
131
131
|
});
|
|
132
132
|
|
|
133
|
+
// 三个 engine 都要拦住 —— 修复曾只加在 lib/database.js,
|
|
134
|
+
// 而 JSQL(wasm)走 lib/wasm_client.js、NativeJSQL 走 lib/native_client.js,
|
|
135
|
+
// 各自的 updateById 是独立实现。
|
|
136
|
+
console.log('\n--- UPDATE 冲突校验:三个 engine 都要拦住 ---');
|
|
137
|
+
const engines = [
|
|
138
|
+
['JSQL(wasm)', () => new JSQL({ mode: 'memory' })],
|
|
139
|
+
['NativeJSQL', () => new NativeJSQL({ mode: 'memory' })],
|
|
140
|
+
['Database', () => new Database(':memory:')],
|
|
141
|
+
];
|
|
142
|
+
for (const [name, mk] of engines) {
|
|
143
|
+
await t(`${name} UPDATE 主键冲突 → 报错且数据完好`, async () => {
|
|
144
|
+
const db = await mk();
|
|
145
|
+
if (db.start) await db.start();
|
|
146
|
+
try {
|
|
147
|
+
const exec = (sql) => executeSQL(db, sql, OPTS);
|
|
148
|
+
await exec(`CREATE TABLE e (id INT PRIMARY KEY, v INT)`);
|
|
149
|
+
await exec(`INSERT INTO e VALUES (1,10),(2,20),(3,30)`);
|
|
150
|
+
let threw = false;
|
|
151
|
+
try { await exec(`UPDATE e SET id = 1 WHERE id = 2`); } catch (_) { threw = true; }
|
|
152
|
+
const after = (await exec(`SELECT id, v FROM e ORDER BY id`)).rows;
|
|
153
|
+
// 改不冲突的值必须仍然可用
|
|
154
|
+
await exec(`UPDATE e SET id = 9 WHERE id = 2`);
|
|
155
|
+
const ok = (await exec(`SELECT id FROM e ORDER BY id`)).rows;
|
|
156
|
+
return threw && eq(after, [[1, 10], [2, 20], [3, 30]]) && eq(ok, [[1], [3], [9]]);
|
|
157
|
+
} finally { try { await db.stop(); } catch (_) { /* ignore */ } }
|
|
158
|
+
});
|
|
159
|
+
await t(`${name} UPDATE UNIQUE 冲突 → 报错`, async () => {
|
|
160
|
+
const db = await mk();
|
|
161
|
+
if (db.start) await db.start();
|
|
162
|
+
try {
|
|
163
|
+
const exec = (sql) => executeSQL(db, sql, OPTS);
|
|
164
|
+
await exec(`CREATE TABLE q (id INT PRIMARY KEY, e TEXT UNIQUE)`);
|
|
165
|
+
await exec(`INSERT INTO q VALUES (1,'a'),(2,'b')`);
|
|
166
|
+
let threw = false;
|
|
167
|
+
try { await exec(`UPDATE q SET e = 'a' WHERE id = 2`); } catch (_) { threw = true; }
|
|
168
|
+
return threw && eq((await exec(`SELECT e FROM q ORDER BY id`)).rows, [['a'], ['b']]);
|
|
169
|
+
} finally { try { await db.stop(); } catch (_) { /* ignore */ } }
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
|
|
133
173
|
console.log('\n--- 既有约束行为回归对照 ---');
|
|
134
174
|
await t('INSERT 主键重复 → 报错', async ({ exec, mustFail }) => {
|
|
135
175
|
await exec(`CREATE TABLE t (id INT PRIMARY KEY)`);
|