jsql-neo 6.5.16 → 6.5.20

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, **and the browser**.
5
5
 
6
- > **v6.5.16** — stable release · [github.com/vexify-org/JSQL-neo](https://github.com/vexify-org/JSQL-neo) · [jsql.vexify.top](https://jsql.vexify.top)
6
+ > **v6.5.20** — 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
  ![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)
@@ -47,10 +47,10 @@ irm jsql.vexify.top/setup | iex
47
47
  npm install jsql-neo
48
48
  ```
49
49
 
50
- **JSQL-NEO** is a Rust-powered embedded database with **zero runtime dependencies**. It runs in Node.js, CLI, Web UI, and the browser (WASM).
50
+ **JSQL-NEO** is a Rust-powered embedded database with a **single runtime dependency** (`@vexify-org/yaggs`, used only by the `jsql` CLI argument parser) — no native libs to compile, no system deps to install. It runs in Node.js, CLI, Web UI, and the browser (WASM).
51
51
 
52
52
  - **Four wire protocols on one port** — `jsql serve` accepts MySQL, PostgreSQL, MongoDB and Redis clients; `mysql2`, `pg`, `mongodb` and `ioredis` connect directly
53
- - **Zero dependencies** — no native libs to compile, no system deps to install
53
+ - **Nearly dependency-free** — only `@vexify-org/yaggs` (CLI arg parsing); no native libs to compile, no system deps to install
54
54
  - **Three engines, same API** — `native` (Rust N-API), `wasm` (same Rust core), `js` (pure JS)
55
55
  - **O(1) primary-key lookups and updates** — hash index, no full-table scan; unindexed filters run in Rust
56
56
 
@@ -80,7 +80,7 @@ JSQL-NEO: MySQL + PostgreSQL + MongoDB + Redis + SQL
80
80
  | **Redis** | `ioredis`, `redis-cli` — all data types + TTL |
81
81
  | **One Port** | Protocol sniffing routes all four clients to the same data |
82
82
  | **Zero-Dep TUI** | `jsql tui` — line editing, history, Tab, CJK tables |
83
- | **Web UI** | Zero-dependency management console + HTTP API |
83
+ | **Web UI** | Management console + HTTP API with no frontend framework |
84
84
  | **WASM** | Full engine in browser, no server needed |
85
85
 
86
86
  ---
package/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * JSQL-NEO v6.5.16 — Rust-Powered Embedded Database (WASM + HTTP)
2
+ * JSQL-NEO v6.5.20 — Rust-Powered Embedded Database (WASM + HTTP)
3
3
  *
4
4
  * @example
5
5
  * const jsql = require('jsql-neo');
@@ -131,6 +131,9 @@ function injectMySQLCompat(mod) {
131
131
 
132
132
  module.exports = {
133
133
  JSQL: WasmClient.JSQL,
134
+ // jsql 1.0.0 怀旧兼容层(同步 createTable/dropTable + db.<表名> 属性)
135
+ // 用法:const { Database } = require('jsql-neo/compat/v1');
136
+ compatV1: require('./lib/compat/v1'),
134
137
  NativeJSQL: NativeClient.JSQL,
135
138
  Database: require('./lib/database'),
136
139
  Table: require('./lib/table'),
package/lib/btree.js CHANGED
@@ -20,10 +20,28 @@ class BTree {
20
20
  * @param {boolean} unique - 是否唯一索引
21
21
  */
22
22
  constructor(order = 64, unique = false) {
23
- this._order = order;
23
+ if (typeof order !== 'number' || !Number.isFinite(order)) {
24
+ throw new Error(`BTree order must be a finite number (got ${order})`);
25
+ }
26
+ // order=3 属于 2-3 树,删除时的 merge 需要「三个 1-key 子节点 + 分隔键 = 3 keys」
27
+ // 塞进一个最多 2 keys 的节点,B-tree 的标准 borrow/merge 在数学上做不到,
28
+ // 必须换一整套 2-3 树删除算法(或者用 2-3-4 树即 order=4 等价替身)。
29
+ // 这里把 order<4 归一化成 4:多路树的所有查询语义(search/range/entries/prefix)
30
+ // 与 order 无关,唯一区别是节点最多存 3 个 key 而不是 2 个 —— 只会更平衡。
31
+ //
32
+ // 以前不归一化时 _minKeys = floor(order/2)-1 = 0,允许空节点;_splitChild 在
33
+ // 只剩 2 个 key 的节点上算出的分隔键下标又超出了右半边界,于是右子节点为空、
34
+ // 父节点出现重复分隔键 —— 插入 19,7,12 后根节点 keys 变成 [19,19],
35
+ // 之后混合增删会静默丢索引条目。
36
+ this._requestedOrder = order;
37
+ this._order = Math.max(4, Math.trunc(order));
24
38
  this._unique = unique;
25
39
  this._root = new BTreeNode();
26
40
  this._size = 0;
41
+ // 必须用归一化后的 this._order 计算下界,否则 order=3 会算出 _minKeys=0
42
+ // (允许空节点),删除时 merge/borrow 判定全错,最后留下 64 条幽灵条目。
43
+ this._minKeys = Math.floor(this._order / 2) - 1;
44
+ this._minDegree = Math.ceil(this._order / 2);
27
45
  }
28
46
 
29
47
  get size() { return this._size; }
@@ -91,22 +109,28 @@ class BTree {
91
109
  const newChild = new BTreeNode(child.leaf);
92
110
  const mid = Math.floor((this._order - 1) / 2);
93
111
 
94
- const midKey = child.keys[mid];
95
- const midVal = child.values[mid];
112
+ // 分隔键下标:必须保证左、右两侧都至少留 1 个 key。
113
+ // 节点满时 keys.length === order-1,右半从 sepIdx+1 开始,
114
+ // 所以 sepIdx <= order-3。取 min(mid, order-3):order>=4 时 mid <= order-3 恒成立
115
+ // (order=4 → 1, order=5 → 2, order=6 → 2, order=64 → 31),行为与标准 B-tree 一致。
116
+ const sepIdx = Math.max(0, Math.min(mid, this._order - 3));
117
+
118
+ const midKey = child.keys[sepIdx];
119
+ const midVal = child.values[sepIdx];
96
120
 
97
121
  if (child.leaf) {
98
122
  // 叶子:右半移到新节点;分隔键保留在左叶(它是真实数据键),父节点只存副本
99
- newChild.keys = child.keys.splice(mid + 1);
100
- newChild.values = child.values.splice(mid + 1);
123
+ newChild.keys = child.keys.splice(sepIdx + 1);
124
+ newChild.values = child.values.splice(sepIdx + 1);
101
125
  newChild.next = child.next;
102
126
  child.next = newChild;
103
127
  } else {
104
128
  // 内部:分隔键提升到父节点并从子节点移除,保持 children = keys + 1
105
- newChild.keys = child.keys.splice(mid + 1);
106
- newChild.values = child.values.splice(mid + 1);
107
- child.keys.splice(mid, 1);
108
- child.values.splice(mid, 1);
109
- newChild.children = child.children.splice(mid + 1);
129
+ newChild.keys = child.keys.splice(sepIdx + 1);
130
+ newChild.values = child.values.splice(sepIdx + 1);
131
+ child.keys.splice(sepIdx, 1);
132
+ child.values.splice(sepIdx, 1);
133
+ newChild.children = child.children.splice(sepIdx + 1);
110
134
  }
111
135
 
112
136
  parent.keys.splice(index, 0, midKey);
@@ -271,81 +295,122 @@ class BTree {
271
295
 
272
296
  remove(key, rowIndex) {
273
297
  if (!this._root) return false;
274
- const res = this._removeFromNode(this._root, key, rowIndex);
275
- if (!res.found) return false;
276
- if (!res.remaining) this._size--;
277
- // 只做惰性删除 + 分隔键归一:不再在下降途中借用/合并。
278
- // 旧实现先对路由到的子节点做 borrow/merge 再检查键是否存在,
279
- // 删除一个不存在的键(或 rowIndex 不匹配)也会重构整棵树,
280
- // 合并时丢掉的分隔键副本可能正好是仍然存活的数据键 —— 条目静默丢失。
281
- this._normalize();
282
- return !res.remaining;
298
+ const removed = this._removeFromNode(this._root, key, rowIndex);
299
+ // 删掉整个键(而非仅删某个 rowIndex)后,叶子的键域可能缩小,
300
+ // 沿途的分隔键副本可能已指向不存在的键 —— 自顶向下重建一次。
301
+ if (removed) {
302
+ this._size--;
303
+ if (!this._root.leaf) this._fixSeparators(this._root);
304
+ }
305
+ if (this._root.keys.length === 0 && !this._root.leaf && this._root.children.length > 0) {
306
+ this._root = this._root.children[0];
307
+ }
308
+ return removed;
283
309
  }
284
310
 
285
311
  _removeFromNode(node, key, rowIndex) {
286
312
  if (node.leaf) {
287
313
  const i = this._lowerBound(node.keys, key);
288
- if (i >= node.keys.length || node.keys[i] !== key) return { found: false };
289
- const pos = node.values[i].indexOf(rowIndex);
290
- if (pos === -1) return { found: false };
314
+ if (i >= node.keys.length || node.keys[i] !== key) return false;
315
+ if (!node.values[i].includes(rowIndex)) return false;
291
316
  const vals = node.values[i].filter(v => v !== rowIndex);
292
317
  if (vals.length === 0) {
293
318
  node.keys.splice(i, 1);
294
319
  node.values.splice(i, 1);
295
- return { found: true, remaining: false };
320
+ return true;
296
321
  }
297
322
  node.values[i] = vals;
298
- return { found: true, remaining: true };
323
+ return false;
299
324
  }
300
325
 
301
- const i = this._route(node, key);
326
+ let i = this._route(node, key);
327
+
328
+ // 下降前确保子节点有足够键,避免欠满子节点
302
329
  const child = node.children[i];
303
- if (!child) return { found: false };
304
- return this._removeFromNode(child, key, rowIndex);
330
+ if (child.keys.length <= this._minKeys) {
331
+ i = this._rebalanceChild(node, i);
332
+ }
333
+ return this._removeFromNode(node.children[i], key, rowIndex);
305
334
  }
306
335
 
307
- /**
308
- * 惰性回收:删掉节点里空的叶子/空子树,折叠只剩一个孩子的内部节点,
309
- * 并按「分隔键 = 左子树最大键」的约定重算所有内部键,最后重建叶子链表。
310
- * 节点欠满不影响正确性(插入时仍然按满则分裂),因此无需借用/合并。
311
- */
312
- _normalize() {
313
- const root = this._cleanNode(this._root);
314
- this._root = root || new BTreeNode();
315
- this._relinkLeaves();
336
+ _rebalanceChild(parent, i) {
337
+ if (i > 0 && parent.children[i - 1].keys.length > this._minKeys) {
338
+ this._borrowFromLeft(parent, i);
339
+ return i;
340
+ }
341
+ if (i + 1 < parent.children.length && parent.children[i + 1].keys.length > this._minKeys) {
342
+ this._borrowFromRight(parent, i);
343
+ return i;
344
+ }
345
+ if (i > 0) {
346
+ this._mergeChildren(parent, i - 1);
347
+ return i - 1;
348
+ }
349
+ this._mergeChildren(parent, i);
350
+ return i;
316
351
  }
317
352
 
318
- _cleanNode(node) {
319
- if (node.leaf) return node.keys.length === 0 ? null : node;
320
- const kids = [];
321
- for (const c of node.children) {
322
- const kept = this._cleanNode(c);
323
- if (kept) kids.push(kept);
353
+ _borrowFromLeft(parent, i) {
354
+ const left = parent.children[i - 1];
355
+ const right = parent.children[i];
356
+ if (right.leaf) {
357
+ // 叶子节点:移动左叶最大键到右叶最小位置
358
+ const k = left.keys.pop();
359
+ const v = left.values.pop();
360
+ right.keys.unshift(k);
361
+ right.values.unshift(v);
362
+ // 父节点分隔键应为左叶新的最大值(pop 后的最后一个键)
363
+ parent.keys[i - 1] = left.keys[left.keys.length - 1];
364
+ parent.values[i - 1] = left.values[left.values.length - 1];
365
+ } else {
366
+ right.keys.unshift(parent.keys[i - 1]);
367
+ right.values.unshift(parent.values[i - 1]);
368
+ parent.keys[i - 1] = left.keys.pop();
369
+ parent.values[i - 1] = left.values.pop();
370
+ right.children.unshift(left.children.pop());
324
371
  }
325
- node.children = kids;
326
- if (kids.length === 0) return null;
327
- if (kids.length === 1) return kids[0];
328
- node.keys = [];
329
- node.values = [];
330
- for (let i = 0; i < kids.length - 1; i++) {
331
- const m = this._getMax(kids[i]);
332
- node.keys.push(m.key);
333
- node.values.push(m.value);
372
+ }
373
+
374
+ _borrowFromRight(parent, i) {
375
+ const left = parent.children[i];
376
+ const right = parent.children[i + 1];
377
+ if (left.leaf) {
378
+ // 叶子节点:用被移动的键而非旧分隔键,避免重复
379
+ const k = right.keys.shift();
380
+ const v = right.values.shift();
381
+ left.keys.push(k);
382
+ left.values.push(v);
383
+ parent.keys[i] = k;
384
+ parent.values[i] = v;
385
+ } else {
386
+ left.keys.push(parent.keys[i]);
387
+ left.values.push(parent.values[i]);
388
+ parent.keys[i] = right.keys.shift();
389
+ parent.values[i] = right.values.shift();
390
+ left.children.push(right.children.shift());
334
391
  }
335
- return node;
336
392
  }
337
393
 
338
- _relinkLeaves() {
339
- let head = this._root;
340
- while (head && !head.leaf) head = head.children[0];
341
- let prev = null;
342
- let node = head;
343
- while (node) {
344
- if (prev) prev.next = node;
345
- prev = node;
346
- node = node.next;
394
+ /**
395
+ * 修复内部节点的分隔键:keys[i] 必须是一个**确实存在于 child[i] 里**的键。
396
+ *
397
+ * 背景:分隔键的约定是「左子树的最大键」(叶子上存真实数据,父节点存副本)。
398
+ * remove() 删掉叶子上的某个键后,如果那个键正好是父节点的分隔键副本,
399
+ * 分隔键就会指向一个已经不存在的值 —— 树还能路由对,但内部键不再严格递增,
400
+ * 下一次 borrow/merge 就会把它复制成重复键,直接破坏结构。
401
+ *
402
+ * 最小复现(BTree(4)):
403
+ * +28:1 +24:4 +0:0 +14:2 -24:4 +25:4 +24:2 +28:1 -26:2
404
+ * 前 8 步都正常,第 9 步把 keys=[24,25] 变成 [24,24]。
405
+ */
406
+ _fixSeparators(node) {
407
+ if (node.leaf) return;
408
+ for (let i = 0; i < node.keys.length; i++) {
409
+ const m = this._getMax(node.children[i]);
410
+ node.keys[i] = m.key;
411
+ node.values[i] = m.value;
347
412
  }
348
- if (prev) prev.next = null;
413
+ for (const c of node.children) this._fixSeparators(c);
349
414
  }
350
415
 
351
416
  _getMax(node) {
@@ -354,6 +419,27 @@ class BTree {
354
419
  return { key: node.keys[i], value: node.values[i] };
355
420
  }
356
421
 
422
+ _mergeChildren(parent, index) {
423
+ const left = parent.children[index];
424
+ const right = parent.children[index + 1];
425
+ const midKey = parent.keys[index];
426
+ const midVal = parent.values[index];
427
+
428
+ // 叶子节点:分隔键是叶子的副本(已在 left 或 right 中),不能重复压入
429
+ if (!left.leaf) {
430
+ left.keys.push(midKey);
431
+ left.values.push(midVal);
432
+ }
433
+ for (const __e of right.keys) left.keys.push(__e);
434
+ for (const __e of right.values) left.values.push(__e);
435
+ if (!left.leaf) for (const __e of right.children) left.children.push(__e);
436
+ if (left.leaf) left.next = right.next;
437
+
438
+ parent.keys.splice(index, 1);
439
+ parent.values.splice(index, 1);
440
+ parent.children.splice(index + 1, 1);
441
+ }
442
+
357
443
  // ============================================================
358
444
  // 遍历
359
445
  // ============================================================
@@ -0,0 +1,208 @@
1
+ // © Vexify 2026 All Rights Reserved.
2
+ /**
3
+ * jsql 1.0.0 兼容层(怀旧 API)
4
+ *
5
+ * 1.0.0 的公开 API 是**全同步**的:
6
+ *
7
+ * const jsql = require('jsql-neo');
8
+ * const db = new jsql.Database('mydb.json');
9
+ * db.createTable('users', {
10
+ * id: { type: 'integer', primaryKey: true, autoIncrement: true },
11
+ * name: { type: 'string', required: true },
12
+ * age: { type: 'integer' }
13
+ * });
14
+ * db.users.insert({ name: 'Alice', age: 25 });
15
+ * db.users.where({ age: { $gte: 18 } }).orderBy('age', 'desc').limit(10).get();
16
+ *
17
+ * v6 引擎本身已经把 Table / Query 的全部方法做成同步的,只有
18
+ * `Database.createTable()` / `Database.dropTable()` 变成了 async。
19
+ * 这层只做两件事:
20
+ *
21
+ * 1. 同步版 createTable/dropTable(内部走 _createTableSync/_dropTableSync);
22
+ * 2. 把 `db.<表名>` 的取值属性镜像到包装对象上,保证老代码直接可用。
23
+ *
24
+ * 除了这两个方法,其余行为完全等同 v6:拿到的 Table 就是 lib/table.js 的
25
+ * Table,查询链就是 lib/query.js 的 Query,不存在第二套实现。
26
+ *
27
+ * 注意:这是兼容层,不是"降级"。它不会关闭 v6 的任何能力,
28
+ * 需要 async API 时通过 `db.raw` 拿到底层 Database 即可。
29
+ */
30
+
31
+ const Database = require('../database');
32
+ const Table = require('../table');
33
+
34
+ /**
35
+ * 把 1.0.0 的简写 schema('string' / 'integer' 等)解析成对象。
36
+ * 与 Database#createTable 里的解析逻辑保持一致,避免两处语义漂移。
37
+ */
38
+ function parseShorthand(schema) {
39
+ if (typeof schema !== 'string') return schema || {};
40
+ const parsed = {};
41
+ for (const seg of schema.split(',')) {
42
+ const trimmed = seg.trim();
43
+ if (!trimmed) continue;
44
+ const parts = trimmed.split(/\s+/);
45
+ const field = parts.shift();
46
+ parsed[field] = parts.join(' ').toLowerCase().trim() || 'string';
47
+ }
48
+ return parsed;
49
+ }
50
+
51
+ /** 深拷贝(优先 structuredClone,退化到 JSON) */
52
+ function deepClone(value) {
53
+ try {
54
+ if (typeof structuredClone === 'function') return structuredClone(value);
55
+ } catch (e) { /* 含函数/类实例时退化 */ }
56
+ return JSON.parse(JSON.stringify(value));
57
+ }
58
+
59
+ /**
60
+ * 从行数据推断 schema(仅用于 import 时表还不存在的场景)。
61
+ * 1.0.0 的 export() 只吐出 rows,不带 schema,所以必须能反推。
62
+ */
63
+ function inferSchema(rows) {
64
+ const schema = {};
65
+ if (!Array.isArray(rows) || rows.length === 0) return schema;
66
+ const sample = rows[0];
67
+ for (const [field, value] of Object.entries(sample)) {
68
+ let type = 'string';
69
+ if (typeof value === 'number') type = Number.isInteger(value) ? 'integer' : 'float';
70
+ else if (typeof value === 'boolean') type = 'boolean';
71
+ else if (Array.isArray(value)) type = 'array';
72
+ else if (value !== null && typeof value === 'object') type = 'object';
73
+ schema[field] = { type };
74
+ }
75
+ // 1.0.0 的 export() 不带 schema,自增主键信息会丢。
76
+ // 常见约定:`id` 且整型唯一 —— 还原成 primaryKey + autoIncrement,
77
+ // 否则 import 后继续 insert 会得到 id=undefined(老代码会当场炸)。
78
+ if (schema.id && schema.id.type === 'integer') {
79
+ const ids = rows.map(r => r.id);
80
+ const unique = new Set(ids).size === ids.length && ids.every(v => typeof v === 'number');
81
+ if (unique) {
82
+ schema.id.primaryKey = true;
83
+ schema.id.autoIncrement = true;
84
+ }
85
+ }
86
+ return schema;
87
+ }
88
+
89
+ class V1Database {
90
+ /**
91
+ * @param {string|null} [filePath] 文件路径;null / ':memory:' 为内存模式
92
+ * @param {object} [options] 透传给 v6 Database
93
+ */
94
+ constructor(filePath, options = {}) {
95
+ this._db = new Database(filePath, options);
96
+ // 文件模式导入或目录模式加载时,表已经存在 —— 同样要把
97
+ // db.<表名> 属性镜像到包装对象上,否则老代码 `db.users` 会 undefined。
98
+ for (const name of this._db.getTables()) this._bindTable(name);
99
+ }
100
+
101
+ /** 底层 v6 Database(需要 async / 高级能力时用) */
102
+ get raw() {
103
+ return this._db;
104
+ }
105
+
106
+ _bindTable(name) {
107
+ if (typeof name !== 'string' || !name) return;
108
+ // 内部字段绝不覆盖(表名叫 _db 时不能把引擎句柄删掉)
109
+ if (name === '_db') return;
110
+ // 表名撞上类方法(如叫 "save")时不覆盖方法,保持 1.0.0 的边界行为。
111
+ if (Object.prototype.hasOwnProperty.call(V1Database.prototype, name)) return;
112
+ if (Object.prototype.hasOwnProperty.call(this, name)) {
113
+ delete this[name];
114
+ }
115
+ Object.defineProperty(this, name, {
116
+ get: () => this._db[name],
117
+ enumerable: true,
118
+ configurable: true
119
+ });
120
+ }
121
+
122
+ // ============================================================
123
+ // 表管理(1.0.0 语义:同步)
124
+ // ============================================================
125
+
126
+ /**
127
+ * 同步建表。
128
+ * @param {string} name
129
+ * @param {object|string} schema
130
+ * @returns {Table}
131
+ */
132
+ createTable(name, schema) {
133
+ const table = this._db._createTableSync(name, parseShorthand(schema));
134
+ this._bindTable(name);
135
+ return table;
136
+ }
137
+
138
+ /**
139
+ * 同步删表(表不存在时静默返回,符合 1.0.0 的宽松行为)。
140
+ * @param {string} name
141
+ */
142
+ dropTable(name) {
143
+ if (this._db.hasTable(name)) this._db._dropTableSync(name);
144
+ if (name !== '_db' && Object.prototype.hasOwnProperty.call(this, name)) delete this[name];
145
+ }
146
+
147
+ hasTable(name) {
148
+ return this._db.hasTable(name);
149
+ }
150
+
151
+ getTable(name) {
152
+ return this._db[name];
153
+ }
154
+
155
+ getTables() {
156
+ return this._db.getTables();
157
+ }
158
+
159
+ // ============================================================
160
+ // 持久化(与 v6 一致,均为同步)
161
+ // ============================================================
162
+
163
+ save() {
164
+ return this._db.save();
165
+ }
166
+
167
+ close() {
168
+ return this._db.close();
169
+ }
170
+
171
+ export() {
172
+ const data = this._db.export();
173
+ // 1.0.0 的 export() 是快照:调用方会存起来 / 发出去 / 过一会儿再 import。
174
+ // v6 的 Table#toJSON() 直接返回 this._rows,原样透传的话后续 insert/update
175
+ // 会同步改写刚导出的"快照"(import 到新库后两个库还共享同一个数组)。
176
+ // 这里做一次深拷贝,保持导出结果的不可变语义。
177
+ const snapshot = {};
178
+ for (const [name, rows] of Object.entries(data)) {
179
+ snapshot[name] = Array.isArray(rows) ? deepClone(rows) : rows;
180
+ }
181
+ return snapshot;
182
+ }
183
+
184
+ /**
185
+ * 导入数据。1.0.0 的 export() 只保留 rows,表不存在时按首行推断 schema 建表,
186
+ * 保证 export → import 往返可用。
187
+ */
188
+ import(data) {
189
+ if (!data || typeof data !== 'object') return this;
190
+ for (const [name, rows] of Object.entries(data)) {
191
+ if (!this._db.hasTable(name)) {
192
+ this.createTable(name, inferSchema(rows));
193
+ }
194
+ // 再拷贝一次:避免导入方后续修改自己的数据时串改库内行
195
+ this._db[name]._loadRows(Array.isArray(rows) ? deepClone(rows) : []);
196
+ }
197
+ this._db._markDirtyLegacy && this._db._markDirtyLegacy();
198
+ return this;
199
+ }
200
+ }
201
+
202
+ module.exports = {
203
+ Database: V1Database,
204
+ Table,
205
+ // 便于测试 / 高级用法
206
+ V1Database,
207
+ _inferSchema: inferSchema
208
+ };
package/lib/database.js CHANGED
@@ -1344,11 +1344,15 @@ class Database {
1344
1344
  startedAt: Date.now()
1345
1345
  };
1346
1346
 
1347
- // 无论隔离级别都要保存完整快照:ROLLBACK 必须撤销本事务写入的行。
1348
- // 此前 READ_COMMITTED 只记 autoIncrement,回滚后行原样残留(静默不撤销)。
1349
- // 隔离级别只影响「读取时能否看到其他事务的已提交数据」(这里是单连接内嵌库,
1350
- // 不涉及并发可见性),不应影响回滚能力。
1351
- this._transaction.snapshot = this._captureTxSnapshot();
1347
+ if (level === 'REPEATABLE_READ') {
1348
+ // 快照:保存当前所有数据(深拷贝,保证嵌套对象可回滚)
1349
+ this._transaction.snapshot = this._captureTxSnapshot();
1350
+ } else {
1351
+ // READ_COMMITTED:只保存 autoIncrement
1352
+ for (const [name, table] of Object.entries(this._tables)) {
1353
+ this._transaction.autoIncrements[name] = table._autoIncrement;
1354
+ }
1355
+ }
1352
1356
 
1353
1357
  this._savepointStack = [];
1354
1358
  this._walWrite({ op: 'begin', level });
@@ -1370,6 +1374,12 @@ class Database {
1370
1374
 
1371
1375
  if (this._transaction.snapshot) {
1372
1376
  this._restoreTxSnapshot(this._transaction.snapshot);
1377
+ } else {
1378
+ // READ_COMMITTED:没有行快照,只能退回自增值
1379
+ for (const [name, table] of Object.entries(this._tables)) {
1380
+ const v = this._transaction.autoIncrements[name];
1381
+ if (v !== undefined) table._autoIncrement = v;
1382
+ }
1373
1383
  }
1374
1384
 
1375
1385
  this._walWrite({ op: 'rollback', at: this._transaction.startedAt });
@@ -1847,8 +1857,8 @@ class Database {
1847
1857
  let rows = table._applyFilterOptimized(table._getVisibleRows(), filter || {});
1848
1858
  if (limit > 0) rows = rows.slice(offset, offset + limit);
1849
1859
  else if (offset > 0) rows = rows.slice(offset);
1850
- // P1-11: 返回深拷贝,防止调用方修改(含嵌套对象/数组)影响内部数据
1851
- rows = rows.map(r => Table.cloneRow(r));
1860
+ // P1-11: 返回浅拷贝,防止调用方修改影响内部数据
1861
+ rows = rows.map(r => ({ ...r }));
1852
1862
  this._touchTable(tableName);
1853
1863
  await this._runHooks('afterFind', [tableName, { filter, opts }, rows]);
1854
1864
  return rows;
@@ -1880,8 +1890,8 @@ class Database {
1880
1890
  const row = this._resolveId(table, id);
1881
1891
  // P1-4: 软删除行对普通读取不可见
1882
1892
  if (row && row._deleted) return null;
1883
- // P1-11: 返回深拷贝,防止调用方修改(含嵌套对象/数组)影响内部数据
1884
- const result = row ? Table.cloneRow(row) : null;
1893
+ // P1-11: 返回浅拷贝,防止调用方修改影响内部数据
1894
+ const result = row ? { ...row } : null;
1885
1895
  this._touchTable(tableName);
1886
1896
  await this._runHooks('afterFind', [tableName, { id }, result]);
1887
1897
  return result;
@@ -1892,7 +1902,7 @@ class Database {
1892
1902
  if (!table) throw createError('ER_NO_SUCH_TABLE', tableName);
1893
1903
  if (!(await this._runHooks('beforeFind', [tableName, { ids }]))) return null;
1894
1904
  // P1-4: 过滤掉软删除行;P1-11: 返回浅拷贝
1895
- const rows = ids.map(id => this._resolveId(table, id)).filter(r => r !== null && !r._deleted).map(r => Table.cloneRow(r));
1905
+ const rows = ids.map(id => this._resolveId(table, id)).filter(r => r !== null && !r._deleted).map(r => ({ ...r }));
1896
1906
  this._touchTable(tableName);
1897
1907
  await this._runHooks('afterFind', [tableName, { ids }, rows]);
1898
1908
  return rows;
@@ -1938,9 +1948,6 @@ class Database {
1938
1948
  const clash = table._rows.find(r => r !== row && r[field] === nv);
1939
1949
  if (clash) throw createError('ER_DUP_ENTRY', `Duplicate entry '${nv}' for key '${field}'`);
1940
1950
  }
1941
- // 主键/必填列的 NULL 校验:updateById 此前只做类型校验与重复检查,
1942
- // `UPDATE t SET id = NULL` 会被静默写入(主键行变成 NULL 主键)。
1943
- table._validateConstraints({ ...row, ...validated }, table._rows.indexOf(row));
1944
1951
  Object.assign(row, validated);
1945
1952
  if (pk) this._walWrite({ op: 'update', table: tableName, pk, id, row: { ...validated } });
1946
1953
  this._emit('update', { table: tableName, id, data: validated });
@@ -1959,9 +1966,7 @@ class Database {
1959
1966
  for (const [id, data] of pairs) {
1960
1967
  const row = this._resolveId(table, id);
1961
1968
  if (row) {
1962
- const validated = table._validateDataTypes({ ...data }, true);
1963
- table._validateConstraints({ ...row, ...validated }, table._rows.indexOf(row));
1964
- Object.assign(row, validated);
1969
+ Object.assign(row, data);
1965
1970
  count++;
1966
1971
  if (pk) this._walWrite({ op: 'update', table: tableName, pk, id, row: { ...data } });
1967
1972
  }
package/lib/errors.js CHANGED
@@ -81,10 +81,22 @@ function createError(codeKey, ...args) {
81
81
  return new JSQL_Error('UNKNOWN', `Unknown error: ${codeKey}`, { codeKey, args });
82
82
  }
83
83
 
84
- // 单趟替换所有 %s:既避免参数值里本身含有 %s 时被后续替换再次消费造成串位,
85
- // 也避免实参不足时消息里残留裸 %s。
86
- let idx = 0;
87
- const msg = template.msg.replace(/%s/g, () => (idx < args.length ? String(args[idx++]) : ''));
84
+ // 单次扫描替换:占位符按模板顺序消费,参数内容原样写入。
85
+ //
86
+ // 旧实现是 `for (args) msg = msg.replace('%s', () => String(arg))`,
87
+ // 每一轮都在**上一轮的结果**上再找第一个 '%s' —— 参数里自带的 '%s'
88
+ // 会被当成下一个占位符吃掉,且顺序错位:
89
+ // createError('ER_BAD_FIELD_ERROR', 'X%sY', 'Z')
90
+ // → "Unknown column 'XZY' in '%s'"(X%sY 被改写成 XZY)
91
+ // 正确结果是 "Unknown column 'X%sY' in 'Z'"。
92
+ let i = 0;
93
+ let msg = template.msg.replace(/%s/g, () => (i < args.length ? String(args[i++]) : '%s'));
94
+ if (args.length > 0 && !/%s/.test(template.msg)) {
95
+ // 模板里没有占位符(如 ER_NO_TRANSACTION)时,参数不能被静默丢弃:
96
+ // 追加在后面,至少保留调用方给出的上下文。
97
+ const rest = args.map(a => String(a)).join(' ');
98
+ msg = rest ? `${msg} (${rest})` : msg;
99
+ }
88
100
 
89
101
  return new JSQL_Error(template.code, msg, { args });
90
102
  }
package/lib/migrate.js CHANGED
@@ -30,12 +30,8 @@ function normalizeSchema(schema) {
30
30
  if (d.type === 'varchar' || d.type === 'text' || d.type === 'char') d.type = 'string';
31
31
  if (d.type === 'double' || d.type === 'real' || d.type === 'decimal' || d.type === 'numeric') d.type = 'float';
32
32
  if (d.type === 'bool') d.type = 'boolean';
33
- // 保留用户显式配置的 length;maxLength 只是别名,仅在 length 未给出时回填。
34
- // 此前无条件 delete d.length 再仅从 maxLength 恢复,导致显式 length 丢失。
35
- if (d.maxLength !== undefined) {
36
- if (d.length === undefined) d.length = d.maxLength;
37
- delete d.maxLength;
38
- }
33
+ delete d.length;
34
+ if (d.maxLength) { d.length = d.maxLength; delete d.maxLength; }
39
35
  out[name] = d;
40
36
  }
41
37
  return out;