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 +22 -1
- package/bin/jsql +90 -0
- package/index.js +1 -1
- package/lib/cache.js +21 -8
- package/lib/database.js +149 -17
- package/lib/date-types.js +5 -2
- package/lib/jsql_format.js +17 -5
- package/lib/mongo_server.js +31 -14
- package/lib/mysql_compat.js +2 -1
- package/lib/mysql_server.js +6 -0
- package/lib/native_client.js +44 -1
- package/lib/nedb_compat.js +6 -6
- package/lib/query.js +2 -1
- package/lib/redis_server.js +38 -13
- package/lib/sql.js +131 -42
- package/lib/sqlite_compat.js +2 -1
- package/lib/table.js +81 -3
- package/lib/wasm_client.js +44 -1
- package/lib/web_ui.js +14 -8
- package/native/jsql-neo-native.node +0 -0
- package/package.json +3 -3
- package/test/coverage.js +3 -1
- package/test/dml-constraints.test.js +16 -0
- package/test/readme-audit.test.js +8 -7
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
|
|
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
|

|
|
9
9
|

|
|
@@ -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
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 (
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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[
|
|
228
|
-
this._expires = this._dbsExpires[
|
|
229
|
-
this._db =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
// 无主键表没有稳定的行标识,
|
|
1866
|
-
//
|
|
1867
|
-
//
|
|
1868
|
-
//
|
|
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
|
|
1992
|
+
const targets = [];
|
|
1873
1993
|
for (const id of ids) {
|
|
1874
|
-
|
|
1875
|
-
|
|
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(
|
|
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
|
-
|
|
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)
|
|
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
|
}
|
package/lib/jsql_format.js
CHANGED
|
@@ -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')
|
|
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)) {
|
|
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
|
-
|
|
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];
|
package/lib/mongo_server.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
461
|
-
|
|
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) {
|