jsql-neo 5.3.0 → 5.3.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 +17 -9
- package/lib/mongo_server.js +1 -1
- package/lib/query.js +20 -2
- package/lib/sql.js +44 -6
- package/lib/table.js +20 -2
- 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. The browser. **And it fits in one npm package.**
|
|
5
5
|
|
|
6
|
-
> **v5.3.
|
|
6
|
+
> **v5.3.1** — official release build · [github.com/vexify-org/JSQL-neo](https://github.com/vexify-org/JSQL-neo)
|
|
7
7
|
|
|
8
8
|

|
|
9
9
|

|
|
@@ -232,7 +232,7 @@ npm install && npm run build # option 3: from source
|
|
|
232
232
|
Verify:
|
|
233
233
|
|
|
234
234
|
```bash
|
|
235
|
-
node -e "console.log(require('jsql-neo/package.json').version)" # 5.3.
|
|
235
|
+
node -e "console.log(require('jsql-neo/package.json').version)" # 5.3.1
|
|
236
236
|
```
|
|
237
237
|
|
|
238
238
|
### 30-second demo
|
|
@@ -1273,7 +1273,7 @@ jsql mod --engine wasm # switch engine (restart required)
|
|
|
1273
1273
|
|
|
1274
1274
|
```bash
|
|
1275
1275
|
$ jsql version
|
|
1276
|
-
jsql-neo v5.3.
|
|
1276
|
+
jsql-neo v5.3.1
|
|
1277
1277
|
engine: native (napi) | wasm | js
|
|
1278
1278
|
node: v22.0.0 platform: linux x64
|
|
1279
1279
|
```
|
|
@@ -1294,7 +1294,7 @@ jsql tui --memory -q # memory mode, quiet
|
|
|
1294
1294
|
jsql tui --prompt 'db> ' --no-color
|
|
1295
1295
|
```
|
|
1296
1296
|
|
|
1297
|
-
The status bar shows: `db=<name> dialect=<d> mode=<tui|batch> ver=5.3.
|
|
1297
|
+
The status bar shows: `db=<name> dialect=<d> mode=<tui|batch> ver=5.3.1`.
|
|
1298
1298
|
|
|
1299
1299
|
### Keyboard shortcuts
|
|
1300
1300
|
|
|
@@ -3858,7 +3858,7 @@ Data dir: /root/.jsql-neo/data
|
|
|
3858
3858
|
|
|
3859
3859
|
```bash
|
|
3860
3860
|
$ jsql version
|
|
3861
|
-
jsql-neo v5.3.
|
|
3861
|
+
jsql-neo v5.3.1
|
|
3862
3862
|
engine: native (napi) | wasm | js
|
|
3863
3863
|
node: v22.0.0
|
|
3864
3864
|
platform: linux x64
|
|
@@ -4754,7 +4754,7 @@ SOFTWARE.
|
|
|
4754
4754
|
|
|
4755
4755
|
*JSQL-NEO — One engine to rule them all. MySQL. PostgreSQL. MongoDB. Redis. SQL. TypeScript. The browser.*
|
|
4756
4756
|
|
|
4757
|
-
*文档版本:v5.3.
|
|
4757
|
+
*文档版本:v5.3.1 · 最后更新:2026-08-12*
|
|
4758
4758
|
|
|
4759
4759
|
---
|
|
4760
4760
|
|
|
@@ -6524,7 +6524,7 @@ Usage: jsql version
|
|
|
6524
6524
|
|
|
6525
6525
|
输出版本与环境信息:
|
|
6526
6526
|
|
|
6527
|
-
jsql-neo v5.3.
|
|
6527
|
+
jsql-neo v5.3.1
|
|
6528
6528
|
engine: native (napi) | wasm | js
|
|
6529
6529
|
node: v22.0.0
|
|
6530
6530
|
platform: linux x64
|
|
@@ -6788,6 +6788,14 @@ new TUIShell({
|
|
|
6788
6788
|
|
|
6789
6789
|
## 附录 N:版本历史 CHANGELOG
|
|
6790
6790
|
|
|
6791
|
+
### v5.3.1 (2026-08-12)
|
|
6792
|
+
|
|
6793
|
+
**修复**
|
|
6794
|
+
- `MysqlConnection` 未导出导致多协议服务器 MySQL 路由崩溃(`lib/mysql_server.js` 补导出)
|
|
6795
|
+
|
|
6796
|
+
**变更**
|
|
6797
|
+
- 版本号升至 5.3.1(package.json / Mongo `buildInfo` 同步)
|
|
6798
|
+
|
|
6791
6799
|
### v5.3.0 (2026-08-12)
|
|
6792
6800
|
|
|
6793
6801
|
**新增**
|
|
@@ -7146,7 +7154,7 @@ CI(GitHub Actions)矩阵:`node 20/22` × `linux/macos/windows` × `native/
|
|
|
7146
7154
|
|
|
7147
7155
|
*JSQL-NEO — One engine to rule them all. MySQL. PostgreSQL. MongoDB. Redis. SQL. TypeScript. The browser.*
|
|
7148
7156
|
|
|
7149
|
-
*文档版本:v5.3.
|
|
7157
|
+
*文档版本:v5.3.1 · 共 19 个附录 · 最后更新:2026-08-12*
|
|
7150
7158
|
|
|
7151
7159
|
---
|
|
7152
7160
|
|
|
@@ -7753,4 +7761,4 @@ npm test
|
|
|
7753
7761
|
|
|
7754
7762
|
*JSQL-NEO — One engine to rule them all. MySQL. PostgreSQL. MongoDB. Redis. SQL. TypeScript. The browser.*
|
|
7755
7763
|
|
|
7756
|
-
*文档版本:v5.3.
|
|
7764
|
+
*文档版本:v5.3.1 · 附录 A–Z · 全文 6000+ 行 · 最后更新:2026-08-12*
|
package/lib/mongo_server.js
CHANGED
|
@@ -322,7 +322,7 @@ class MongoServer {
|
|
|
322
322
|
localTime: new Date(), logicalSessionTimeoutMinutes: 30, connectionId: 1,
|
|
323
323
|
};
|
|
324
324
|
case 'ping': return { ok: 1 };
|
|
325
|
-
case 'buildInfo': return { ok: 1, version: '5.3.
|
|
325
|
+
case 'buildInfo': return { ok: 1, version: '5.3.1-jsql-neo', gitVersion: 'jsql-neo', versionArray: [5, 3, 1, 0] };
|
|
326
326
|
case 'getParameter': return { ok: 1 };
|
|
327
327
|
case 'endSessions': return { ok: 1 };
|
|
328
328
|
case 'listDatabases':
|
package/lib/query.js
CHANGED
|
@@ -537,7 +537,7 @@ class Query {
|
|
|
537
537
|
const key = fr[join.foreignField];
|
|
538
538
|
const matches = localRows.filter(lr => lr[join.localField] === key);
|
|
539
539
|
if (matches.length === 0) {
|
|
540
|
-
result.push(
|
|
540
|
+
result.push(this._rightNullRow(localRows, fr, join));
|
|
541
541
|
} else {
|
|
542
542
|
for (const lr of matches) {
|
|
543
543
|
result.push(this._mergeJoinRow(lr, fr, join));
|
|
@@ -582,7 +582,7 @@ class Query {
|
|
|
582
582
|
for (const fr of foreignRows) {
|
|
583
583
|
const matches = localRows.filter(lr => lr[join.localField] === fr[join.foreignField]);
|
|
584
584
|
if (matches.length === 0) {
|
|
585
|
-
result.push(
|
|
585
|
+
result.push(this._rightNullRow(localRows, fr, join));
|
|
586
586
|
} else {
|
|
587
587
|
for (const lr of matches) {
|
|
588
588
|
result.push(this._mergeJoinRow(lr, fr, join));
|
|
@@ -621,6 +621,24 @@ class Query {
|
|
|
621
621
|
return nulls;
|
|
622
622
|
}
|
|
623
623
|
|
|
624
|
+
/**
|
|
625
|
+
* RIGHT JOIN 未匹配右表行:保留右表数据,本地表字段填 null。
|
|
626
|
+
* 返回 { localNulls, merged },其中 merged = { ...nulls, ...fr }
|
|
627
|
+
*/
|
|
628
|
+
_rightNullRow(localRows, foreignRow, join) {
|
|
629
|
+
const localSchema = this._table._schema || {};
|
|
630
|
+
const localNulls = {};
|
|
631
|
+
for (const field of Object.keys(localSchema)) {
|
|
632
|
+
if (field !== '_softDelete') localNulls[field] = null;
|
|
633
|
+
}
|
|
634
|
+
const prefix = join.as ? join.as + '_' : '';
|
|
635
|
+
const foreignPrefixed = {};
|
|
636
|
+
for (const [key, value] of Object.entries(foreignRow)) {
|
|
637
|
+
foreignPrefixed[prefix + key] = value;
|
|
638
|
+
}
|
|
639
|
+
return { ...localNulls, ...foreignPrefixed };
|
|
640
|
+
}
|
|
641
|
+
|
|
624
642
|
// ============================================================
|
|
625
643
|
// 内部: 排序
|
|
626
644
|
// ============================================================
|
package/lib/sql.js
CHANGED
|
@@ -261,12 +261,18 @@ class Parser {
|
|
|
261
261
|
if (this.peek().type === 'op' && this.peek().value === '.') {
|
|
262
262
|
this.next();
|
|
263
263
|
const t2 = this.next();
|
|
264
|
-
if (t2.type !== 'ident'
|
|
264
|
+
if (t2.type !== 'ident' && !(t2.type === 'keyword' && this._isSchemaView(t2.value))) {
|
|
265
|
+
throw new Error(`Expected table name after '.', got '${t2.value}'`);
|
|
266
|
+
}
|
|
265
267
|
return t.value + '.' + t2.value;
|
|
266
268
|
}
|
|
267
269
|
return t.value;
|
|
268
270
|
}
|
|
269
271
|
|
|
272
|
+
_isSchemaView(v) {
|
|
273
|
+
return ['TABLES', 'COLUMNS', 'SCHEMATA', 'STATISTICS', 'KEY_COLUMN_USAGE', 'REFERENTIAL_CONSTRAINTS', 'TABLE_CONSTRAINTS', 'VIEWS'].includes(String(v).toUpperCase());
|
|
274
|
+
}
|
|
275
|
+
|
|
270
276
|
parseCreateTable() {
|
|
271
277
|
this.expectKeyword('CREATE');
|
|
272
278
|
this.expectKeyword('TABLE');
|
|
@@ -1870,11 +1876,12 @@ class SQLExecutor {
|
|
|
1870
1876
|
const schema = this.engine.getTableSchema
|
|
1871
1877
|
? await this.engine.getTableSchema(statement.table)
|
|
1872
1878
|
: (this.engine._schemas ? this.engine._schemas[statement.table] : null);
|
|
1879
|
+
const pkCols = schema ? Object.keys(schema).filter(k => schema[k].primaryKey) : [];
|
|
1873
1880
|
const all = (await this.engine.find(statement.table, {}, { limit: 1e9, offset: 0 })).map(r => normalizeRow(r, schema));
|
|
1874
1881
|
let count = 0;
|
|
1875
1882
|
for (const row of all) {
|
|
1876
1883
|
if (!statement.where || evaluateExpr(statement.where, row, this.ctx)) {
|
|
1877
|
-
const id =
|
|
1884
|
+
const id = this._rowPkId(row, pkCols);
|
|
1878
1885
|
if (id !== undefined) {
|
|
1879
1886
|
const data = {};
|
|
1880
1887
|
for (const [col, val] of statement.assignments) {
|
|
@@ -1892,11 +1899,12 @@ class SQLExecutor {
|
|
|
1892
1899
|
const schema = this.engine.getTableSchema
|
|
1893
1900
|
? await this.engine.getTableSchema(statement.table)
|
|
1894
1901
|
: (this.engine._schemas ? this.engine._schemas[statement.table] : null);
|
|
1902
|
+
const pkCols = schema ? Object.keys(schema).filter(k => schema[k].primaryKey) : [];
|
|
1895
1903
|
const all = (await this.engine.find(statement.table, {}, { limit: 1e9, offset: 0 })).map(r => normalizeRow(r, schema));
|
|
1896
1904
|
const ids = [];
|
|
1897
1905
|
for (const row of all) {
|
|
1898
1906
|
if (!statement.where || evaluateExpr(statement.where, row, this.ctx)) {
|
|
1899
|
-
const id =
|
|
1907
|
+
const id = this._rowPkId(row, pkCols);
|
|
1900
1908
|
if (id !== undefined) ids.push(id);
|
|
1901
1909
|
}
|
|
1902
1910
|
}
|
|
@@ -2166,6 +2174,19 @@ class SQLExecutor {
|
|
|
2166
2174
|
return schema;
|
|
2167
2175
|
}
|
|
2168
2176
|
|
|
2177
|
+
/**
|
|
2178
|
+
* 取行的行 ID:优先内部 _rid,否则用实际主键字段值(不再硬编码 id)。
|
|
2179
|
+
*/
|
|
2180
|
+
_rowPkId(row, pkCols) {
|
|
2181
|
+
if (row && row._rid !== undefined) return row._rid;
|
|
2182
|
+
if (pkCols.length > 0) {
|
|
2183
|
+
for (const c of pkCols) {
|
|
2184
|
+
if (row[c] !== undefined && row[c] !== null) return row[c];
|
|
2185
|
+
}
|
|
2186
|
+
}
|
|
2187
|
+
return row ? row.id : undefined;
|
|
2188
|
+
}
|
|
2189
|
+
|
|
2169
2190
|
async _readTable(table) {
|
|
2170
2191
|
const schema = await this._getSchema(table);
|
|
2171
2192
|
const rows = (await this.engine.find(table, {}, { limit: 1e9, offset: 0 })).map(r => normalizeRow(r, schema));
|
|
@@ -2366,6 +2387,19 @@ class SQLExecutor {
|
|
|
2366
2387
|
return out;
|
|
2367
2388
|
}
|
|
2368
2389
|
|
|
2390
|
+
/**
|
|
2391
|
+
* 生成对端表的前缀 null 行:仅含 `prefix.col` 键(值为 null),
|
|
2392
|
+
* 用于 JOIN 未匹配行补齐限定列,避免回退到未前缀副本拿错值。
|
|
2393
|
+
*/
|
|
2394
|
+
_nullPrefixedRow(schema, prefix) {
|
|
2395
|
+
const out = {};
|
|
2396
|
+
for (const k of Object.keys(schema)) {
|
|
2397
|
+
if (k === '_softDelete') continue;
|
|
2398
|
+
out[prefix + '.' + k] = null;
|
|
2399
|
+
}
|
|
2400
|
+
return out;
|
|
2401
|
+
}
|
|
2402
|
+
|
|
2369
2403
|
_aggValue(rows, fn, column) {
|
|
2370
2404
|
if (fn === 'COUNT') return rows.length;
|
|
2371
2405
|
const op = typeof column === 'string' ? { type: 'column', name: column } : column;
|
|
@@ -2445,6 +2479,7 @@ class SQLExecutor {
|
|
|
2445
2479
|
|
|
2446
2480
|
let schema = null;
|
|
2447
2481
|
let all;
|
|
2482
|
+
let rowsAll;
|
|
2448
2483
|
if (!statement.from) {
|
|
2449
2484
|
// 无 FROM:虚拟行(SELECT 1, 'a')
|
|
2450
2485
|
all = [{ _virtual: true }];
|
|
@@ -2469,7 +2504,6 @@ class SQLExecutor {
|
|
|
2469
2504
|
const rows = filtered.map(r => outCols.map(c => (c in r ? r[c] : null)));
|
|
2470
2505
|
return { ok: true, type: 'select', table: firstItem.table, columns: outCols, rows, raw: filtered };
|
|
2471
2506
|
}
|
|
2472
|
-
let rowsAll;
|
|
2473
2507
|
if (firstItem.subquery) {
|
|
2474
2508
|
const res = await this.executeSelect(firstItem.subquery);
|
|
2475
2509
|
rowsAll = { rows: this._subQueryRows(res), schema: null, columns: res.columns };
|
|
@@ -2495,6 +2529,10 @@ class SQLExecutor {
|
|
|
2495
2529
|
}
|
|
2496
2530
|
const matched = [];
|
|
2497
2531
|
const unmatchedRight = new Set(rightRows.map((r, i) => i));
|
|
2532
|
+
// 未匹配行补对端表的前缀 null 列:限定列名(如 a.id / b.id)按前缀解析,
|
|
2533
|
+
// 避免回退到未前缀副本拿到错误值。
|
|
2534
|
+
const rightNulls = (rightRes.schema) ? this._nullPrefixedRow(rightRes.schema, rightPrefix) : null;
|
|
2535
|
+
const leftNulls = (rowsAll && rowsAll.schema) ? this._nullPrefixedRow(rowsAll.schema, firstPrefix) : null;
|
|
2498
2536
|
rows.forEach(l => {
|
|
2499
2537
|
let m = null;
|
|
2500
2538
|
for (let ri = 0; ri < rightRows.length; ri++) {
|
|
@@ -2507,11 +2545,11 @@ class SQLExecutor {
|
|
|
2507
2545
|
matched.push({ ...l, ...rightRows[m] });
|
|
2508
2546
|
unmatchedRight.delete(m);
|
|
2509
2547
|
} else if (j.type === 'left') {
|
|
2510
|
-
matched.push({ ...l });
|
|
2548
|
+
matched.push(rightNulls ? { ...rightNulls, ...l } : { ...l });
|
|
2511
2549
|
}
|
|
2512
2550
|
});
|
|
2513
2551
|
if (j.type === 'right') {
|
|
2514
|
-
for (const ri of unmatchedRight) matched.push({ ...rightRows[ri] });
|
|
2552
|
+
for (const ri of unmatchedRight) matched.push(leftNulls ? { ...leftNulls, ...rightRows[ri] } : { ...rightRows[ri] });
|
|
2515
2553
|
}
|
|
2516
2554
|
rows = matched;
|
|
2517
2555
|
}
|
package/lib/table.js
CHANGED
|
@@ -242,6 +242,23 @@ class Table {
|
|
|
242
242
|
const schema = this._schema;
|
|
243
243
|
const checkConstraints = this._checkConstraints;
|
|
244
244
|
const foreignKeys = this._foreignKeys;
|
|
245
|
+
// 批量预分配自增 ID 范围:先扫描显式提供的最大值,一次性推进计数器,
|
|
246
|
+
// 再在循环内用本地序号递增,避免批量内重复读取/写入同一计数器字段。
|
|
247
|
+
const baseAutoInc = this._autoIncrement;
|
|
248
|
+
if (autoIncField) {
|
|
249
|
+
let maxExplicit = 0;
|
|
250
|
+
for (const it of items) {
|
|
251
|
+
const v = it[autoIncField];
|
|
252
|
+
if (v !== undefined && v !== null) {
|
|
253
|
+
const n = Number(v);
|
|
254
|
+
if (!isNaN(n) && n > maxExplicit) maxExplicit = n;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
if (maxExplicit > baseAutoInc) {
|
|
258
|
+
this._autoIncrement = maxExplicit;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
let autoIncSeq = 0;
|
|
245
262
|
for (let i = 0; i < N; i++) {
|
|
246
263
|
let data = items[i];
|
|
247
264
|
for (const [f, dv] of Object.entries(defaults)) {
|
|
@@ -279,8 +296,9 @@ class Table {
|
|
|
279
296
|
}
|
|
280
297
|
}
|
|
281
298
|
if (autoIncField && data[autoIncField] === undefined) {
|
|
282
|
-
|
|
283
|
-
data[autoIncField] =
|
|
299
|
+
autoIncSeq++;
|
|
300
|
+
data[autoIncField] = baseAutoInc + autoIncSeq;
|
|
301
|
+
this._autoIncrement = data[autoIncField];
|
|
284
302
|
} else if (autoIncField && data[autoIncField] > this._autoIncrement) {
|
|
285
303
|
this._autoIncrement = data[autoIncField];
|
|
286
304
|
}
|