jsql-neo 6.3.3 → 6.3.4

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.0.1** — official release build · [github.com/vexify-org/JSQL-neo](https://github.com/vexify-org/JSQL-neo)
6
+ > **v6.3.4** — 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)
@@ -233,7 +233,7 @@ npm install && npm run build # option 3: from source
233
233
  Verify:
234
234
 
235
235
  ```bash
236
- node -e "console.log(require('jsql-neo/package.json').version)" # 6.0.1
236
+ node -e "console.log(require('jsql-neo/package.json').version)" # 6.3.4
237
237
  ```
238
238
 
239
239
  ### 30-second demo
@@ -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.0.1
1473
+ jsql-neo v6.3.4
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.0.1`.
1494
+ The status bar shows: `db=<name> dialect=<d> mode=<tui|batch> ver=6.3.4`.
1495
1495
 
1496
1496
  ### Keyboard shortcuts
1497
1497
 
@@ -1705,10 +1705,27 @@ db.createIndex('orders', ['status', 'created_at']);
1705
1705
 
1706
1706
  ### WAL & crash recovery
1707
1707
 
1708
- - Every write appends to a transaction log (tlog) before touching memory
1709
- - `flush()` / auto-save: write snapshot → clear log on success
1710
- - Startup: load last snapshot → replay log if present → consistent state
1711
- - Corrupt/truncated logs degrade to the last good snapshot with a warning
1708
+ > **Requires an explicit opt-in:** `new Database(path, { wal: true })`. Off by default.
1709
+
1710
+ - Every write is appended to a **write-ahead log** (`<file>.wal`) *before* the snapshot is written
1711
+ - The log is **append-only JSONL** with `fsync` per record — a crash mid-write can lose at most the last record, never the whole log
1712
+ - Logged operations: `createTable` / `dropTable` / `insert` / `update` / `removeById` (transaction markers are recorded for observability)
1713
+ - `save()` / `flush()` / auto-save: write snapshot → **on success** clear the log (checkpoint)
1714
+ - Startup: load last snapshot → **replay the log in order** → checkpoint immediately
1715
+ - A half-written trailing record (torn write) is discarded; the rest still replays
1716
+ - Recovery checkpoints to disk immediately, so a second crash cannot lose replayed data
1717
+
1718
+ **Honest limits:**
1719
+
1720
+ - Rows in tables **without a primary key** are not logged (replay could not deduplicate them). Add a primary key if you need crash recovery on a table.
1721
+ - `fsync` is best-effort; on filesystems that reject it the log may sit in page cache.
1722
+ - This is a **single-process** durability guarantee. It does not cover power loss on exotic storage hardware, nor concurrent writes from multiple processes (a `.lock` file guards the latter).
1723
+
1724
+ ```js
1725
+ const db = new Database('./data.jsql', { wal: true });
1726
+ await db.insert('users', { id: 1, name: 'Alice' });
1727
+ // crash here (kill -9) → data is still recovered on next open
1728
+ ```
1712
1729
 
1713
1730
  ### Snapshots & compression
1714
1731
 
@@ -4158,7 +4175,7 @@ Data dir: /root/.jsql-neo/data
4158
4175
 
4159
4176
  ```bash
4160
4177
  $ jsql version
4161
- jsql-neo v6.0.1
4178
+ jsql-neo v6.3.4
4162
4179
  engine: native (napi) | wasm | js
4163
4180
  node: v22.0.0
4164
4181
  platform: linux x64
@@ -4603,16 +4620,39 @@ db.createIndex('orders', ['status', 'created_at']);
4603
4620
 
4604
4621
  ### WAL 与崩溃恢复 WAL & crash recovery
4605
4622
 
