jsql-neo 6.3.10 → 6.4.0-beta.3

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.3.10** — official release build · [github.com/vexify-org/JSQL-neo](https://github.com/vexify-org/JSQL-neo) · [jsql.vexify.top](https://jsql.vexify.top)
6
+ > **v6.4.0-beta.3** — official release build · [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)
@@ -112,6 +112,27 @@ const db = new Database(':memory:');
112
112
  // same API as Node.js
113
113
  ```
114
114
 
115
+ ### 🦀 NativeJSQL
116
+
117
+ Pure Rust native bindings — the highest-performance way to run JSQL-NEO:
118
+
119
+ - **Zero abstraction overhead** — direct calls into the Rust core
120
+ - **Batch operations** — up to **1.54M rows/sec** peak throughput
121
+ - **Automatic memory management** with smart caching
122
+
123
+ **Install:**
124
+
125
+ ```bash
126
+ npm install jsql-neo
127
+ ```
128
+
129
+ **Usage:**
130
+
131
+ ```javascript
132
+ import { Database } from 'jsql-neo';
133
+ const db = new Database('mydb.jsql');
134
+ ```
135
+
115
136
  ### Storage modes
116
137
 
117
138
  | Mode | Usage | Use Case |
package/bin/jsql CHANGED
@@ -247,6 +247,96 @@ const cli = yaggs({ pkg: require('../package.json') })
247
247
  console.log(`jsql-neo Redis server on ${argv.host || '127.0.0.1'}:${argv.port != null ? argv.port : 6379}${argv['data-dir'] ? ' (data: ' + argv['data-dir'] + ')' : ''}`);
248
248
  setInterval(() => {}, 1 << 30);
249
249
  })
250
+ .command('plugin', 'Plugin registry management (list, search, install, uninstall, info)', (sub) => {
251
+ sub.option('json', { type: 'boolean', description: 'Output as JSON' });
252
+ sub.option('category', { type: 'string', description: 'Filter by category' });
253
+ sub.option('limit', { type: 'number', description: 'Max results per page' });
254
+ }, async (argv) => {
255
+ const pluginMgr = require('../src/cli/plugin');
256
+ const action = argv._[0];
257
+ const arg = argv._[1];
258
+ const useJson = !!argv.json;
259
+
260
+ const fail = (e) => { console.error(`Error: ${e.message}`); process.exitCode = 1; };
261
+ const out = (obj) => {
262
+ if (obj === undefined) return;
263
+ if (useJson) console.log(JSON.stringify(obj, null, 2));
264
+ else console.log(obj);
265
+ };
266
+
267
+ try {
268
+ switch (action) {
269
+ case 'list':
270
+ case 'ls': {
271
+ const opts = {};
272
+ if (argv.category) opts.category = argv.category;
273
+ if (argv.limit) opts.limit = argv.limit;
274
+ const plugins = await pluginMgr.listPlugins(opts);
275
+ out(useJson ? plugins : pluginMgr.formatPluginList(plugins));
276
+ break;
277
+ }
278
+ case 'search': {
279
+ if (!arg) throw new Error('Usage: jsql plugin search <query>');
280
+ const opts = {};
281
+ if (argv.category) opts.category = argv.category;
282
+ const data = await pluginMgr.searchPlugins(arg, opts);
283
+ // search 返回 { results: [...], query, total } 或 [...]
284
+ const plugins = Array.isArray(data) ? data : (data.results || []);
285
+ if (useJson) {
286
+ out(data);
287
+ } else {
288
+ if (!plugins.length) {
289
+ console.log(`No plugins found for '${arg}'.`);
290
+ } else {
291
+ console.log(`Found ${plugins.length} plugin(s) for '${arg}':`);
292
+ console.log(pluginMgr.formatPluginList(plugins.map(p => p.plugin || p)));
293
+ }
294
+ }
295
+ break;
296
+ }
297
+ case 'install': {
298
+ if (!arg) throw new Error('Usage: jsql plugin install <name>');
299
+ const manifest = await pluginMgr.installPlugin(arg);
300
+ out(useJson ? manifest : `Installed plugin '${manifest.name}' (${manifest.version})`);
301
+ break;
302
+ }
303
+ case 'uninstall':
304
+ case 'remove': {
305
+ if (!arg) throw new Error('Usage: jsql plugin uninstall <name>');
306
+ const r = pluginMgr.uninstallPlugin(arg);
307
+ out(useJson ? r : `Uninstalled plugin '${r.name}'.`);
308
+ break;
309
+ }
310
+ case 'info': {
311
+ if (!arg) throw new Error('Usage: jsql plugin info <name>');
312
+ const p = await pluginMgr.getPluginInfo(arg);
313
+ out(useJson ? p : pluginMgr.formatPluginInfo(p));
314
+ break;
315
+ }
316
+ case undefined:
317
+ // 无子命令 → 显示已安装插件
318
+ const installed = pluginMgr.listInstalled();
319
+ if (useJson) {
320
+ out(installed);
321
+ } else {
322
+ if (!installed.length) {
323
+ console.log('No plugins installed.');
324
+ console.log(`Plugin directory: ${pluginMgr.PLUGIN_DIR}`);
325
+ } else {
326
+ console.log(`Installed plugins (${installed.length}):`);
327
+ installed.forEach(p => {
328
+ console.log(` ${p.name.padEnd(24)} ${p.version || '?'}`);
329
+ });
330
+ }
331
+ }
332
+ break;
333
+ default:
334
+ throw new Error(`Unknown plugin action: ${action}`);
335
+ }
336
+ } catch (e) {
337
+ fail(e);
338
+ }
339
+ })
250
340
  .command('version', 'Show version', null, () => {
251
341
  console.log(require('../package.json').version);
252
342
  })
package/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * JSQL-NEO v6.3.10 — Rust-Powered Embedded Database (WASM + HTTP)
2
+ * JSQL-NEO v6.4.0-beta.3 — Rust-Powered Embedded Database (WASM + HTTP)
3
3
  *
4
4
  * @example
5
5
  * const jsql = require('jsql-neo');
package/lib/cache.js CHANGED
@@ -50,7 +50,7 @@ class Cache {
50
50
  if (typeof val === 'string') return 'string';
51
51
  if (val instanceof Set) return 'set';
52
52
  if (Array.isArray(val)) return val._type === 'hash' ? 'hash' : 'list';
53
- if (typeof val === 'object' && val._type === 'hash') return 'hash';
53
+ if (val !== null && typeof val === 'object' && val._type === 'hash') return 'hash';
54
54
  return 'string';
55
55
  }
56
56
  expire(key, seconds) {
@@ -85,7 +85,10 @@ class Cache {
85
85
  hset(key, field, value) {
86
86
  const k = String(key);
87
87
  let hash = this._getData().get(k);
88
- if (!hash || hash._type !== 'hash') { hash = { _type: 'hash' }; this._getData().set(k, hash); }
88
+ if (hash !== undefined && (typeof hash !== 'object' || hash._type !== 'hash')) {
89
+ throw new Error('WRONGTYPE Operation against a key holding the wrong kind of value');
90
+ }
91
+ if (!hash) { hash = { _type: 'hash' }; this._getData().set(k, hash); }
89
92
  const existed = hash[field] !== undefined;
90
93
  hash[field] = String(value);
91
94
  this._dirty = true;
@@ -134,7 +137,10 @@ class Cache {
134
137
  lpush(key, ...values) {
135
138
  const k = String(key);
136
139
  let list = this._getData().get(k);
137
- if (!list || !Array.isArray(list) || list._type === 'hash') { list = []; this._getData().set(k, list); }
140
+ if (list !== undefined && (!Array.isArray(list) || list._type === 'hash')) {
141
+ throw new Error('WRONGTYPE Operation against a key holding the wrong kind of value');
142
+ }
143
+ if (!list) { list = []; this._getData().set(k, list); }
138
144
  list.unshift(...values.map(String).reverse());
139
145
  this._dirty = true;
140
146
  return list.length;
@@ -142,7 +148,10 @@ class Cache {
142
148
  rpush(key, ...values) {
143
149
  const k = String(key);
144
150
  let list = this._getData().get(k);
145
- if (!list || !Array.isArray(list) || list._type === 'hash') { list = []; this._getData().set(k, list); }
151
+ if (list !== undefined && (!Array.isArray(list) || list._type === 'hash')) {
152
+ throw new Error('WRONGTYPE Operation against a key holding the wrong kind of value');
153
+ }
154
+ if (!list) { list = []; this._getData().set(k, list); }
146
155
  list.push(...values.map(String));
147
156
  this._dirty = true;
148
157
  return list.length;
@@ -185,7 +194,10 @@ class Cache {
185
194
  sadd(key, ...members) {
186
195
  const k = String(key);
187
196
  let set = this._getData().get(k);
188
- if (!set || !(set instanceof Set)) { set = new Set(); this._getData().set(k, set); }
197
+ if (set !== undefined && !(set instanceof Set)) {
198
+ throw new Error('WRONGTYPE Operation against a key holding the wrong kind of value');
199
+ }
200
+ if (!set) { set = new Set(); this._getData().set(k, set); }
189
201
  let count = 0;
190
202
  for (const m of members) { if (!set.has(String(m))) { set.add(String(m)); count++; } }
191
203
  if (count > 0) this._dirty = true;
@@ -223,10 +235,11 @@ class Cache {
223
235
  }
224
236
 
225
237
  flushall() {
238
+ const currentDb = this._db;
226
239
  for (let i = 0; i < this._dbs.length; i++) { if (this._dbs[i]) this._dbs[i].clear(); if (this._dbsExpires[i]) this._dbsExpires[i].clear(); }
227
- this._data = this._dbs[0] = new Map();
228
- this._expires = this._dbsExpires[0] = new Map();
229
- this._db = 0;
240
+ this._data = this._dbs[currentDb] = new Map();
241
+ this._expires = this._dbsExpires[currentDb] = new Map();
242
+ this._db = currentDb;
230
243
  this._dirty = true;
231
244
  return 'OK';
232
245
  }
package/lib/database.js CHANGED
@@ -56,6 +56,35 @@ function parseFieldShorthand(str) {
56
56
  return def;
57
57
  }
58
58
 
59
+ /**
60
+ * 深度相等比较两个行对象(用于无主键表的行匹配)。
61
+ * 剥离内部字段(_rid, _deleted)后逐字段比较。
62
+ */
63
+ function _rowDeepEqual(a, b) {
64
+ if (a === b) return true;
65
+ if (!a || !b || typeof a !== 'object' || typeof b !== 'object') return false;
66
+ const strip = (obj) => {
67
+ const out = {};
68
+ for (const [k, v] of Object.entries(obj)) {
69
+ if (k === '_rid' || k === '_deleted') continue;
70
+ out[k] = v;
71
+ }
72
+ return out;
73
+ };
74
+ const sa = strip(a), sb = strip(b);
75
+ const ka = Object.keys(sa), kb = Object.keys(sb);
76
+ if (ka.length !== kb.length) return false;
77
+ for (const k of ka) {
78
+ if (!Object.prototype.hasOwnProperty.call(sb, k)) return false;
79
+ const va = sa[k], vb = sb[k];
80
+ if (va === vb) continue;
81
+ if (va === null || va === undefined || vb === null || vb === undefined) return false;
82
+ if (typeof va === 'number' && typeof vb === 'number' && va === vb) continue;
83
+ if (String(va) !== String(vb)) return false;
84
+ }
85
+ return true;
86
+ }
87
+
59
88
  class Database {
60
89
  /**
61
90
  * @param {string} filePath - 数据库文件/目录路径,传 null 或 ':memory:' 使用内存模式
@@ -113,6 +142,12 @@ class Database {
113
142
  this._plugins = [];
114
143
  this._hooks = createHookRegistry();
115
144
  this._eventListeners = [];
145
+
146
+ // P1: {modules:true} 自动加载插件(mod.config + ~/.jsql/plugins/)
147
+ this._autoPlugins = options.modules !== false;
148
+ if (this._autoPlugins) {
149
+ this._loadAutoPlugins();
150
+ }
116
151
  this._tableNames = new Set(Object.keys(this._tables));
117
152
  this._migrations = [];
118
153
  this._observers = []; // 响应式观察者
@@ -1570,6 +1605,80 @@ class Database {
1570
1605
  return buildPluginContext(this, plugin);
1571
1606
  }
1572
1607
 
1608
+ /**
1609
+ * P1 修复:{modules:true} 自动加载插件。
1610
+ * 1. 通过 ModuleManager 读取 ~/.config/jsql/mod.config,应用已启用的模块
1611
+ * 2. 扫描 ~/.jsql/plugins/ 加载 registry 安装的插件
1612
+ */
1613
+ _loadAutoPlugins() {
1614
+ // 1. mod.config 中的模块
1615
+ try {
1616
+ const { ModuleManager } = require('./mod');
1617
+ new ModuleManager().applyTo(this);
1618
+ } catch (e) {
1619
+ if (process.env.JSQL_DEBUG) console.warn('[database] Failed to load mod.config:', e.message);
1620
+ }
1621
+ // 2. ~/.jsql/plugins/ 中的 registry 插件
1622
+ try {
1623
+ this._loadInstalledPlugins();
1624
+ } catch (e) {
1625
+ if (process.env.JSQL_DEBUG) console.warn('[database] Failed to load installed plugins:', e.message);
1626
+ }
1627
+ }
1628
+
1629
+ /**
1630
+ * 扫描 ~/.jsql/plugins/ 目录,加载已安装的 registry 插件。
1631
+ * 兼容两种布局:根级入口文件 + plugins/ 子目录入口文件。
1632
+ */
1633
+ _loadInstalledPlugins() {
1634
+ const os = require('os');
1635
+ const fs = require('fs');
1636
+ const PLUGIN_DIR = path.join(os.homedir(), '.jsql', 'plugins');
1637
+ if (!fs.existsSync(PLUGIN_DIR)) return;
1638
+ const entries = fs.readdirSync(PLUGIN_DIR, { withFileTypes: true });
1639
+ for (const entry of entries) {
1640
+ if (!entry.isDirectory()) continue;
1641
+ const dir = path.join(PLUGIN_DIR, entry.name);
1642
+ const manifestPath = path.join(dir, 'manifest.json');
1643
+ if (!fs.existsSync(manifestPath)) continue;
1644
+ let manifest;
1645
+ try {
1646
+ manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
1647
+ } catch { continue; }
1648
+ if (manifest.disabled) continue;
1649
+ const pluginName = manifest.name || entry.name;
1650
+ // 查找入口文件:manifest.main || manifest.entry || 'index.js'
1651
+ const entryFile = manifest.main || manifest.entry || 'index.js';
1652
+ // 兼容两种路径:根级 + plugins/ 子目录
1653
+ const candidates = [
1654
+ path.join(dir, entryFile),
1655
+ path.join(dir, entryFile + '.js'),
1656
+ path.join(dir, 'plugins', entryFile),
1657
+ path.join(dir, 'plugins', entryFile + '.js'),
1658
+ ];
1659
+ let entryPath = null;
1660
+ for (const c of candidates) {
1661
+ if (fs.existsSync(c) && fs.statSync(c).isFile()) {
1662
+ entryPath = c;
1663
+ break;
1664
+ }
1665
+ }
1666
+ if (!entryPath) continue;
1667
+ try {
1668
+ const resolved = require.resolve(entryPath);
1669
+ delete require.cache[resolved];
1670
+ const plugin = require(resolved);
1671
+ if (typeof plugin === 'function') {
1672
+ this.use({ name: pluginName, install: plugin });
1673
+ } else if (plugin && typeof plugin === 'object') {
1674
+ if (!plugin.name) plugin.name = pluginName;
1675
+ this.use(plugin);
1676
+ }
1677
+ } catch (e) {
1678
+ if (process.env.JSQL_DEBUG) console.warn('[database] Failed to load plugin', entry.name, e.message);
1679
+ }
1680
+ }
1681
+ }n
1573
1682
  on(event, fn) {
1574
1683
  return registerHook(this, event, fn);
1575
1684
  }
@@ -1702,7 +1811,15 @@ class Database {
1702
1811
  _resolveId(table, id) {
1703
1812
  const pk = this._pkOf(table);
1704
1813
  if (pk) return table._rows.find(r => r[pk] === id) || null;
1705
- return table._rows[id - 1] || null;
1814
+ // 无主键表:id 是行对象(可能是 find() 返回的浅拷贝)时,用内容匹配代替引用比较。
1815
+ // 做法:逐行比对所有字段值是否相等;是数字时当 1-based 行号。
1816
+ if (id && typeof id === 'object') {
1817
+ const keys = Object.keys(id);
1818
+ return table._rows.find(r => keys.every(k => r[k] === id[k])) || null;
1819
+ }
1820
+ const idx = Number(id);
1821
+ if (!isNaN(idx)) return table._rows[idx - 1] || null;
1822
+ return null;
1706
1823
  }
1707
1824
 
1708
1825
  find(tableName, filter, opts = {}) {
@@ -1710,7 +1827,8 @@ class Database {
1710
1827
  if (!table) throw createError('ER_NO_SUCH_TABLE', tableName);
1711
1828
  if (!this._runHooks('beforeFind', [tableName, { filter, opts }])) return [];
1712
1829
  const { limit, offset = 0 } = opts;
1713
- let rows = table._applyFilterOptimized(table._rows, filter || {});
1830
+ // P1-4: 软删除模式下不得泄露已删除行——只看可见行
1831
+ let rows = table._applyFilterOptimized(table._getVisibleRows(), filter || {});
1714
1832
  if (limit > 0) rows = rows.slice(offset, offset + limit);
1715
1833
  else if (offset > 0) rows = rows.slice(offset);
1716
1834
  // P1-11: 返回浅拷贝,防止调用方修改影响内部数据
@@ -1744,6 +1862,8 @@ class Database {
1744
1862
  if (!table) throw createError('ER_NO_SUCH_TABLE', tableName);
1745
1863
  if (!this._runHooks('beforeFind', [tableName, { id }])) return null;
1746
1864
  const row = this._resolveId(table, id);
1865
+ // P1-4: 软删除行对普通读取不可见
1866
+ if (row && row._deleted) return null;
1747
1867
  // P1-11: 返回浅拷贝,防止调用方修改影响内部数据
1748
1868
  const result = row ? { ...row } : null;
1749
1869
  this._touchTable(tableName);
@@ -1755,8 +1875,8 @@ class Database {
1755
1875
  const table = this._ensureTable(tableName);
1756
1876
  if (!table) throw createError('ER_NO_SUCH_TABLE', tableName);
1757
1877
  if (!this._runHooks('beforeFind', [tableName, { ids }])) return null;
1758
- // P1-11: 返回浅拷贝
1759
- const rows = ids.map(id => this._resolveId(table, id)).filter(r => r !== null).map(r => ({ ...r }));
1878
+ // P1-4: 过滤掉软删除行;P1-11: 返回浅拷贝
1879
+ const rows = ids.map(id => this._resolveId(table, id)).filter(r => r !== null && !r._deleted).map(r => ({ ...r }));
1760
1880
  this._touchTable(tableName);
1761
1881
  this._runHooks('afterFind', [tableName, { ids }, rows]);
1762
1882
  return rows;
@@ -1770,7 +1890,8 @@ class Database {
1770
1890
  const table = this._ensureTable(tableName);
1771
1891
  if (!table) throw createError('ER_NO_SUCH_TABLE', tableName);
1772
1892
  this._runHooks('beforeCount', [tableName]);
1773
- const n = table._rows.length;
1893
+ // P1-4: 软删除模式下只统计可见行
1894
+ const n = table._softDelete ? table._getVisibleRows().length : table._rows.length;
1774
1895
  this._touchTable(tableName);
1775
1896
  this._runHooks('afterCount', [tableName, n]);
1776
1897
  return n;
@@ -1783,7 +1904,8 @@ class Database {
1783
1904
  // P1-9: updateById 绕过类型校验——先校验再更新
1784
1905
  const validated = table._validateDataTypes({ ...data }, true);
1785
1906
  const row = this._resolveId(table, id);
1786
- if (!row) return { ok: false, error: 'not found' };
1907
+ // P1-4: 软删除行不可更新
1908
+ if (!row || row._deleted) return { ok: false, error: 'not found' };
1787
1909
  // 改主键/唯一列时要先确认新值不与其它行冲突。
1788
1910
  // 此前直接 Object.assign,导致
1789
1911
  // UPDATE t SET id = 1 WHERE id = 2 (t 已有 id=1)
@@ -1862,20 +1984,29 @@ class Database {
1862
1984
  if (table._primaryKey) {
1863
1985
  for (const id of ids) removed += table.removeById(id);
1864
1986
  } else {
1865
- // 无主键表没有稳定的行标识,_resolveId 只能把 id 当 1-based 行号,
1866
- // 而 _rowPkId 在无主键时回退到 row.id(业务列名)——
1867
- // 两者语义不一致,导致 DELETE ... WHERE id IN (3,5) 删错行。
1868
- // 这里先按「行对象 === id」尝试(SQL 层可以直接传行引用),
1869
- // 退化到行号解析;并一次性收集目标后统一移除,避免边删边移位。
1870
- const targets = [];
1987
+ // 无主键表没有稳定的行标识,P1-11 后 find() 返回浅拷贝({...r}),
1988
+ // 对象引用与 table._rows 里的原对象不同,不能用 Set.has(row) 做引用匹配。
1989
+ // _resolveId 对无主键表做 table._rows[id-1],传入对象时 id-1=NaN → undefined。
1990
+ // 所以这里对对象 id 做深度相等匹配,对值 id 走 _resolveId(1-based 行号)。
1871
1991
  const seen = new Set();
1872
- const push = (row) => { if (row && !seen.has(row)) { seen.add(row); targets.push(row); } };
1992
+ const targets = [];
1873
1993
  for (const id of ids) {
1874
- if (id && typeof id === 'object') { push(id); continue; }
1875
- push(this._resolveId(table, id));
1994
+ let rowIndex;
1995
+ if (id && typeof id === 'object') {
1996
+ // id 是行对象(find() 返回的浅拷贝),用深度相等匹配
1997
+ rowIndex = table._rows.findIndex(r => _rowDeepEqual(r, id));
1998
+ } else {
1999
+ // id 是值(1-based 行号),_resolveId 能处理
2000
+ const original = this._resolveId(table, id);
2001
+ if (original) rowIndex = table._rows.indexOf(original);
2002
+ }
2003
+ if (rowIndex >= 0 && !seen.has(rowIndex)) {
2004
+ seen.add(rowIndex);
2005
+ targets.push(rowIndex);
2006
+ }
1876
2007
  }
1877
2008
  if (targets.length) {
1878
- table._rows = table._rows.filter(r => !seen.has(r));
2009
+ table._rows = table._rows.filter((_, idx) => !seen.has(idx));
1879
2010
  removed = targets.length;
1880
2011
  }
1881
2012
  table._rebuildPKIndex();
@@ -1893,7 +2024,8 @@ class Database {
1893
2024
  if (!table) throw createError('ER_NO_SUCH_TABLE', tableName);
1894
2025
  if (!this._runHooks('beforeDelete', [tableName, [filter]])) return { ok: true, count: 0 };
1895
2026
  const before = table._rows.length;
1896
- const matched = table._applyFilter(table._rows, filter || {});
2027
+ // P1-4: 只匹配可见行(软删除行已“不存在”,不应被硬删操作波及)
2028
+ const matched = table._applyFilter(table._getVisibleRows(), filter || {});
1897
2029
  const matchSet = new Set(matched);
1898
2030
  table._rows = table._rows.filter(r => !matchSet.has(r));
1899
2031
  const removed = before - table._rows.length;
package/lib/date-types.js CHANGED
@@ -199,9 +199,12 @@ function validateTime(value, fieldName) {
199
199
  return value;
200
200
  }
201
201
  }
202
- // 负时间: -HH:MM:SS
202
+ // 负时间: -HH:MM:SS(同样校验范围)
203
203
  const neg = value.match(/^-(\d{2}):(\d{2}):(\d{2})$/);
204
- if (neg) return value;
204
+ if (neg) {
205
+ const h = parseInt(neg[1]), min = parseInt(neg[2]), sec = parseInt(neg[3]);
206
+ if (h >= 0 && h < 24 && min >= 0 && min < 60 && sec >= 0 && sec < 60) return value;
207
+ }
205
208
  }
206
209
  throw createError('ER_INVALID_DATE', fieldName, String(value));
207
210
  }
@@ -12,12 +12,19 @@ const SZ = {
12
12
 
13
13
  const BT = { FREE: 0, DATA: 1, IDX: 2 };
14
14
 
15
- const TYPE = { NUL:0, BOL:1, I32:2, F64:3, STR:4, BUF:5, ARR:6, OBJ:7 };
15
+ const TYPE = { NUL:0, BOL:1, I32:2, F64:3, STR:4, BUF:5, ARR:6, OBJ:7, I64:8 };
16
16
 
17
17
  function tag(v) {
18
18
  if (v===null||v===undefined) return TYPE.NUL;
19
19
  if (typeof v==='boolean') return TYPE.BOL;
20
- if (typeof v==='number') return Number.isInteger(v)?TYPE.I32:TYPE.F64;
20
+ if (typeof v==='number') {
21
+ if (Number.isInteger(v)) {
22
+ // P2-29-fix: 超出 int32 范围的整数用 I64 存储
23
+ if (v >= -0x80000000 && v <= 0x7FFFFFFF) return TYPE.I32;
24
+ return TYPE.I64;
25
+ }
26
+ return TYPE.F64;
27
+ }
21
28
  if (typeof v==='string') return TYPE.STR;
22
29
  if (v instanceof Buffer) return TYPE.BUF;
23
30
  return Array.isArray(v)?TYPE.ARR:TYPE.OBJ;
@@ -29,6 +36,7 @@ function enc(v) {
29
36
  case TYPE.NUL: return Buffer.from([t]);
30
37
  case TYPE.BOL: return Buffer.from([t, v?1:0]);
31
38
  case TYPE.I32: { const b=Buffer.alloc(5); b[0]=t; b.writeInt32LE(v,1); return b; }
39
+ case TYPE.I64: { const b=Buffer.alloc(9); b[0]=t; b.writeBigInt64LE(BigInt(v),1); return b; }
32
40
  case TYPE.F64: { const b=Buffer.alloc(9); b[0]=t; b.writeDoubleLE(v,1); return b; }
33
41
  case TYPE.STR: { const s=Buffer.from(v,'utf8'); const b=Buffer.alloc(5+s.length); b[0]=t; b.writeUInt32LE(s.length,1); s.copy(b,5); return b; }
34
42
  case TYPE.BUF: { const b=Buffer.alloc(5+v.length); b[0]=t; b.writeUInt32LE(v.length,1); v.copy(b,5); return b; }
@@ -43,6 +51,7 @@ function dec(buf, off) {
43
51
  case TYPE.NUL: return {v:null,off};
44
52
  case TYPE.BOL: return {v:buf[off]!==0,off:off+1};
45
53
  case TYPE.I32: return {v:buf.readInt32LE(off),off:off+4};
54
+ case TYPE.I64: return {v:Number(buf.readBigInt64LE(off)),off:off+8};
46
55
  case TYPE.F64: return {v:buf.readDoubleLE(off),off:off+8};
47
56
  case TYPE.STR: { const l=buf.readUInt32LE(off);off+=4; return {v:buf.toString('utf8',off,off+l),off:off+l}; }
48
57
  case TYPE.BUF: { const l=buf.readUInt32LE(off);off+=4; return {v:Buffer.from(buf.subarray(off,off+l)),off:off+l}; }
@@ -62,7 +71,11 @@ function writeEnc(buf, off, v) {
62
71
  if (v === null || v === undefined) { buf[off] = 0; return off + 1; }
63
72
  if (typeof v === 'boolean') { buf[off] = 1; buf[off+1] = v ? 1 : 0; return off + 2; }
64
73
  if (typeof v === 'number') {
65
- if (Number.isInteger(v)) { buf[off] = 2; buf.writeInt32LE(v, off+1); return off + 5; }
74
+ if (Number.isInteger(v)) {
75
+ // P2-29-fix: 超出 int32 范围的整数用 I64 存储
76
+ if (v >= -0x80000000 && v <= 0x7FFFFFFF) { buf[off] = 2; buf.writeInt32LE(v, off+1); return off + 5; }
77
+ buf[off] = 8; buf.writeBigInt64LE(BigInt(v), off+1); return off + 9;
78
+ }
66
79
  buf[off] = 3; buf.writeDoubleLE(v, off+1); return off + 9;
67
80
  }
68
81
  if (typeof v === 'string') {
@@ -414,8 +427,7 @@ class JSQLFormat {
414
427
  }
415
428
  if (buf[36] !== BT.DATA || buf[37] !== t.tableId) continue;
416
429
  if (!verifyHash(buf)) {
417
- console.warn(`[JSQLFormat] Table '${name}': data block ${bid} failed integrity check, skipping`);
418
- continue;
430
+ throw new Error(`[JSQLFormat] Table '${name}': data block ${bid} failed integrity check`);
419
431
  }
420
432
 
421
433
  const compFlag = buf[48];
@@ -154,6 +154,7 @@ class MongoServer {
154
154
  this._engine = null;
155
155
  this._server = null;
156
156
  this._sockets = new Set();
157
+ this._dbCollections = new Map(); // db name → Set of collection names
157
158
  }
158
159
 
159
160
  async _getEngine() {
@@ -166,11 +167,15 @@ class MongoServer {
166
167
  return this._engine;
167
168
  }
168
169
 
169
- async _ensureCollection(name) {
170
+ async _ensureCollection(name, db = null) {
170
171
  const engine = await this._getEngine();
171
172
  if (!engine.getTableSchema(name)) {
172
173
  engine.createTable(name, {});
173
174
  }
175
+ if (db) {
176
+ if (!this._dbCollections.has(db)) this._dbCollections.set(db, new Set());
177
+ this._dbCollections.get(db).add(name);
178
+ }
174
179
  return engine;
175
180
  }
176
181
 
@@ -339,7 +344,7 @@ class MongoServer {
339
344
  return { ok: 1, cursor: { id: { $long: 0 }, ns: `${this._dbOf(doc)}.$cmd.listCollections`, firstBatch } };
340
345
  }
341
346
  case 'create': {
342
- const engine = await this._ensureCollection(String(doc.create));
347
+ const engine = await this._ensureCollection(String(doc.create), this._dbOf(doc));
343
348
  return { ok: 1, ns: engine ? `${this._dbOf(doc)}.${doc.create}` : '' };
344
349
  }
345
350
  case 'drop': {
@@ -353,7 +358,7 @@ class MongoServer {
353
358
  }
354
359
  case 'insert': {
355
360
  const coll = String(doc.insert);
356
- const engine = await this._ensureCollection(coll);
361
+ const engine = await this._ensureCollection(coll, this._dbOf(doc));
357
362
  const docs = doc.documents || [];
358
363
  const ids = [];
359
364
  for (const d of docs) {
@@ -369,7 +374,7 @@ class MongoServer {
369
374
  }
370
375
  case 'findAndModify': {
371
376
  const coll = String(doc.findAndModify);
372
- const engine = await this._ensureCollection(coll);
377
+ const engine = await this._ensureCollection(coll, this._dbOf(doc));
373
378
  const q = doc.query || {};
374
379
  const rows = this._matching(engine, coll, q);
375
380
  const found = rows[0] || null;
@@ -393,7 +398,7 @@ class MongoServer {
393
398
  }
394
399
  case 'distinct': {
395
400
  const coll = String(doc.distinct);
396
- const engine = await this._ensureCollection(coll);
401
+ const engine = await this._ensureCollection(coll, this._dbOf(doc));
397
402
  const key = String(doc.key || '');
398
403
  const rows = this._matching(engine, coll, doc.query);
399
404
  const seen = new Set();
@@ -404,11 +409,21 @@ class MongoServer {
404
409
  }
405
410
  return { ok: 1, values };
406
411
  }
407
- case 'dropDatabase':
408
- return { ok: 1, dropped: String(doc.dropDatabase || this._dbOf(doc)) };
412
+ case 'dropDatabase': {
413
+ const dbName = String(doc.$db || 'test');
414
+ const engine = await this._getEngine();
415
+ const colls = this._dbCollections.get(dbName);
416
+ if (colls) {
417
+ for (const c of colls) {
418
+ try { engine.dropTable(c); } catch (_) {}
419
+ }
420
+ this._dbCollections.delete(dbName);
421
+ }
422
+ return { ok: 1, dropped: dbName };
423
+ }
409
424
  case 'find': {
410
425
  const coll = String(doc.find);
411
- const engine = await this._ensureCollection(coll);
426
+ const engine = await this._ensureCollection(coll, this._dbOf(doc));
412
427
  let rows = this._matching(engine, coll, doc.filter);
413
428
  const skip = typeof doc.skip === 'number' ? doc.skip : 0;
414
429
  if (skip > 0) rows = rows.slice(skip);
@@ -420,7 +435,7 @@ class MongoServer {
420
435
  return { ok: 1, cursor: { id: { $long: 0 }, ns: String(doc.collection || ''), nextBatch: [] } };
421
436
  case 'update': {
422
437
  const coll = String(doc.update);
423
- const engine = await this._ensureCollection(coll);
438
+ const engine = await this._ensureCollection(coll, this._dbOf(doc));
424
439
  let n = 0;
425
440
  const updates = doc.updates || [];
426
441
  for (const u of updates) {
@@ -453,24 +468,26 @@ class MongoServer {
453
468
  }
454
469
  case 'delete': {
455
470
  const coll = String(doc.delete);
456
- const engine = await this._ensureCollection(coll);
471
+ const engine = await this._ensureCollection(coll, this._dbOf(doc));
457
472
  let n = 0;
458
473
  for (const d of (doc.deletes || [])) {
459
474
  const rows = this._matching(engine, coll, d.q || {});
460
- if (rows.length > 0) this._removeRows(engine, coll, rows);
461
- n += rows.length;
475
+ // MongoDB delete: limit=1 means delete only one, limit=0 means delete all
476
+ const toDelete = d.limit === 1 ? rows.slice(0, 1) : rows;
477
+ if (toDelete.length > 0) this._removeRows(engine, coll, toDelete);
478
+ n += toDelete.length;
462
479
  }
463
480
  return { ok: 1, n };
464
481
  }
465
482
  case 'count': {
466
483
  const coll = String(doc.count);
467
- const engine = await this._ensureCollection(coll);
484
+ const engine = await this._ensureCollection(coll, this._dbOf(doc));
468
485
  const rows = this._matching(engine, coll, doc.query);
469
486
  return { ok: 1, n: Number(doc.limit) > 0 ? Math.min(rows.length, doc.limit) : rows.length };
470
487
  }
471
488
  case 'aggregate': {
472
489
  const coll = String(doc.aggregate);
473
- const engine = await this._ensureCollection(coll);
490
+ const engine = await this._ensureCollection(coll, this._dbOf(doc));
474
491
  const pipeline = doc.pipeline || [];
475
492
  let rows = engine.find(coll, {});
476
493
  for (const stage of pipeline) {