jsql-neo 6.4.0-beta.12 → 6.4.0-beta.14
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 +1 -1
- package/index.js +1 -1
- package/lib/database.js +9 -0
- package/lib/multiserver.js +5 -4
- package/lib/sql.js +103 -6
- package/package.json +1 -1
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.4.0-beta.
|
|
6
|
+
> **v6.4.0-beta.14** — 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
|

|
package/index.js
CHANGED
package/lib/database.js
CHANGED
|
@@ -2406,6 +2406,15 @@ class Database {
|
|
|
2406
2406
|
this._savepointStack = [];
|
|
2407
2407
|
if (!snap) return;
|
|
2408
2408
|
this._restoreTxSnapshot(snap);
|
|
2409
|
+
// 修复 B1:rollback 后必须落盘,否则事务期间已 flush 到磁盘的写入
|
|
2410
|
+
// 不会被撤销,kill -9 重启后数据不一致(ACID 原子性被破坏)。
|
|
2411
|
+
// dirMode 下事务写入已逐表 flush,rollback 后需重新 flush 恢复后的数据;
|
|
2412
|
+
// 非 dirMode 下 commitTx 会 save(),rollback 也要 save() 保持一致。
|
|
2413
|
+
if (this._dirMode) {
|
|
2414
|
+
try { this._flushDirty(); } catch (e) { this._emit('error', e); }
|
|
2415
|
+
} else if (!this._memoryMode) {
|
|
2416
|
+
this.save();
|
|
2417
|
+
}
|
|
2409
2418
|
}
|
|
2410
2419
|
|
|
2411
2420
|
// ============================================================
|
package/lib/multiserver.js
CHANGED
|
@@ -21,10 +21,8 @@ const { MongoServer } = require('./mongo_server');
|
|
|
21
21
|
|
|
22
22
|
function sniffProtocol(buf) {
|
|
23
23
|
if (buf.length < 1) return null;
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
if (b === 0x0a || b === 0x0d) return 'mysql';
|
|
27
|
-
// MongoDB: header 前 4 字节 = int32LE 长度,第 13 字节 = opCode(OP_QUERY 2004 / OP_COMPRESSED 2012 / OP_MSG 2013)
|
|
24
|
+
// MongoDB 先检查:opCode 校验更具体,避免消息长度为 256 倍数时
|
|
25
|
+
// 首字节 0x00 被误判为 PG(PG 也用 0x00 开头)
|
|
28
26
|
if (buf.length >= 16) {
|
|
29
27
|
const opc = buf.readInt32LE(12);
|
|
30
28
|
if (opc === 2012 || opc === 2013 || opc === 2004) {
|
|
@@ -32,6 +30,9 @@ function sniffProtocol(buf) {
|
|
|
32
30
|
if (len >= 16 && len <= 48 * 1024 * 1024) return 'mongo';
|
|
33
31
|
}
|
|
34
32
|
}
|
|
33
|
+
const b = buf[0];
|
|
34
|
+
if (b === 0x00) return 'pg';
|
|
35
|
+
if (b === 0x0a || b === 0x0d) return 'mysql';
|
|
35
36
|
return 'redis';
|
|
36
37
|
}
|
|
37
38
|
|
package/lib/sql.js
CHANGED
|
@@ -3621,6 +3621,71 @@ function applyScalarFunction(fnNode, row, ctx) {
|
|
|
3621
3621
|
return v.toString(16);
|
|
3622
3622
|
});
|
|
3623
3623
|
case 'DATABASE': case 'SCHEMA': return row && row.__db !== undefined ? row.__db : 'default';
|
|
3624
|
+
// ===== JSON 函数(B2 修复:此前未实现,静默返回 NULL)=====
|
|
3625
|
+
case 'JSON_OBJECT': {
|
|
3626
|
+
// JSON_OBJECT(k1, v1, k2, v2, ...) → { k1: v1, k2: v2 }
|
|
3627
|
+
const obj = {};
|
|
3628
|
+
for (let i = 0; i + 1 < args.length; i += 2) {
|
|
3629
|
+
const k = String(args[i] == null ? '' : args[i]);
|
|
3630
|
+
const v = args[i + 1];
|
|
3631
|
+
obj[k] = v === undefined ? null : v;
|
|
3632
|
+
}
|
|
3633
|
+
return obj;
|
|
3634
|
+
}
|
|
3635
|
+
case 'JSON_ARRAY': {
|
|
3636
|
+
// JSON_ARRAY(v1, v2, ...) → [v1, v2]
|
|
3637
|
+
return args.map(v => v === undefined ? null : v);
|
|
3638
|
+
}
|
|
3639
|
+
case 'JSON_EXTRACT': {
|
|
3640
|
+
// JSON_EXTRACT(json, '$.key' or '$.a.b') → 值
|
|
3641
|
+
const json = args[0];
|
|
3642
|
+
const path = args[1] == null ? '' : String(args[1]);
|
|
3643
|
+
if (json == null || path == null) return null;
|
|
3644
|
+
// 解析 JSON 字符串或对象
|
|
3645
|
+
let data = json;
|
|
3646
|
+
if (typeof json === 'string') {
|
|
3647
|
+
try { data = JSON.parse(json); } catch (e) { return null; }
|
|
3648
|
+
}
|
|
3649
|
+
// 解析 JSONPath: $.key 或 $.a.b 或 $.a[0].b
|
|
3650
|
+
const m = path.match(/^\$\.?([^.\[]+)/);
|
|
3651
|
+
if (!m) {
|
|
3652
|
+
// 尝试 $ 开头后直接跟 key
|
|
3653
|
+
if (path === '$') return data;
|
|
3654
|
+
return null;
|
|
3655
|
+
}
|
|
3656
|
+
const firstKey = m[1];
|
|
3657
|
+
if (firstKey === '') return data;
|
|
3658
|
+
let current = data;
|
|
3659
|
+
// 逐段解析路径
|
|
3660
|
+
const segments = path.replace(/^\$/, '').split('.');
|
|
3661
|
+
for (const seg of segments) {
|
|
3662
|
+
if (current == null) return null;
|
|
3663
|
+
// 处理数组索引: key[0] 或 [0]
|
|
3664
|
+
const arrMatch = seg.match(/^([^\[]*)\[(\d+)\]$/);
|
|
3665
|
+
if (arrMatch) {
|
|
3666
|
+
const key = arrMatch[1];
|
|
3667
|
+
const idx = parseInt(arrMatch[2], 10);
|
|
3668
|
+
if (key) current = current[key];
|
|
3669
|
+
if (Array.isArray(current)) current = current[idx];
|
|
3670
|
+
else return null;
|
|
3671
|
+
} else {
|
|
3672
|
+
if (typeof current !== 'object' || current === null) return null;
|
|
3673
|
+
current = current[seg];
|
|
3674
|
+
}
|
|
3675
|
+
}
|
|
3676
|
+
return current === undefined ? null : current;
|
|
3677
|
+
}
|
|
3678
|
+
case 'JSON_CONTAINS': {
|
|
3679
|
+
// JSON_CONTAINS(json, value) → 1 or 0
|
|
3680
|
+
const json = args[0];
|
|
3681
|
+
const val = args[1];
|
|
3682
|
+
if (json == null) return null;
|
|
3683
|
+
let data = json;
|
|
3684
|
+
if (typeof json === 'string') {
|
|
3685
|
+
try { data = JSON.parse(json); } catch (e) { return null; }
|
|
3686
|
+
}
|
|
3687
|
+
return JSON.stringify(data).includes(JSON.stringify(val)) ? 1 : 0;
|
|
3688
|
+
}
|
|
3624
3689
|
default:
|
|
3625
3690
|
return null;
|
|
3626
3691
|
}
|
|
@@ -6176,7 +6241,30 @@ class SQLExecutor {
|
|
|
6176
6241
|
all = [{ _virtual: true }];
|
|
6177
6242
|
} else {
|
|
6178
6243
|
const items = statement.from.tables.concat(statement.from.joins.map(j => j.item));
|
|
6179
|
-
|
|
6244
|
+
// B5 修复:自连接(同表多次出现且无别名)时,用 t1/t2 等自动别名(MySQL 惯例),
|
|
6245
|
+
// 而非直接用表名。不同表无别名时仍用表名,保持与 SQL 查询中的表名限定一致。
|
|
6246
|
+
const tableCount = new Map();
|
|
6247
|
+
for (const item of items) {
|
|
6248
|
+
if (item.table) {
|
|
6249
|
+
const c = (tableCount.get(item.table) || 0) + 1;
|
|
6250
|
+
tableCount.set(item.table, c);
|
|
6251
|
+
}
|
|
6252
|
+
}
|
|
6253
|
+
const autoAliasMap = new Map();
|
|
6254
|
+
let autoIdx = 0;
|
|
6255
|
+
const prefix = item => {
|
|
6256
|
+
if (item.alias) return item.alias;
|
|
6257
|
+
if (item.subquery) return item.subquery.alias || item.alias;
|
|
6258
|
+
// 无别名且同表出现多次时生成 t1, t2;否则用表名
|
|
6259
|
+
if (item.table && (tableCount.get(item.table) || 0) > 1) {
|
|
6260
|
+
if (!autoAliasMap.has(item)) {
|
|
6261
|
+
autoIdx++;
|
|
6262
|
+
autoAliasMap.set(item, `t${autoIdx}`);
|
|
6263
|
+
}
|
|
6264
|
+
return autoAliasMap.get(item);
|
|
6265
|
+
}
|
|
6266
|
+
return item.table;
|
|
6267
|
+
};
|
|
6180
6268
|
|
|
6181
6269
|
// 读第一表
|
|
6182
6270
|
const firstItem = statement.from.tables[0];
|
|
@@ -6207,10 +6295,10 @@ class SQLExecutor {
|
|
|
6207
6295
|
// 递归 CTE 迭代中:自身引用绑定到已累积的行
|
|
6208
6296
|
rowsAll = { rows: firstItem._virtualRows.map(r => Object.assign({}, r)), schema: null, columns: null };
|
|
6209
6297
|
} else {
|
|
6210
|
-
rowsAll = await this._readTable(firstItem.table, statement, firstItem
|
|
6298
|
+
rowsAll = await this._readTable(firstItem.table, statement, prefix(firstItem));
|
|
6211
6299
|
}
|
|
6212
6300
|
schema = rowsAll.schema;
|
|
6213
|
-
const firstPrefix = firstItem
|
|
6301
|
+
const firstPrefix = prefix(firstItem);
|
|
6214
6302
|
// 虚拟行分两种,语义不同:
|
|
6215
6303
|
// - _virtualRows:递归 CTE **迭代过程内部**,FROM 是自身引用,列名不带前缀
|
|
6216
6304
|
// (否则递归项里的 `n+1` 取不到值)
|
|
@@ -6238,8 +6326,8 @@ class SQLExecutor {
|
|
|
6238
6326
|
? { rows: (j.item._virtualRows || j.item.subquery._virtualSource).map(r => Object.assign({}, r)) }
|
|
6239
6327
|
: j.item.subquery
|
|
6240
6328
|
? await this.executeSelect(j.item.subquery)
|
|
6241
|
-
: await this._readTable(j.item.table, statement, j.item
|
|
6242
|
-
const rightPrefix = j.item
|
|
6329
|
+
: await this._readTable(j.item.table, statement, prefix(j.item));
|
|
6330
|
+
const rightPrefix = prefix(j.item);
|
|
6243
6331
|
// 迭代内部的虚拟行不加前缀;物化后的派生表要加(与普通子查询一致)
|
|
6244
6332
|
const jRaw = j.item._virtualRows
|
|
6245
6333
|
? j.item._virtualRows
|
|
@@ -6515,7 +6603,16 @@ class SQLExecutor {
|
|
|
6515
6603
|
|
|
6516
6604
|
if (statement.limit !== null) {
|
|
6517
6605
|
const start = statement.offset || 0;
|
|
6518
|
-
|
|
6606
|
+
// B5 修复:超大 LIMIT 时 start + limit 可能超过 Number.MAX_SAFE_INTEGER,
|
|
6607
|
+
// 导致 JS 精度丢失,slice 行为异常。这里加友好错误处理。
|
|
6608
|
+
if (statement.limit > rows.length - start) {
|
|
6609
|
+
// LIMIT 超过实际行数是合法的,直接返回全部剩余行
|
|
6610
|
+
rows = rows.slice(start);
|
|
6611
|
+
} else if (start + statement.limit > Number.MAX_SAFE_INTEGER) {
|
|
6612
|
+
throw new Error(`LIMIT value ${statement.limit} is too large; maximum safe value is ${Number.MAX_SAFE_INTEGER - start}`);
|
|
6613
|
+
} else {
|
|
6614
|
+
rows = rows.slice(start, start + statement.limit);
|
|
6615
|
+
}
|
|
6519
6616
|
}
|
|
6520
6617
|
|
|
6521
6618
|
// 无分组、无聚合的正常输出
|
package/package.json
CHANGED