jsql-neo 6.3.10 → 6.4.0-beta.1
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 +12 -6
- 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/nedb_compat.js +6 -6
- package/lib/query.js +2 -1
- package/lib/redis_server.js +38 -13
- package/lib/sql.js +102 -27
- package/lib/table.js +81 -3
- package/package.json +2 -2
- 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.
|
|
6
|
+
> **v6.4.0-beta.1** — 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
|
@@ -1710,7 +1710,8 @@ class Database {
|
|
|
1710
1710
|
if (!table) throw createError('ER_NO_SUCH_TABLE', tableName);
|
|
1711
1711
|
if (!this._runHooks('beforeFind', [tableName, { filter, opts }])) return [];
|
|
1712
1712
|
const { limit, offset = 0 } = opts;
|
|
1713
|
-
|
|
1713
|
+
// P1-4: 软删除模式下不得泄露已删除行——只看可见行
|
|
1714
|
+
let rows = table._applyFilterOptimized(table._getVisibleRows(), filter || {});
|
|
1714
1715
|
if (limit > 0) rows = rows.slice(offset, offset + limit);
|
|
1715
1716
|
else if (offset > 0) rows = rows.slice(offset);
|
|
1716
1717
|
// P1-11: 返回浅拷贝,防止调用方修改影响内部数据
|
|
@@ -1744,6 +1745,8 @@ class Database {
|
|
|
1744
1745
|
if (!table) throw createError('ER_NO_SUCH_TABLE', tableName);
|
|
1745
1746
|
if (!this._runHooks('beforeFind', [tableName, { id }])) return null;
|
|
1746
1747
|
const row = this._resolveId(table, id);
|
|
1748
|
+
// P1-4: 软删除行对普通读取不可见
|
|
1749
|
+
if (row && row._deleted) return null;
|
|
1747
1750
|
// P1-11: 返回浅拷贝,防止调用方修改影响内部数据
|
|
1748
1751
|
const result = row ? { ...row } : null;
|
|
1749
1752
|
this._touchTable(tableName);
|
|
@@ -1755,8 +1758,8 @@ class Database {
|
|
|
1755
1758
|
const table = this._ensureTable(tableName);
|
|
1756
1759
|
if (!table) throw createError('ER_NO_SUCH_TABLE', tableName);
|
|
1757
1760
|
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 }));
|
|
1761
|
+
// P1-4: 过滤掉软删除行;P1-11: 返回浅拷贝
|
|
1762
|
+
const rows = ids.map(id => this._resolveId(table, id)).filter(r => r !== null && !r._deleted).map(r => ({ ...r }));
|
|
1760
1763
|
this._touchTable(tableName);
|
|
1761
1764
|
this._runHooks('afterFind', [tableName, { ids }, rows]);
|
|
1762
1765
|
return rows;
|
|
@@ -1770,7 +1773,8 @@ class Database {
|
|
|
1770
1773
|
const table = this._ensureTable(tableName);
|
|
1771
1774
|
if (!table) throw createError('ER_NO_SUCH_TABLE', tableName);
|
|
1772
1775
|
this._runHooks('beforeCount', [tableName]);
|
|
1773
|
-
|
|
1776
|
+
// P1-4: 软删除模式下只统计可见行
|
|
1777
|
+
const n = table._softDelete ? table._getVisibleRows().length : table._rows.length;
|
|
1774
1778
|
this._touchTable(tableName);
|
|
1775
1779
|
this._runHooks('afterCount', [tableName, n]);
|
|
1776
1780
|
return n;
|
|
@@ -1783,7 +1787,8 @@ class Database {
|
|
|
1783
1787
|
// P1-9: updateById 绕过类型校验——先校验再更新
|
|
1784
1788
|
const validated = table._validateDataTypes({ ...data }, true);
|
|
1785
1789
|
const row = this._resolveId(table, id);
|
|
1786
|
-
|
|
1790
|
+
// P1-4: 软删除行不可更新
|
|
1791
|
+
if (!row || row._deleted) return { ok: false, error: 'not found' };
|
|
1787
1792
|
// 改主键/唯一列时要先确认新值不与其它行冲突。
|
|
1788
1793
|
// 此前直接 Object.assign,导致
|
|
1789
1794
|
// UPDATE t SET id = 1 WHERE id = 2 (t 已有 id=1)
|
|
@@ -1893,7 +1898,8 @@ class Database {
|
|
|
1893
1898
|
if (!table) throw createError('ER_NO_SUCH_TABLE', tableName);
|
|
1894
1899
|
if (!this._runHooks('beforeDelete', [tableName, [filter]])) return { ok: true, count: 0 };
|
|
1895
1900
|
const before = table._rows.length;
|
|
1896
|
-
|
|
1901
|
+
// P1-4: 只匹配可见行(软删除行已“不存在”,不应被硬删操作波及)
|
|
1902
|
+
const matched = table._applyFilter(table._getVisibleRows(), filter || {});
|
|
1897
1903
|
const matchSet = new Set(matched);
|
|
1898
1904
|
table._rows = table._rows.filter(r => !matchSet.has(r));
|
|
1899
1905
|
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) {
|
package/lib/mysql_compat.js
CHANGED
|
@@ -19,7 +19,7 @@ function toResultPacket(r, table, rowsAsArray) {
|
|
|
19
19
|
// P3-9: fields 类型全 253——根据实际类型设置
|
|
20
20
|
const fields = columns.map(name => {
|
|
21
21
|
let type = 253; // VAR_STRING
|
|
22
|
-
const val =
|
|
22
|
+
const val = rawRows[0] ? rawRows[0][columns.indexOf(name)] : null;
|
|
23
23
|
if (typeof val === 'number') type = 8; // LONGLONG
|
|
24
24
|
else if (typeof val === 'boolean') type = 1; // TINY
|
|
25
25
|
else if (val instanceof Date) type = 12; // DATETIME
|
|
@@ -73,6 +73,7 @@ class Connection extends EventEmitter {
|
|
|
73
73
|
this.filename = options.filename || null;
|
|
74
74
|
this.engine = null;
|
|
75
75
|
this._ownEngine = false;
|
|
76
|
+
this._connectEmitted = false;
|
|
76
77
|
this.state = 'disconnected';
|
|
77
78
|
this.stream = {
|
|
78
79
|
destroyed: false,
|
package/lib/mysql_server.js
CHANGED
|
@@ -131,6 +131,12 @@ function parseLenenc(buf, offset) {
|
|
|
131
131
|
if (first === 0xfb) return { value: null, size: 1 };
|
|
132
132
|
if (first === 0xfc) return { value: buf.readUInt16LE(offset + 1), size: 3 };
|
|
133
133
|
if (first === 0xfd) return { value: buf.readUIntLE(offset + 1, 3), size: 4 };
|
|
134
|
+
// 0xfe: 8-byte int64
|
|
135
|
+
if (first === 0xfe) {
|
|
136
|
+
const low = buf.readUInt32LE(offset + 1);
|
|
137
|
+
const high = buf.readUInt32LE(offset + 5);
|
|
138
|
+
return { value: low + high * 4294967296, size: 9 };
|
|
139
|
+
}
|
|
134
140
|
return { value: buf.readUInt32LE(offset + 1), size: 5 };
|
|
135
141
|
}
|
|
136
142
|
|
package/lib/nedb_compat.js
CHANGED
|
@@ -53,7 +53,7 @@ function arraysEqual(a, b) {
|
|
|
53
53
|
function looseEqual(a, b) {
|
|
54
54
|
if (a === b) return true;
|
|
55
55
|
if (a === null || b === null || a === undefined || b === undefined) return false;
|
|
56
|
-
if (typeof a === 'number'
|
|
56
|
+
if (typeof a === 'number' && typeof b === 'number') {
|
|
57
57
|
const na = Number(a), nb = Number(b);
|
|
58
58
|
if (!Number.isNaN(na) && !Number.isNaN(nb)) return na === nb;
|
|
59
59
|
}
|
|
@@ -63,17 +63,17 @@ function looseEqual(a, b) {
|
|
|
63
63
|
if (ka.length !== kb.length) return false;
|
|
64
64
|
return ka.every(k => looseEqual(a[k], b[k]));
|
|
65
65
|
}
|
|
66
|
-
return
|
|
66
|
+
return false;
|
|
67
67
|
}
|
|
68
68
|
|
|
69
69
|
function matchOperator(value, op, expected) {
|
|
70
70
|
switch (op) {
|
|
71
71
|
case '$eq': return looseEqual(value, expected);
|
|
72
72
|
case '$ne': return !looseEqual(value, expected);
|
|
73
|
-
case '$lt': return value !== undefined && (value === null ?
|
|
74
|
-
case '$lte': return value !== undefined && (value === null ?
|
|
75
|
-
case '$gt': return value !== undefined && (value === null ?
|
|
76
|
-
case '$gte': return value !== undefined && (value === null ?
|
|
73
|
+
case '$lt': return value !== undefined && (value === null ? 0 < expected : value < expected);
|
|
74
|
+
case '$lte': return value !== undefined && (value === null ? 0 <= expected : value <= expected);
|
|
75
|
+
case '$gt': return value !== undefined && (value === null ? 0 > expected : value > expected);
|
|
76
|
+
case '$gte': return value !== undefined && (value === null ? 0 >= expected : value >= expected);
|
|
77
77
|
case '$in': return Array.isArray(expected) && expected.some(e => looseEqual(value, e));
|
|
78
78
|
case '$nin': return Array.isArray(expected) && !expected.some(e => looseEqual(value, e));
|
|
79
79
|
case '$exists': return expected ? value !== undefined : value === undefined;
|
package/lib/query.js
CHANGED
|
@@ -232,7 +232,8 @@ class Query {
|
|
|
232
232
|
return this._cache.result;
|
|
233
233
|
}
|
|
234
234
|
|
|
235
|
-
|
|
235
|
+
// P1-4: 软删除模式下 QueryBuilder 也不得泄露已删除行
|
|
236
|
+
let rows = this._table._applyFilterOptimized(this._table._getVisibleRows(), this._conditions);
|
|
236
237
|
|
|
237
238
|
// JOIN
|
|
238
239
|
for (const join of this._joins) {
|
package/lib/redis_server.js
CHANGED
|
@@ -26,7 +26,7 @@ const TYPE_SIGNATURES = {
|
|
|
26
26
|
INCR: 1, DECR: 1, INCRBY: 2, DECRBY: 2, APPEND: 2, STRLEN: 1,
|
|
27
27
|
MSET: -2, MGET: -1,
|
|
28
28
|
HSET: -3, HGET: 2, HGETALL: 1, HDEL: -2, HEXISTS: 2, HLEN: 1, HKEYS: 1, HVALS: 1,
|
|
29
|
-
LPUSH: -2, RPUSH: -2, LPOP: 1, RPOP: 1, LLEN: 1, LRANGE: 3, LINDEX: 2, LREM: 3,
|
|
29
|
+
LPUSH: -2, RPUSH: -2, LPOP: -1, RPOP: -1, LLEN: 1, LRANGE: 3, LINDEX: 2, LREM: 3,
|
|
30
30
|
SADD: -2, SREM: -2, SMEMBERS: 1, SISMEMBER: 2, SCARD: 1,
|
|
31
31
|
ZADD: -3, ZRANGE: 3, ZREVRANGE: 3, ZSCORE: 2, ZCARD: 1, ZREM: -2, ZINCRBY: 3,
|
|
32
32
|
DBSIZE: 0, FLUSHALL: 0, FLUSHDB: 0, SELECT: 1, INFO: 0, AUTH: 1, QUIT: 0,
|
|
@@ -167,17 +167,22 @@ class RedisServer {
|
|
|
167
167
|
return v.val;
|
|
168
168
|
}
|
|
169
169
|
case 'APPEND': {
|
|
170
|
+
const argCount = args.length;
|
|
171
|
+
if (argCount > 2) throw new Error(`ERR wrong number of arguments for 'append' command`);
|
|
170
172
|
const v = this._get(args[0]);
|
|
171
|
-
|
|
173
|
+
// N2-fix: 对非 string 类型的 key(list/hash/set/zset)必须报 WRONGTYPE,
|
|
174
|
+
// 此前静默把整值覆盖成 string,丢数据且与真 Redis 行为不符。
|
|
175
|
+
if (v && v.type !== 'string') throw new Error('WRONGTYPE Operation against a key holding the wrong kind of value');
|
|
176
|
+
const prev = v ? v.val : '';
|
|
172
177
|
this.db.set(args[0], { type: 'string', val: prev + args[1], ttl: v ? v.ttl : null });
|
|
173
178
|
this._scheduleSnapshot();
|
|
174
|
-
return this.db.get(args[0]).val
|
|
179
|
+
return Buffer.byteLength(this.db.get(args[0]).val, 'utf8');
|
|
175
180
|
}
|
|
176
181
|
case 'STRLEN': {
|
|
177
182
|
const v = this._get(args[0]);
|
|
178
183
|
if (!v) return 0;
|
|
179
184
|
if (v.type !== 'string') throw new Error('WRONGTYPE Operation against a key holding the wrong kind of value');
|
|
180
|
-
return v.val
|
|
185
|
+
return Buffer.byteLength(v.val, 'utf8');
|
|
181
186
|
}
|
|
182
187
|
case 'INCR': case 'DECR': case 'INCRBY': case 'DECRBY': {
|
|
183
188
|
const by = cmd === 'INCRBY' || cmd === 'DECRBY' ? parseInt(args[1], 10) : 1;
|
|
@@ -241,7 +246,8 @@ class RedisServer {
|
|
|
241
246
|
case 'HSET': {
|
|
242
247
|
if (args.length % 2 !== 1) throw new Error(`ERR wrong number of arguments for 'hset' command`);
|
|
243
248
|
const v = this._get(args[0]);
|
|
244
|
-
|
|
249
|
+
if (v && v.type !== 'hash') throw new Error('WRONGTYPE Operation against a key holding the wrong kind of value');
|
|
250
|
+
const h = v ? { ...v.val } : {};
|
|
245
251
|
let n = 0;
|
|
246
252
|
for (let i = 1; i < args.length; i += 2) {
|
|
247
253
|
if (!(args[i] in h)) n++;
|
|
@@ -290,7 +296,8 @@ class RedisServer {
|
|
|
290
296
|
}
|
|
291
297
|
case 'LPUSH': case 'RPUSH': {
|
|
292
298
|
const v = this._get(args[0]);
|
|
293
|
-
|
|
299
|
+
if (v && v.type !== 'list') throw new Error('WRONGTYPE Operation against a key holding the wrong kind of value');
|
|
300
|
+
const list = v ? [...v.val] : [];
|
|
294
301
|
if (cmd === 'LPUSH') {
|
|
295
302
|
for (const val of args.slice(1)) list.unshift(val);
|
|
296
303
|
} else {
|
|
@@ -301,16 +308,32 @@ class RedisServer {
|
|
|
301
308
|
return list.length;
|
|
302
309
|
}
|
|
303
310
|
case 'LPOP': case 'RPOP': {
|
|
311
|
+
// N3-fix: 支持可选 count 参数(Redis 6.2+)。旧实现忽略 count,LPOP key 2 只弹 1 个。
|
|
312
|
+
// 无 count:弹 1 个,key 不存在时返回 nil(bulk null)。
|
|
313
|
+
// 有 count:返回数组(最多 count 个元素),key 不存在时返回空数组;count<0 报错。
|
|
314
|
+
const hasCount = args.length >= 2;
|
|
315
|
+
let count = 1;
|
|
316
|
+
if (hasCount) {
|
|
317
|
+
count = parseInt(args[1], 10);
|
|
318
|
+
if (isNaN(count) || count < 0) throw new Error('ERR value is out of range, must be positive');
|
|
319
|
+
}
|
|
304
320
|
const v = this._get(args[0]);
|
|
305
|
-
if (!v) return null;
|
|
321
|
+
if (!v) return hasCount ? [] : null;
|
|
306
322
|
if (v.type !== 'list') throw new Error('WRONGTYPE Operation against a key holding the wrong kind of value');
|
|
307
|
-
const
|
|
323
|
+
const popped = [];
|
|
324
|
+
for (let i = 0; i < count && v.val.length > 0; i++) {
|
|
325
|
+
popped.push(cmd === 'LPOP' ? v.val.shift() : v.val.pop());
|
|
326
|
+
}
|
|
327
|
+
if (v.val.length === 0) this.db.delete(args[0]);
|
|
308
328
|
this._scheduleSnapshot();
|
|
309
|
-
|
|
329
|
+
if (hasCount) return popped;
|
|
330
|
+
return popped.length ? popped[0] : null;
|
|
310
331
|
}
|
|
311
332
|
case 'LLEN': {
|
|
312
333
|
const v = this._get(args[0]);
|
|
313
|
-
|
|
334
|
+
if (!v) return 0;
|
|
335
|
+
if (v.type !== 'list') throw new Error('WRONGTYPE Operation against a key holding the wrong kind of value');
|
|
336
|
+
return v.val.length;
|
|
314
337
|
}
|
|
315
338
|
case 'LRANGE': {
|
|
316
339
|
const v = this._get(args[0]);
|
|
@@ -366,7 +389,8 @@ class RedisServer {
|
|
|
366
389
|
}
|
|
367
390
|
case 'SADD': case 'SREM': {
|
|
368
391
|
const v = this._get(args[0]);
|
|
369
|
-
|
|
392
|
+
if (v && v.type !== 'set') throw new Error('WRONGTYPE Operation against a key holding the wrong kind of value');
|
|
393
|
+
const set = v ? new Set(v.val) : new Set();
|
|
370
394
|
let n = 0;
|
|
371
395
|
for (const m of args.slice(1)) {
|
|
372
396
|
if (cmd === 'SADD') { if (!set.has(m)) { set.add(m); n++; } }
|
|
@@ -495,8 +519,9 @@ class RedisServer {
|
|
|
495
519
|
case 'DBSIZE': {
|
|
496
520
|
let count = 0;
|
|
497
521
|
const now = Date.now();
|
|
498
|
-
for (const v of this.db
|
|
499
|
-
if (
|
|
522
|
+
for (const [k, v] of this.db) {
|
|
523
|
+
if (v.ttl && v.ttl <= now) { this.db.delete(k); continue; }
|
|
524
|
+
count++;
|
|
500
525
|
}
|
|
501
526
|
return count;
|
|
502
527
|
}
|
package/lib/sql.js
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
// N1-fix: createError 此前未导入,UPDATE 无主键表时抛 ReferenceError(而非预期的 SQL 错误)
|
|
2
|
+
const { createError } = require('./errors');
|
|
3
|
+
|
|
1
4
|
const DANGEROUS_SQL = [
|
|
2
5
|
{ re: /^INTO$/, next: /^(OUTFILE|DUMPFILE)$/, name: 'INTO OUTFILE/DUMPFILE' },
|
|
3
6
|
{ re: /^LOAD$/, next: /^(FILE|DATA)$/, name: 'LOAD_FILE/LOAD DATA' },
|
|
@@ -616,7 +619,9 @@ class Parser {
|
|
|
616
619
|
const nextTok = this.peek(1);
|
|
617
620
|
if (nextTok && (nextTok.type === 'ident' || nextTok.type === 'keyword') &&
|
|
618
621
|
['TEXT', 'INT', 'INTEGER', 'VARCHAR', 'CHAR', 'DATE', 'DATETIME', 'TIMESTAMP', 'BOOLEAN', 'BOOL', 'FLOAT', 'DOUBLE', 'DECIMAL', 'BLOB', 'JSON'].includes(String(nextTok.value).toUpperCase())) {
|
|
619
|
-
|
|
622
|
+
// P1-24: KEY/INDEX 是保留字,不能直接用 parseTableName(),改用 nameOfToken + next()
|
|
623
|
+
const col = nameOfToken(t);
|
|
624
|
+
this.next(); // 消费 KEY/INDEX token
|
|
620
625
|
const def = this.parseColumnDef();
|
|
621
626
|
schema[col] = def;
|
|
622
627
|
if (def.primaryKey) hasPk = true;
|
|
@@ -983,6 +988,24 @@ class Parser {
|
|
|
983
988
|
}
|
|
984
989
|
|
|
985
990
|
parseValue() {
|
|
991
|
+
const atom = this.parseValueAtom();
|
|
992
|
+
return this.parseBinaryFromAtom(atom);
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
/** P2-3 helper: parse binary expression starting from an already-parsed left atom. */
|
|
996
|
+
parseBinaryFromAtom(left) {
|
|
997
|
+
const ops = ['+', '-', '*', '/', '%', '<<', '>>', '&', '|', '^'];
|
|
998
|
+
for (;;) {
|
|
999
|
+
const opTok = this.peek();
|
|
1000
|
+
if (opTok.type !== 'op' || !ops.includes(opTok.value)) break;
|
|
1001
|
+
this.next();
|
|
1002
|
+
const right = this.parseValueAtom();
|
|
1003
|
+
left = { type: 'binary', op: opTok.value, left, right };
|
|
1004
|
+
}
|
|
1005
|
+
return left;
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
parseValueAtom() {
|
|
986
1009
|
const t = this.next();
|
|
987
1010
|
if (t.type === 'number' || t.type === 'string') return t.value;
|
|
988
1011
|
// MySQL: ON DUPLICATE KEY UPDATE col = VALUES(col) —— 引用"本次本应插入的值"
|
|
@@ -1007,17 +1030,15 @@ class Parser {
|
|
|
1007
1030
|
if (num.type !== 'number') throw new Error('Expected number after +');
|
|
1008
1031
|
return num.value;
|
|
1009
1032
|
}
|
|
1010
|
-
// P1-19/P2-3: parseValue 不支持表达式 RHS,如 col = col + 1
|
|
1011
|
-
// 支持简单的二元表达式:left op right
|
|
1012
1033
|
if (t.type === 'ident') {
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
if (opTok.type === 'op' && ['+', '-', '*', '/', '%', '<<', '>>', '&', '|', '^'].includes(opTok.value)) {
|
|
1034
|
+
// Handle dotted column refs (a.b) without double-consuming the token
|
|
1035
|
+
if (this.peek().type === 'op' && this.peek().value === '.') {
|
|
1016
1036
|
this.next();
|
|
1017
|
-
const
|
|
1018
|
-
|
|
1037
|
+
const col = this.next();
|
|
1038
|
+
if (!isUsableNameToken(col)) throw new Error(`Expected column name after '.', got '${col.value}'`);
|
|
1039
|
+
return nameOfToken(t) + '.' + nameOfToken(col);
|
|
1019
1040
|
}
|
|
1020
|
-
return
|
|
1041
|
+
return nameOfToken(t);
|
|
1021
1042
|
}
|
|
1022
1043
|
throw new Error(`Expected value, got '${t.value}'`);
|
|
1023
1044
|
}
|
|
@@ -2355,9 +2376,11 @@ class Parser {
|
|
|
2355
2376
|
return { type: 'value', value: t.value === '-' ? -num.value : num.value };
|
|
2356
2377
|
}
|
|
2357
2378
|
// 负号/正号后面是列名或表达式:解析为 unary 运算符
|
|
2358
|
-
if (num.type === '
|
|
2359
|
-
|
|
2360
|
-
|
|
2379
|
+
if (num.type === 'ident') {
|
|
2380
|
+
return { type: 'negate', operand: { type: 'column', name: nameOfToken(num) }, positive: t.value === '+' };
|
|
2381
|
+
}
|
|
2382
|
+
if (num.type === 'string') {
|
|
2383
|
+
return { type: 'negate', operand: { type: 'value', value: num.value }, positive: t.value === '+' };
|
|
2361
2384
|
}
|
|
2362
2385
|
throw new Error('Expected number or column after sign');
|
|
2363
2386
|
}
|
|
@@ -2825,7 +2848,9 @@ function resolveOperand(operand, row, ctx) {
|
|
|
2825
2848
|
case 'negate': {
|
|
2826
2849
|
const v = resolveOperand(operand.operand, row, ctx);
|
|
2827
2850
|
if (v === null || v === undefined) return null;
|
|
2828
|
-
|
|
2851
|
+
const n = Number(v);
|
|
2852
|
+
if (isNaN(n)) return null;
|
|
2853
|
+
return operand.positive ? n : -n;
|
|
2829
2854
|
}
|
|
2830
2855
|
// P1-19: ON DUPLICATE KEY UPDATE 表达式赋值,如 count = count + 1
|
|
2831
2856
|
case 'binary': {
|
|
@@ -3990,8 +4015,14 @@ class SQLExecutor {
|
|
|
3990
4015
|
break;
|
|
3991
4016
|
}
|
|
3992
4017
|
case 'dropPrimary': {
|
|
3993
|
-
for (const def of Object.
|
|
3994
|
-
|
|
4018
|
+
for (const [colName, def] of Object.entries(table._schema)) {
|
|
4019
|
+
if (def && typeof def === 'object' && def.primaryKey) {
|
|
4020
|
+
def.primaryKey = false;
|
|
4021
|
+
// P1-23: 同时清除 unique 标记,否则后续 INSERT 仍会拒绝重复值
|
|
4022
|
+
def.unique = false;
|
|
4023
|
+
}
|
|
4024
|
+
}
|
|
4025
|
+
// 清除主键缓存
|
|
3995
4026
|
table._primaryKey = null;
|
|
3996
4027
|
table._pkIndex = null;
|
|
3997
4028
|
break;
|
|
@@ -6409,24 +6440,24 @@ class SQLExecutor {
|
|
|
6409
6440
|
const tableName = statement.from ? (statement.from.tables[0].table || null) : null;
|
|
6410
6441
|
if (statement.columns.length === 1 && statement.columns[0].expr === '*') {
|
|
6411
6442
|
const schemaKeys = schema ? Object.keys(schema) : [];
|
|
6412
|
-
const pkCols = schemaKeys.filter(k => schema && schema[k] && schema[k].primaryKey);
|
|
6413
|
-
const pk = pkCols.length > 0 ? pkCols[0] : (schemaKeys[0] || 'id');
|
|
6414
6443
|
let cols;
|
|
6415
6444
|
// 有 JOIN 时,SELECT * 应展开所有表的列(合并后的行包含所有前缀列)
|
|
6416
6445
|
const hasJoins = statement.from && statement.from.joins && statement.from.joins.length > 0;
|
|
6417
6446
|
if (schema && !hasJoins) {
|
|
6418
|
-
|
|
6447
|
+
// P1-17: SELECT * 按列声明顺序输出,不得把主键强制排到首位(MySQL 语义)
|
|
6448
|
+
cols = schemaKeys.filter(k => k !== '_softDelete');
|
|
6419
6449
|
} else if (rowsAll && rowsAll.columns) {
|
|
6420
6450
|
cols = rowsAll.columns;
|
|
6421
6451
|
} else {
|
|
6422
|
-
// JOIN
|
|
6452
|
+
// JOIN 时:只使用带前缀的键,避免同一列重复。
|
|
6453
|
+
// 不能按裸名去重——右表同名列(如 b.id)会被丢弃,导致右表列全部丢失。
|
|
6423
6454
|
if (hasJoins) {
|
|
6424
6455
|
const seen = new Set();
|
|
6425
6456
|
cols = [];
|
|
6426
6457
|
for (const r of rows) {
|
|
6427
6458
|
for (const k of Object.keys(r)) {
|
|
6428
|
-
|
|
6429
|
-
if (!seen.has(
|
|
6459
|
+
if (!k.includes('.')) continue; // 只取带前缀的键
|
|
6460
|
+
if (!seen.has(k)) { seen.add(k); cols.push(k); }
|
|
6430
6461
|
}
|
|
6431
6462
|
}
|
|
6432
6463
|
} else {
|
|
@@ -6453,15 +6484,56 @@ class SQLExecutor {
|
|
|
6453
6484
|
return { ok: true, type: 'select', table: tableName, columns: cols, rows: rows.map(r => cols.map(c => (r[c] !== undefined ? r[c] : r[String(c).slice(String(c).indexOf('.') + 1)]))), raw: rows };
|
|
6454
6485
|
}
|
|
6455
6486
|
|
|
6487
|
+
// P1-18: SELECT *, col —— 星号必须展开为全部列,列名与值对齐。
|
|
6488
|
+
// 此前把 '*' 当成单列输出嵌套数组,导致每行值错位重复/输出 null。
|
|
6489
|
+
if (statement.columns.some(c => c.expr === '*')) {
|
|
6490
|
+
const hasJoins2 = statement.from && statement.from.joins && statement.from.joins.length > 0;
|
|
6491
|
+
let starKeys = [];
|
|
6492
|
+
if (schema && !hasJoins2) {
|
|
6493
|
+
starKeys = Object.keys(schema).filter(k => k !== '_softDelete');
|
|
6494
|
+
} else if (hasJoins2) {
|
|
6495
|
+
// JOIN 时:保留所有带前缀的键,不按裸名去重(否则右表同名列丢失)
|
|
6496
|
+
const seenK = new Set();
|
|
6497
|
+
for (const r of rows) {
|
|
6498
|
+
for (const k of Object.keys(r)) {
|
|
6499
|
+
if (!k.includes('.')) continue;
|
|
6500
|
+
if (!seenK.has(k)) { seenK.add(k); starKeys.push(k); }
|
|
6501
|
+
}
|
|
6502
|
+
}
|
|
6503
|
+
} else {
|
|
6504
|
+
const seenK = new Set();
|
|
6505
|
+
for (const r of rows) {
|
|
6506
|
+
for (const k of Object.keys(r)) {
|
|
6507
|
+
const bare = k.includes('.') ? k.slice(k.indexOf('.') + 1) : k;
|
|
6508
|
+
if (!seenK.has(bare)) { seenK.add(bare); starKeys.push(k); }
|
|
6509
|
+
}
|
|
6510
|
+
}
|
|
6511
|
+
}
|
|
6512
|
+
const cols = [];
|
|
6513
|
+
for (const c of statement.columns) {
|
|
6514
|
+
if (c.expr === '*') { for (const k of starKeys) cols.push(k.includes('.') ? k.slice(k.indexOf('.') + 1) : k); }
|
|
6515
|
+
else cols.push(scalarColumnName(c));
|
|
6516
|
+
}
|
|
6517
|
+
const mapped = await this._projectRows(statement, rows, (r) => {
|
|
6518
|
+
const out = [];
|
|
6519
|
+
for (const c of statement.columns) {
|
|
6520
|
+
if (c.expr === '*') {
|
|
6521
|
+
for (const k of starKeys) out.push(r[k] !== undefined ? r[k] : r[String(k).slice(String(k).indexOf('.') + 1)]);
|
|
6522
|
+
} else if (c.over || (c.scalar && c.scalar.over)) out.push(windowColumnValue(c, r));
|
|
6523
|
+
else if (c.scalar) out.push(projectScalar(resolveOperand(c.scalar, r, this.ctx)));
|
|
6524
|
+
else if (c.literal !== undefined) out.push(c.literal);
|
|
6525
|
+
else if (c.caseExpr) out.push(projectScalar(evaluateCaseVal(c.caseExpr, r)));
|
|
6526
|
+
else out.push(projectScalar(resolveOperand({ type: 'column', name: c.expr }, r, this.ctx)));
|
|
6527
|
+
}
|
|
6528
|
+
return out;
|
|
6529
|
+
});
|
|
6530
|
+
return { ok: true, type: 'select', table: tableName, columns: cols, rows: mapped, raw: rows };
|
|
6531
|
+
}
|
|
6532
|
+
|
|
6456
6533
|
const cols = statement.columns.map(c => scalarColumnName(c));
|
|
6457
6534
|
const mapped = await this._projectRows(statement, rows, (r) => statement.columns.map(c => {
|
|
6458
6535
|
if (c.over || (c.scalar && c.scalar.over)) return windowColumnValue(c, r);
|
|
6459
6536
|
if (c.scalar) return projectScalar(resolveOperand(c.scalar, r, this.ctx));
|
|
6460
|
-
// P1-18: SELECT *, col 星号列输出 null——应展开为所有列
|
|
6461
|
-
if (c.expr === '*') {
|
|
6462
|
-
// 展开星号:返回行的所有值
|
|
6463
|
-
return Object.keys(r).map(k => r[k]);
|
|
6464
|
-
}
|
|
6465
6537
|
if (c.literal !== undefined) return c.literal;
|
|
6466
6538
|
if (c.caseExpr) return projectScalar(evaluateCaseVal(c.caseExpr, r));
|
|
6467
6539
|
return projectScalar(resolveOperand({ type: 'column', name: c.expr }, r, this.ctx));
|
|
@@ -6604,7 +6676,10 @@ function escapeValue(value) {
|
|
|
6604
6676
|
}
|
|
6605
6677
|
return value.map(escapeValue).join(', ');
|
|
6606
6678
|
}
|
|
6607
|
-
|
|
6679
|
+
// P1-25: 对象参数先 JSON 序列化,再走与字符串完全相同的转义通道。
|
|
6680
|
+
// 此前只对单引号做替换,反斜杠原样拼进 SQL 文本,解析时丢失/翻倍,
|
|
6681
|
+
// 导致 C:\temp\x 这类值存成两个反斜杠或直接 JSON 解析失败。
|
|
6682
|
+
if (typeof value === 'object') return escapeValue(JSON.stringify(value));
|
|
6608
6683
|
const str = String(value)
|
|
6609
6684
|
.replace(/\\/g, '\\\\')
|
|
6610
6685
|
.replace(/\0/g, '\\0')
|
package/lib/table.js
CHANGED
|
@@ -175,6 +175,18 @@ class Table {
|
|
|
175
175
|
this._autoCreateBTrees();
|
|
176
176
|
}
|
|
177
177
|
|
|
178
|
+
// P1-2: DDL 后统一刷新 schema 缓存。
|
|
179
|
+
// dropColumn/renameColumn 过去只改 _cachedSchemaFields,unique/required/numeric/
|
|
180
|
+
// date/computed 缓存残留旧列 → insertMany 等路径拿到陈旧 schema 直接崩溃或漏校验。
|
|
181
|
+
_refreshSchemaCache() {
|
|
182
|
+
this._cachedSchemaFields = Object.keys(this._schema).filter(f => f !== '_softDelete');
|
|
183
|
+
this._cachedDateFields = Object.keys(this._dateFields);
|
|
184
|
+
this._cachedUniqueFields = this._cachedSchemaFields.filter(f => this._schema[f] && this._schema[f].unique);
|
|
185
|
+
this._cachedRequiredFields = this._cachedSchemaFields.filter(f => this._schema[f] && this._schema[f].required);
|
|
186
|
+
this._cachedNumericFields = this._cachedSchemaFields.filter(f => ['integer', 'int', 'float', 'number', 'decimal'].includes(this._schema[f] && this._schema[f].type));
|
|
187
|
+
this._cachedComputedFields = this._cachedSchemaFields.filter(f => this._computedFields[f]);
|
|
188
|
+
}
|
|
189
|
+
|
|
178
190
|
_autoCreateBTrees() {
|
|
179
191
|
for (const [field, def] of Object.entries(this._schema)) {
|
|
180
192
|
if (field === '_softDelete') continue;
|
|
@@ -463,6 +475,10 @@ class Table {
|
|
|
463
475
|
if (rowIndex === undefined) return 0;
|
|
464
476
|
const row = this._rows[rowIndex];
|
|
465
477
|
if (row && row._deleted) return 0;
|
|
478
|
+
// P1-3:主键被更新时传播到外键引用(CASCADE / SET NULL / RESTRICT)
|
|
479
|
+
if (updates[this._primaryKey] !== undefined && updates[this._primaryKey] !== id) {
|
|
480
|
+
this._propagatePKUpdate(id, updates[this._primaryKey]);
|
|
481
|
+
}
|
|
466
482
|
const oldData = { ...row };
|
|
467
483
|
const merged = { ...row, ...updates };
|
|
468
484
|
this._validateConstraints(merged, rowIndex);
|
|
@@ -708,6 +724,7 @@ class Table {
|
|
|
708
724
|
if (val !== undefined && val !== null) this._pkIndex.set(val, i);
|
|
709
725
|
}
|
|
710
726
|
}
|
|
727
|
+
this._refreshSchemaCache();
|
|
711
728
|
this._dirty = true;
|
|
712
729
|
this._db._markDirty(this._name);
|
|
713
730
|
return this;
|
|
@@ -732,6 +749,7 @@ class Table {
|
|
|
732
749
|
if (this._pkIndex) this._pkIndex.clear();
|
|
733
750
|
}
|
|
734
751
|
if (this._autoIncrementField === field) this._autoIncrementField = null;
|
|
752
|
+
this._refreshSchemaCache();
|
|
735
753
|
this._dirty = true;
|
|
736
754
|
this._db._markDirty(this._name);
|
|
737
755
|
return this;
|
|
@@ -759,8 +777,27 @@ class Table {
|
|
|
759
777
|
row[newName] = row[oldName];
|
|
760
778
|
delete row[oldName];
|
|
761
779
|
}
|
|
780
|
+
if (this._indexes[oldName]) {
|
|
781
|
+
this._indexes[newName] = this._indexes[oldName];
|
|
782
|
+
delete this._indexes[oldName];
|
|
783
|
+
}
|
|
784
|
+
this._foreignKeys = this._foreignKeys.filter(fk => true);
|
|
785
|
+
for (const fk of this._foreignKeys) {
|
|
786
|
+
if (fk.field === oldName) fk.field = newName;
|
|
787
|
+
}
|
|
788
|
+
this._checkConstraints = this._checkConstraints.map(c =>
|
|
789
|
+
c.field === oldName ? { ...c, field: newName } : c);
|
|
790
|
+
if (this._computedFields[oldName]) {
|
|
791
|
+
this._computedFields[newName] = this._computedFields[oldName];
|
|
792
|
+
delete this._computedFields[oldName];
|
|
793
|
+
}
|
|
794
|
+
if (this._dateFields[oldName]) {
|
|
795
|
+
this._dateFields[newName] = this._dateFields[oldName];
|
|
796
|
+
delete this._dateFields[oldName];
|
|
797
|
+
}
|
|
762
798
|
if (this._primaryKey === oldName) this._primaryKey = newName;
|
|
763
799
|
if (this._autoIncrementField === oldName) this._autoIncrementField = newName;
|
|
800
|
+
this._refreshSchemaCache();
|
|
764
801
|
this._dirty = true;
|
|
765
802
|
this._db._markDirty(this._name);
|
|
766
803
|
return this;
|
|
@@ -780,6 +817,12 @@ class Table {
|
|
|
780
817
|
delete this._btrees[field];
|
|
781
818
|
}
|
|
782
819
|
}
|
|
820
|
+
if (definition.type && ['date', 'datetime', 'timestamp', 'time'].includes(definition.type)) {
|
|
821
|
+
this._dateFields[field] = definition.type;
|
|
822
|
+
} else if (definition.type) {
|
|
823
|
+
delete this._dateFields[field];
|
|
824
|
+
}
|
|
825
|
+
this._refreshSchemaCache();
|
|
783
826
|
this._dirty = true;
|
|
784
827
|
this._db._markDirty(this._name);
|
|
785
828
|
return this;
|
|
@@ -1212,20 +1255,55 @@ class Table {
|
|
|
1212
1255
|
|
|
1213
1256
|
/**
|
|
1214
1257
|
* 级联外键操作:当删除当前表的行时,查找所有引用此表的子表
|
|
1215
|
-
* 并执行 CASCADE
|
|
1258
|
+
* 并执行 CASCADE / SET NULL / RESTRICT(P1-3:RESTRICT 之前静默放行)
|
|
1216
1259
|
*/
|
|
1217
|
-
_cascadeForeignKeys(row) {
|
|
1260
|
+
_cascadeForeignKeys(row, opts = {}) {
|
|
1218
1261
|
for (const [, table] of Object.entries(this._db._tables)) {
|
|
1219
1262
|
if (table._name === this._name) continue;
|
|
1220
1263
|
for (const fk of table._foreignKeys) {
|
|
1221
1264
|
const refTable = table._resolveTable(fk.table);
|
|
1222
1265
|
if (!refTable || refTable._name !== this._name) continue;
|
|
1223
1266
|
|
|
1224
|
-
//
|
|
1267
|
+
// 被引用列值为 NULL 时不构成引用(SQL 语义),不参与约束
|
|
1268
|
+
const refVal = row[fk.refField];
|
|
1269
|
+
if (refVal === undefined || refVal === null) continue;
|
|
1270
|
+
|
|
1271
|
+
// 检查子表行是否引用当前行(软删除模式下行已不可见则不阻断)
|
|
1272
|
+
const refs = table._applyFilterOptimized(table._getVisibleRows(), { [fk.field]: refVal });
|
|
1273
|
+
if (refs.length === 0) continue;
|
|
1274
|
+
|
|
1225
1275
|
if (fk.onDelete === 'cascade') {
|
|
1226
1276
|
table.remove({ [fk.field]: row[fk.refField] });
|
|
1227
1277
|
} else if (fk.onDelete === 'set null') {
|
|
1228
1278
|
table.update({ [fk.field]: row[fk.refField] }, { [fk.field]: null });
|
|
1279
|
+
} else {
|
|
1280
|
+
// RESTRICT / NO ACTION / 未指定:默认拒绝删除(ER_ROW_IS_REFERENCED)
|
|
1281
|
+
if (opts.skipRestrict) continue;
|
|
1282
|
+
throw createError('ER_ROW_IS_REFERENCED');
|
|
1283
|
+
}
|
|
1284
|
+
}
|
|
1285
|
+
}
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
/**
|
|
1289
|
+
* P1-3:父表主键被更新时,传播到引用它的子表。
|
|
1290
|
+
* CASCADE → 同步新值;SET NULL → 置空;RESTRICT → 拒绝更新。
|
|
1291
|
+
*/
|
|
1292
|
+
_propagatePKUpdate(oldVal, newVal) {
|
|
1293
|
+
for (const [, table] of Object.entries(this._db._tables)) {
|
|
1294
|
+
if (table._name === this._name) continue;
|
|
1295
|
+
for (const fk of table._foreignKeys) {
|
|
1296
|
+
const refTable = table._resolveTable(fk.table);
|
|
1297
|
+
if (!refTable || refTable._name !== this._name) continue;
|
|
1298
|
+
const refs = table._applyFilterOptimized(table._getVisibleRows(), { [fk.field]: oldVal });
|
|
1299
|
+
if (refs.length === 0) continue;
|
|
1300
|
+
const action = (fk.onUpdate || 'restrict').toLowerCase();
|
|
1301
|
+
if (action === 'cascade') {
|
|
1302
|
+
table.update({ [fk.field]: oldVal }, { [fk.field]: newVal });
|
|
1303
|
+
} else if (action === 'set null') {
|
|
1304
|
+
table.update({ [fk.field]: oldVal }, { [fk.field]: null });
|
|
1305
|
+
} else {
|
|
1306
|
+
throw createError('ER_ROW_IS_REFERENCED');
|
|
1229
1307
|
}
|
|
1230
1308
|
}
|
|
1231
1309
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jsql-neo",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.4.0-beta.1",
|
|
4
4
|
"description": "JSQL-NEO — Rust-powered embedded database with WASM, REST API, B-Tree indexes, WAL, crash recovery",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"types": "index.d.ts",
|
|
@@ -88,7 +88,7 @@
|
|
|
88
88
|
"node": ">=14.0.0"
|
|
89
89
|
},
|
|
90
90
|
"dependencies": {
|
|
91
|
-
"@vexify-org/yaggs": "^8.1.
|
|
91
|
+
"@vexify-org/yaggs": "^8.1.1"
|
|
92
92
|
},
|
|
93
93
|
"devDependencies": {
|
|
94
94
|
"c8": "^10.1.2"
|
|
@@ -533,22 +533,23 @@ async function main() {
|
|
|
533
533
|
const pkg = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'package.json'), 'utf8'));
|
|
534
534
|
const raw = fs.readFileSync(path.join(__dirname, '..', 'README.md'), 'utf8');
|
|
535
535
|
const v = pkg.version;
|
|
536
|
-
|
|
536
|
+
// 支持 prerelease 版本如 6.4.0-beta.1
|
|
537
|
+
ok('package.json 版本号格式合法', /^\d+\.\d+\.\d+(-[a-zA-Z0-9.]+)?$/.test(v), v);
|
|
537
538
|
|
|
538
|
-
// README 头部横幅必须与 package.json
|
|
539
|
-
const banner = raw.match(/^> \*\*v(\d+\.\d+\.\d+)\*\*/m);
|
|
539
|
+
// README 头部横幅必须与 package.json 一致(支持 prerelease)
|
|
540
|
+
const banner = raw.match(/^> \*\*v(\d+\.\d+\.\d+(?:-[a-zA-Z0-9.]+)?)\*\*/m);
|
|
540
541
|
ok('README 顶部版本横幅与 package.json 一致', banner && banner[1] === v,
|
|
541
542
|
banner ? `README=${banner[1]} package=${v}` : '未找到顶部横幅');
|
|
542
543
|
|
|
543
|
-
// 所有 "jsql-neo vX.Y.Z" / "文档版本:vX.Y.Z"
|
|
544
|
-
const claims = [...raw.matchAll(/(?:jsql-neo v|文档版本:v|ver=)(\d+\.\d+\.\d+)/g)]
|
|
544
|
+
// 所有 "jsql-neo vX.Y.Z" / "文档版本:vX.Y.Z" 形式的版本声明都必须等于当前版本(支持 prerelease)
|
|
545
|
+
const claims = [...raw.matchAll(/(?:jsql-neo v|文档版本:v|ver=)(\d+\.\d+\.\d+(?:-[a-zA-Z0-9.]+)?)/g)]
|
|
545
546
|
.map(m => m[1]);
|
|
546
547
|
const stale = [...new Set(claims.filter(c => c !== v))];
|
|
547
548
|
ok('README 无陈旧版本号声明', stale.length === 0, stale);
|
|
548
549
|
|
|
549
|
-
// index.js
|
|
550
|
+
// index.js 头部注释也必须同步(支持 prerelease)
|
|
550
551
|
const idx = fs.readFileSync(path.join(__dirname, '..', 'index.js'), 'utf8');
|
|
551
|
-
const hdr = idx.match(/JSQL-NEO v(\d+\.\d+\.\d+)/);
|
|
552
|
+
const hdr = idx.match(/JSQL-NEO v(\d+\.\d+\.\d+(?:-[a-zA-Z0-9.]+)?)/);
|
|
552
553
|
ok('index.js 头部版本注释与 package.json 一致', hdr && hdr[1] === v,
|
|
553
554
|
hdr ? `index.js=${hdr[1]} package=${v}` : '未找到版本注释');
|
|
554
555
|
}
|