4606
- - 每次写操作先追加变更日志(tlog),再应用内存
4607
- - `flush()` / 自动保存时:写快照 → 成功后清空日志
4608
- - 启动时若检测到快照 + 未清空的日志:**重放日志**恢复到最近一致状态
4609
- - 日志截断/损坏时自动降级为加载最后完整快照并告警
4623
+ > **需显式开启**:`new Database(path, { wal: true })`,默认关闭。
4624
+ >
4625
+ > 6.3.4 起本节描述的是**真实实现**。此前(≤6.3.3)文档承诺的「重放日志恢复」
4626
+ > 并不存在 —— `_recoverFromWAL()` 只删日志不回放,且构造函数在数据文件不存在时
4627
+ > 直接短路,导致开启 `wal:true` 后 `kill -9` 必然丢数据。已修复并补齐回归测试。
4628
+
4629
+ - 每次写操作先 **append 追加**到预写日志(`<file>.wal`),再落快照
4630
+ - 日志为 **append-only JSONL**,每条记录 `fsync`:崩溃最多丢最后一条,不会损坏整个日志
4631
+ - 记录的操作:`createTable` / `dropTable` / `insert` / `update` / `removeById`
4632
+ (事务标记 `begin/commit/rollback` 仅记录以供观测)
4633
+ - `save()` / `flush()` / 自动保存:**写快照成功后才**清空日志(检查点)
4634
+ - 启动流程:加载最后快照 → **按顺序回放日志** → 立即做检查点落盘
4635
+ - 崩溃写半的尾行会被丢弃,其余记录照常回放
4636
+ - 回放后立即落盘,因此二次崩溃不会再次丢失已回放的数据
4637
+
4638
+ **诚实说明边界**:
4639
+
4640
+ - **无主键的表不记录行数据**(回放时无法去重,可能产生重复行)。需要崩溃恢复的表请加主键。
4641
+ - `fsync` 为 best-effort;部分文件系统不支持时日志可能仍在页缓存中。
4642
+ - 这是**单进程**持久性保证,不覆盖特殊硬件下的掉电,也不支持多进程并发写
4643
+ (后者由 `.lock` 文件互斥保护)。
4610
4644
 
4611
4645
  ```
4612
4646
  启动流程:
4613
- 加载最后快照 ──► 检测 tlog ──► 有? ──► 重放 ──► 就绪
4614
- │ │
4615
- └── 无 ─────────┘
4647
+ 加载最后快照 ──► 检测 WAL ──► 有? ──► 按序回放 ──► 立即检查点 ──► 就绪
4648
+ │ │
4649
+ └── 无 ────────────────────────┘
4650
+ ```
4651
+
4652
+ ```js
4653
+ const db = new Database('./data.jsql', { wal: true });
4654
+ await db.insert('users', { id: 1, name: 'Alice' });
4655
+ // 在此 kill -9 → 下次打开数据仍在
4616
4656
  ```
4617
4657
 
4618
4658
  ### 快照与压缩 Snapshots & compression
@@ -5235,7 +5275,7 @@ Apache License
5235
5275
 
5236
5276
  *JSQL-NEO — One engine to rule them all. MySQL. PostgreSQL. MongoDB. Redis. SQL. TypeScript. The browser.*
5237
5277
 
5238
- *文档版本:v6.0.1 · 最后更新:2026-09-27*
5278
+ *文档版本:v6.3.4 · 最后更新:2026-09-27*
5239
5279
 
5240
5280
  ---
5241
5281
 
@@ -7007,7 +7047,7 @@ Usage: jsql version
7007
7047
 
7008
7048
  输出版本与环境信息:
7009
7049
 
7010
- jsql-neo v6.0.1
7050
+ jsql-neo v6.3.4
7011
7051
  engine: native (napi) | wasm | js
7012
7052
  node: v22.0.0
7013
7053
  platform: linux x64
