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 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.5** — official release build · [github.com/vexify-org/JSQL-neo](https://github.com/vexify-org/JSQL-neo)
6
+ > **v6.3.6** — official release build · [github.com/vexify-org/JSQL-neo](https://github.com/vexify-org/JSQL-neo)
7
7
 
8
8
  ![Engines](https://img.shields.io/badge/engines-Native%20%7C%20WASM%20%7C%20Pure%20JS-7ee787)
9
9
  ![MySQL](https://img.shields.io/badge/protocol-MySQL%20compatible-1f6feb)
@@ -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.5
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.5`.
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.5
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.5 · 最后更新:2026-09-27*
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.5
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.5 · 共 19 个附录 · 最后更新:2026-09-27*
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.5 · 附录 A–Z · 全文 6000+ 行 · 最后更新:2026-09-27*
8313
+ *文档版本:v6.3.6 · 附录 A–Z · 全文 6000+ 行 · 最后更新:2026-09-27*
package/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * JSQL-NEO v6.3.5 — Rust-Powered Embedded Database (WASM + HTTP)
2
+ * JSQL-NEO v6.3.6 — Rust-Powered Embedded Database (WASM + HTTP)
3
3
  *
4
4
  * @example
5
5
  * const jsql = require('jsql-neo');
@@ -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
- this.engine.updateById(statement.table, id, data);
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
  }
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jsql-neo",
3
- "version": "6.3.5",
3
+ "version": "6.3.6",
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",
@@ -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)`);