@@ -7663,7 +7703,7 @@ CI(GitHub Actions)矩阵:`node 20/22` × `linux/macos/windows` × `native/
7663
7703
 
7664
7704
  *JSQL-NEO — One engine to rule them all. MySQL. PostgreSQL. MongoDB. Redis. SQL. TypeScript. The browser.*
7665
7705
 
7666
- *文档版本:v6.0.1 · 共 19 个附录 · 最后更新:2026-09-27*
7706
+ *文档版本:v6.3.4 · 共 19 个附录 · 最后更新:2026-09-27*
7667
7707
 
7668
7708
  ---
7669
7709
 
@@ -8270,4 +8310,4 @@ npm test
8270
8310
 
8271
8311
  *JSQL-NEO — One engine to rule them all. MySQL. PostgreSQL. MongoDB. Redis. SQL. TypeScript. The browser.*
8272
8312
 
8273
- *文档版本:v6.0.1 · 附录 A–Z · 全文 6000+ 行 · 最后更新:2026-09-27*
8313
+ *文档版本:v6.3.4 · 附录 A–Z · 全文 6000+ 行 · 最后更新:2026-09-27*
package/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * JSQL-NEO v6.0.0 — Rust-Powered Embedded Database (WASM + HTTP)
2
+ * JSQL-NEO v6.3.4 — Rust-Powered Embedded Database (WASM + HTTP)
3
3
  *
4
4
  * @example
5
5
  * const jsql = require('jsql-neo');
@@ -30,10 +30,46 @@ const { TUIShell, createTUI } = require('./lib/tui');
30
30
 
31
31
  /**
32
32
  * 全局注入:把项目内 `require('mysql2')` 全部替换为 jsql-neo 内存引擎兼容层。
33
- * 调用一次后,TypeORM / Drizzle / MikroORM / Kysely 等直接依赖 mysql2 的库
34
- * 无需改代码即可享受本地内存速度。
33
+ * 该函数**不再**改写 `require.cache`。
34
+ *
35
+ * 背景:旧实现会遍历 node_modules,把 `mysql2/index.js` 与 `mysql2/promise.js`
36
+ * 的 require.cache 条目替换为内存兼容层。由于它靠 `process.cwd()` 猜测路径,
37
+ * 命中哪个 node_modules 完全取决于启动目录,且 mysql2 通常尚未加载,
38
+ * 于是被静默替换 —— 真实 MySQL 连接悄无声息地改查内存库且不报错,
39
+ * 表现为「本地跑通、上线全错」。这是不可接受的隐式副作用。
40
+ *
41
+ * 现在请显式选择注入方式:
42
+ *
43
+ * 1) 直接使用兼容层(推荐,零副作用):
44
+ * const { mysql2 } = require('jsql-neo');
45
+ * const conn = await mysql2.createConnection(opts);
46
+ *
47
+ * 2) 让某个依赖改用兼容层(只对指定模块生效,不污染全局缓存):
48
+ * const { injectMySQLCompat } = require('jsql-neo');
49
+ * injectMySQLCompat(require('typeorm')); // 只改写 typeorm 自己的引用
50
+ *
51
+ * 3) 确实需要全局劫持(危险,仅限测试环境):
52
+ * enableMySQLCompat({ global: true, force: true });
53
+ * 需同时设置 JSQL_NEO_ALLOW_GLOBAL_HIJACK=1 作为显式确认。
54
+ *
55
+ * @param {object} [opts]
56
+ * @param {boolean} [opts.global=false] 是否允许改写 require.cache(需环境变量确认)
57
+ * @param {boolean} [opts.force=false] 是否覆盖已加载的真实 mysql2
58
+ * @returns {object} mysql2 兼容层
35
59
  */
36
- function enableMySQLCompat() {
60
+ function enableMySQLCompat(opts = {}) {
61
+ if (!opts || opts.global !== true) {
62
+ // 默认安全:什么都不做,仅返回兼容层供显式使用
63
+ return mysqlCompat;
64
+ }
65
+ if (process.env.JSQL_NEO_ALLOW_GLOBAL_HIJACK !== '1') {
66
+ throw new Error(
67
+ '[jsql-neo] 已拒绝全局劫持 require.cache:这会静默替换真实 mysql2,' +
68
+ '导致本地测试通过但生产连错库。\n' +
69
+ '如确需启用,请显式设置环境变量 JSQL_NEO_ALLOW_GLOBAL_HIJACK=1 并传 { global: true }。\n' +
70
+ '推荐改用显式注入:injectMySQLCompat(require(\'你的orm\')),或直接使用导出的 mysql2/createConnection。'
71
+ );
72
+ }
37
73
  const fs = require('fs');
38
74
  const path = require('path');
39
75
  const seen = new Set();
@@ -51,8 +87,7 @@ function enableMySQLCompat() {
51
87
  if (seen.has(resolved)) return;
52
88
  if (!fs.existsSync(resolved)) return;
53
89
  seen.add(resolved);
54
- // 不覆盖已加载的真实 mysql2,避免全局劫持副作用
55
- if (require.cache[resolved]) return;
90
+ if (require.cache[resolved] && !opts.force) return;
56
91
  require.cache[resolved] = { exports: mod, id: resolved, filename: resolved, loaded: true, children: [] };
57
92
  } catch (e) { /* ignore */ }
58
93
  };
@@ -63,6 +98,37 @@ function enableMySQLCompat() {
63
98
  return mysqlCompat;
64
99
  }
65
100
 
101
+ /**
102
+ * 把已加载模块自身持有的 `mysql2` 引用定向到 jsql-neo 兼容层。
103
+ *
104
+ * 与全局劫持不同,本函数**只修改传入模块对象上的引用**,
105
+ * 不触碰 require.cache,不影响进程内其他模块。
106
+ *
107
+ * @param {object} mod 目标模块的 exports(如 require('typeorm'))
108
+ * @returns {boolean} 是否成功改写
109
+ */
110
+ function injectMySQLCompat(mod) {
111
+ if (!mod || typeof mod !== 'object') return false;
112
+ let touched = false;
113
+ // 常见形态:模块把 createConnection / createPool 挂在自身或嵌套对象上
114
+ const patch = (target) => {
115
+ if (!target || typeof target !== 'object') return;
116
+ for (const key of ['createConnection', 'createPool']) {
117
+ if (typeof target[key] === 'function' && target[key] !== mysqlCompat[key]) {
118
+ target[key] = mysqlCompat[key];
119
+ touched = true;
120
+ }
121
+ }
122
+ };
123
+ patch(mod);
124
+ // 一层嵌套(drizzle / kysely 等常把驱动挂在子对象上)
125
+ for (const key of Object.keys(mod)) {
126
+ const v = mod[key];
127
+ if (v && typeof v === 'object' && v !== mod && !Array.isArray(v)) patch(v);
128
+ }
129
+ return touched;
130
+ }
131
+
66
132
  module.exports = {
67
133
  JSQL: WasmClient.JSQL,
68
134
  NativeJSQL: NativeClient.JSQL,
@@ -96,6 +162,7 @@ module.exports = {
96
162
  mysql: mysqlCompat,
97
163
  mysql2: mysqlCompat,
98
164
  enableMySQLCompat,
165
+ injectMySQLCompat,
99
166
  createMysqlServer,
100
167
  MysqlServer,
101
168
  // 迁移工具: mysqldump 导入 / JSON / CSV
package/lib/btree.js CHANGED
@@ -433,48 +433,12 @@ class BTree {
433
433
  /**
434
434
  * 批量加载:从已排序的 [key, rowIndex] 数组底部向上构建
435
435
  * O(n) 时间,适合大数据量批量构建
436
+ *
437
+ * @deprecated 已移除(6.3.4)。原因:本方法用 `child.keys[0]` 作为内部分隔键,
438
+ * 而 insert/_splitChild 路径的分隔键是「左叶最大键的副本」,两者语义不一致。
439
+ * 混用会导致 search/range 漏查或错查。当前索引构建统一走 insert()
440
+ * (见 table.js 的 rebuild 路径),本方法无任何调用方,故直接移除而非修复。
436
441
  */
437
- bulkLoad(sortedPairs) {
438
- if (sortedPairs.length === 0) { this.clear(); return; }
439
- const order = this._order;
440
- // 第一层:构建叶子节点
441
- const leaves = [];
442
- let i = 0;
443
- while (i < sortedPairs.length) {
444
- const node = new BTreeNode(true);
445
- const end = Math.min(i + order - 1, sortedPairs.length);
446
- for (; i < end; i++) {
447
- const [key, val] = sortedPairs[i];
448
- node.keys.push(key);
449
- node.values.push(Array.isArray(val) ? val : [val]);
450
- }
451
- leaves.push(node);
452
- }
453
- for (let i = 0; i < leaves.length - 1; i++) leaves[i].next = leaves[i + 1];
454
- if (leaves.length === 1) { this._root = leaves[0]; this._size = sortedPairs.length; return; }
455
- // 逐层向上构建内部节点
456
- let currentLevel = leaves;
457
- while (currentLevel.length > 1) {
458
- const parents = [];
459
- let j = 0;
460
- while (j < currentLevel.length) {
461
- const node = new BTreeNode(false);
462
- const end = Math.min(j + order, currentLevel.length);
463
- for (; j < end; j++) {
464
- if (node.children.length > 0) {
465
- const child = currentLevel[j];
466
- node.keys.push(child.keys[0]);
467
- node.values.push(child.values[0]);
468
- }
469
- node.children.push(currentLevel[j]);
470
- }
471
- parents.push(node);
472
- }
473
- currentLevel = parents;
474
- }
475
- this._root = currentLevel[0];
476
- this._size = sortedPairs.length;
477
- }
478
442
  }
479
443
 
480
444
  module.exports = BTree;