jsql-neo 6.5.26 → 6.5.28
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 +26 -1
- package/bin/jsql +32 -0
- package/index.d.ts +8 -1
- package/index.js +5 -1
- package/lib/database.js +5 -1
- package/lib/mongo_server.js +21 -5
- package/lib/mysql_compat.js +1 -1
- package/lib/sql.js +52 -1
- package/lib/table.js +139 -38
- package/package.json +3 -3
- package/postinstall.js +136 -6
- package/test/coverage.js +3 -1
- package/test/finalize-bigint.test.js +55 -0
- package/test/issue-44.test.js +92 -0
- package/test/issue-45.test.js +62 -0
- package/test/issue-46.test.js +42 -0
- package/test/issue-47.test.js +65 -0
- package/test/issue-48.test.js +39 -0
- package/test/native-cli.test.js +122 -0
- package/test/native-install.test.js +93 -0
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.5.
|
|
6
|
+
> **v6.5.28** — stable release · [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
|

|
|
@@ -171,6 +171,31 @@ await db.start();
|
|
|
171
171
|
|
|
172
172
|
---
|
|
173
173
|
|
|
174
|
+
## Query results
|
|
175
|
+
|
|
176
|
+
`db.query()` / `executeSQL()` return **one result in three shapes**:
|
|
177
|
+
|
|
178
|
+
```js
|
|
179
|
+
const r = await db.query('SELECT city, COUNT(*) AS c FROM users GROUP BY city');
|
|
180
|
+
r.columns // ['city', 'c']
|
|
181
|
+
r.rows // [['LA', 1], ['NY', 2]]
|
|
182
|
+
r.raw // [{ city: 'LA', c: 1 }, { city: 'NY', c: 2 }]
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
`raw` is the object form of `rows`: its keys are exactly `columns`, and that
|
|
186
|
+
includes computed, aggregate and scalar-subquery columns. It is **not** the
|
|
187
|
+
pre-projection source row. For SELECT results `raw.length === rows.length`
|
|
188
|
+
always holds; non-SELECT statements may omit `raw`.
|
|
189
|
+
|
|
190
|
+
To check which native addon this installation is actually using:
|
|
191
|
+
|
|
192
|
+
- `jsql native status` — tag/version match (`0` = OK, `1` = mismatch or unverified)
|
|
193
|
+
- `jsql native verify` — re-hashes the `.node` on disk against the checksum recorded
|
|
194
|
+
at install time, so a swapped or corrupted binary is caught too. `--json` prints
|
|
195
|
+
a structured result for CI. States: `OK` / `MISMATCH` / `CORRUPT` / `MISSING` /
|
|
196
|
+
`UNVERIFIED` / `UNREADABLE`.
|
|
197
|
+
- `jsql native refresh` — re-fetches a checksum-verified addon
|
|
198
|
+
|
|
174
199
|
## ✨ XQL 集成
|
|
175
200
|
|
|
176
201
|
用 [XQL](https://github.com/vexify-org/XQL) 查询 JSQL 表:把 SQL 直接接到文档上,
|
package/bin/jsql
CHANGED
|
@@ -531,6 +531,38 @@ const cli = (yaggs || require('./yaggs-fallback').buildYaggsFallback(_pkg))({
|
|
|
531
531
|
.command('update', 'Update jsql-neo to the latest version', updateOptions, updateHandler)
|
|
532
532
|
.command('uninstall', 'Remove jsql user data and/or the global package', uninstallOptions, uninstallHandler)
|
|
533
533
|
.command('remove', 'Alias of uninstall', uninstallOptions, uninstallHandler)
|
|
534
|
+
.command('native', 'Inspect or refresh the Rust native addon', (sub) => {
|
|
535
|
+
sub.option('json', { type: 'boolean', description: 'Machine-readable output (verify only)' });
|
|
536
|
+
}, async (argv) => {
|
|
537
|
+
const action = argv._[0] || 'status';
|
|
538
|
+
const pi = require('../postinstall.js');
|
|
539
|
+
if (action === 'verify') {
|
|
540
|
+
// 内容级:重算 .node 的 SHA256 与安装记录比对,status 只看 tag 发现不了替换/损坏
|
|
541
|
+
const r = pi.verifyNative();
|
|
542
|
+
if (argv.json) {
|
|
543
|
+
console.log(JSON.stringify(r, null, 2));
|
|
544
|
+
} else {
|
|
545
|
+
console.log(`[jsql-neo] VERIFY : ${r.state}`);
|
|
546
|
+
console.log(`[jsql-neo] tag : ${r.installedTag || 'unknown'} (expect ${r.expectedTag})`);
|
|
547
|
+
console.log(`[jsql-neo] sha256 : ${r.actualSha256 || 'n/a'}`);
|
|
548
|
+
if (r.state !== 'OK') console.log('[jsql-neo] refresh with: jsql native refresh');
|
|
549
|
+
}
|
|
550
|
+
process.exitCode = r.state === 'OK' ? 0 : 1;
|
|
551
|
+
return;
|
|
552
|
+
}
|
|
553
|
+
if (action === 'status') {
|
|
554
|
+
// 0 = 当前 .node 与包版本匹配;1 = 错配 / 未记录(可直接当 CI 门禁)
|
|
555
|
+
process.exitCode = pi.nativeStatus();
|
|
556
|
+
return;
|
|
557
|
+
}
|
|
558
|
+
if (action === 'refresh') {
|
|
559
|
+
await pi.ensureNativeNode();
|
|
560
|
+
process.exitCode = pi.nativeStatus();
|
|
561
|
+
return;
|
|
562
|
+
}
|
|
563
|
+
console.error(`Unknown native action: ${action}. Usage: jsql native <status|refresh|verify>`);
|
|
564
|
+
process.exitCode = 1;
|
|
565
|
+
})
|
|
534
566
|
.command('version', 'Show version', null, () => {
|
|
535
567
|
console.log(require('../package.json').version);
|
|
536
568
|
})
|
package/index.d.ts
CHANGED
|
@@ -272,9 +272,16 @@ declare namespace JSQLNeo {
|
|
|
272
272
|
ok: boolean;
|
|
273
273
|
type: string;
|
|
274
274
|
table?: string | null;
|
|
275
|
+
/** 投影列名;rows[i][j] 与 columns[j] 一一对应。 */
|
|
275
276
|
columns?: string[];
|
|
277
|
+
/** 投影后的二维数组形式。 */
|
|
276
278
|
rows?: unknown[][];
|
|
277
|
-
|
|
279
|
+
/**
|
|
280
|
+
* 与 rows 指向**同一份结果**的对象形式:raw[i] 的键恰好是 columns,
|
|
281
|
+
* 值取自 rows[i]。聚合 / 计算列 / 标量子查询都包含在内(issue #43)。
|
|
282
|
+
* 非 SELECT 语句可能没有此字段。
|
|
283
|
+
*/
|
|
284
|
+
raw?: Array<Record<string, unknown>>;
|
|
278
285
|
affectedRows?: number;
|
|
279
286
|
insertId?: number | null;
|
|
280
287
|
ids?: unknown[];
|
package/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* JSQL-NEO v6.5.
|
|
2
|
+
* JSQL-NEO v6.5.28 — Rust-Powered Embedded Database (WASM + HTTP)
|
|
3
3
|
*
|
|
4
4
|
* @example
|
|
5
5
|
* const jsql = require('jsql-neo');
|
|
@@ -153,6 +153,10 @@ module.exports = {
|
|
|
153
153
|
SQL: sql,
|
|
154
154
|
executeSQL: sql.executeSQL,
|
|
155
155
|
parseSQL: sql.parseSQL,
|
|
156
|
+
// Issue #46:index.d.ts 早就声明了这三个顶层导出,index.js 却一直没给
|
|
157
|
+
applyParams: sql.applyParams,
|
|
158
|
+
escapeValue: sql.escapeValue,
|
|
159
|
+
escapeId: sql.escapeId,
|
|
156
160
|
// AST 访问与改写(5.5.0+)
|
|
157
161
|
AST: sql.AST,
|
|
158
162
|
walk: sql.walk,
|
package/lib/database.js
CHANGED
|
@@ -1837,7 +1837,11 @@ class Database {
|
|
|
1837
1837
|
|
|
1838
1838
|
_resolveId(table, id) {
|
|
1839
1839
|
const pk = this._pkOf(table);
|
|
1840
|
-
if (pk)
|
|
1840
|
+
if (pk) {
|
|
1841
|
+
// Issue #44:主键查询值按列类型归一('5' → 5),与索引/insert 保持一致
|
|
1842
|
+
const key = typeof table._coerceQueryValue === 'function' ? table._coerceQueryValue(pk, id) : id;
|
|
1843
|
+
return table._rows.find(r => r[pk] === key || r[pk] === id) || null;
|
|
1844
|
+
}
|
|
1841
1845
|
// 无主键表:id 是行对象(可能是 find() 返回的浅拷贝)时,用内容匹配代替引用比较。
|
|
1842
1846
|
// 做法:逐行比对所有字段值是否相等;是数字时当 1-based 行号。
|
|
1843
1847
|
if (id && typeof id === 'object') {
|
package/lib/mongo_server.js
CHANGED
|
@@ -430,9 +430,16 @@ class MongoServer {
|
|
|
430
430
|
for (const d of docs) {
|
|
431
431
|
const doc2 = { ...d };
|
|
432
432
|
// 检查 _id 唯一性
|
|
433
|
+
// Issue #44:索引查找是严格相等的,空结果不能直接当「不存在」——
|
|
434
|
+
// 查询值与已存值的类型不一致时会漏判。这里再加一道按 schema 归一的直接扫描。
|
|
433
435
|
if (doc2._id !== undefined) {
|
|
434
436
|
const existing = await engine.find(coll, { _id: doc2._id });
|
|
435
437
|
if (existing.length > 0) continue; // 跳过重复 _id
|
|
438
|
+
const table = typeof engine._ensureTable === 'function' ? engine._ensureTable(coll) : null;
|
|
439
|
+
if (table && Array.isArray(table._rows) && typeof table._coerceQueryValue === 'function') {
|
|
440
|
+
const idKey = table._coerceQueryValue('_id', doc2._id);
|
|
441
|
+
if (table._rows.some((r) => r && !r._deleted && r._id === idKey)) continue;
|
|
442
|
+
}
|
|
436
443
|
}
|
|
437
444
|
await engine.insert(coll, doc2);
|
|
438
445
|
ids.push(doc2._id);
|
|
@@ -568,10 +575,13 @@ class MongoServer {
|
|
|
568
575
|
for (const [k, dir] of sortKeys) {
|
|
569
576
|
const av = a[k]; const bv = b[k];
|
|
570
577
|
if (av === bv) continue;
|
|
571
|
-
|
|
572
|
-
|
|
578
|
+
const desc = Number(dir) < 0;
|
|
579
|
+
// Issue #47:NULL 的位置随排序方向走(升序在前、降序在后),
|
|
580
|
+
// 与 MongoDB 一致;此前固定返回 1/-1,升序时 null 恒排最后。
|
|
581
|
+
if (av == null) return desc ? 1 : -1;
|
|
582
|
+
if (bv == null) return desc ? -1 : 1;
|
|
573
583
|
const cmp = av < bv ? -1 : 1;
|
|
574
|
-
return
|
|
584
|
+
return desc ? -cmp : cmp;
|
|
575
585
|
}
|
|
576
586
|
return 0;
|
|
577
587
|
});
|
|
@@ -643,8 +653,14 @@ class MongoServer {
|
|
|
643
653
|
const sum = grow.reduce((s, r) => s + (Number(r[field]) || 0), 0);
|
|
644
654
|
doc[k] = (doc[k] || 0) + sum;
|
|
645
655
|
} else if (v.$avg !== undefined) {
|
|
646
|
-
|
|
647
|
-
|
|
656
|
+
// Issue #45:null/undefined 不计入分子的同时也不计入分母;
|
|
657
|
+
// 全空组返回 null(MongoDB 语义),而不是 0。
|
|
658
|
+
const vals = grow
|
|
659
|
+
.map(r => r[field])
|
|
660
|
+
.filter(x => x !== null && x !== undefined)
|
|
661
|
+
.map(Number)
|
|
662
|
+
.filter(x => !Number.isNaN(x));
|
|
663
|
+
doc[k] = vals.length ? vals.reduce((a, b) => a + b, 0) / vals.length : null;
|
|
648
664
|
} else if (v.$min !== undefined) {
|
|
649
665
|
const vals = grow.map(r => r[field]).filter(v => v != null);
|
|
650
666
|
let best = null;
|
package/lib/mysql_compat.js
CHANGED
|
@@ -20,7 +20,7 @@ function toResultPacket(r, table, rowsAsArray) {
|
|
|
20
20
|
const fields = columns.map(name => {
|
|
21
21
|
let type = 253; // VAR_STRING
|
|
22
22
|
const val = rawRows[0] ? rawRows[0][columns.indexOf(name)] : null;
|
|
23
|
-
if (typeof val === 'number') type = 8; // LONGLONG
|
|
23
|
+
if (typeof val === 'number') type = Number.isInteger(val) ? 8 : 5; // LONGLONG / DOUBLE(Issue #48:浮点列曾被误报为 8)
|
|
24
24
|
else if (typeof val === 'boolean') type = 1; // TINY
|
|
25
25
|
else if (val instanceof Date) type = 12; // DATETIME
|
|
26
26
|
else if (Buffer.isBuffer(val)) type = 252; // BLOB
|
package/lib/sql.js
CHANGED
|
@@ -8337,6 +8337,57 @@ function finalizeBigInts(value, seen) {
|
|
|
8337
8337
|
return value;
|
|
8338
8338
|
}
|
|
8339
8339
|
|
|
8340
|
+
/**
|
|
8341
|
+
* SQLResult 专用的 BigInt 收尾 —— 保留语义,去掉 20 万次递归和 Set.add。
|
|
8342
|
+
*
|
|
8343
|
+
* 通用 finalizeBigInts 会对 rows(10 万个数组)+ raw(10 万个对象)逐个
|
|
8344
|
+
* 深递归,每进一个对象就 seen.add 一次。node --prof 显示光
|
|
8345
|
+
* SetPrototypeAdd 就占 18 ticks(约 14% JS),加上 finalizeBigInts 自身
|
|
8346
|
+
* 共 ~23% JS ticks,整体 33% 时间花在 GC 上。
|
|
8347
|
+
*
|
|
8348
|
+
* 而结果的形状是规则的:rows 是二维数组,raw 是对象数组,元素绝大多数是
|
|
8349
|
+
* number / string / null —— 根本不可能是 BigInt。用紧凑循环只做 typeof
|
|
8350
|
+
* 检查,遇到嵌套对象才退回通用实现。语义与深递归等价,但不再给每个
|
|
8351
|
+
* 标量行建 Set 条目。
|
|
8352
|
+
*/
|
|
8353
|
+
function finalizeResultBigInts(r) {
|
|
8354
|
+
if (!r || typeof r !== 'object' || Array.isArray(r)) return finalizeBigInts(r);
|
|
8355
|
+
|
|
8356
|
+
if (Array.isArray(r.rows)) {
|
|
8357
|
+
const rows = r.rows;
|
|
8358
|
+
for (let i = 0; i < rows.length; i++) {
|
|
8359
|
+
const row = rows[i];
|
|
8360
|
+
if (!Array.isArray(row)) { rows[i] = finalizeBigInts(row); continue; }
|
|
8361
|
+
for (let j = 0; j < row.length; j++) {
|
|
8362
|
+
const v = row[j];
|
|
8363
|
+
if (typeof v === 'bigint') row[j] = v.toString();
|
|
8364
|
+
else if (v && typeof v === 'object') row[j] = finalizeBigInts(v);
|
|
8365
|
+
}
|
|
8366
|
+
}
|
|
8367
|
+
}
|
|
8368
|
+
|
|
8369
|
+
if (Array.isArray(r.raw)) {
|
|
8370
|
+
const raw = r.raw;
|
|
8371
|
+
for (let i = 0; i < raw.length; i++) {
|
|
8372
|
+
const obj = raw[i];
|
|
8373
|
+
if (!obj || typeof obj !== 'object' || Array.isArray(obj)) continue;
|
|
8374
|
+
for (const k of Object.keys(obj)) {
|
|
8375
|
+
const v = obj[k];
|
|
8376
|
+
if (typeof v === 'bigint') obj[k] = v.toString();
|
|
8377
|
+
else if (v && typeof v === 'object') obj[k] = finalizeBigInts(v);
|
|
8378
|
+
}
|
|
8379
|
+
}
|
|
8380
|
+
}
|
|
8381
|
+
|
|
8382
|
+
// 其余字段(columns / insertId / ids / columnTypes / 元数据)都很小,
|
|
8383
|
+
// 走通用实现,保证不漏掉任何可能含 BigInt 的位置。
|
|
8384
|
+
for (const k of Object.keys(r)) {
|
|
8385
|
+
if (k === 'rows' || k === 'raw') continue;
|
|
8386
|
+
r[k] = finalizeBigInts(r[k]);
|
|
8387
|
+
}
|
|
8388
|
+
return r;
|
|
8389
|
+
}
|
|
8390
|
+
|
|
8340
8391
|
async function executeSQL(engine, sql, paramsOrOpts, opts = {}) {
|
|
8341
8392
|
// 参数守卫:engine 必须是引擎对象,sql 必须是字符串。误用时报清晰错误,
|
|
8342
8393
|
// 而不是在后续像 `reading 'length'` 那样抛出令人费解的 TypeError。
|
|
@@ -8410,7 +8461,7 @@ async function executeSQL(engine, sql, paramsOrOpts, opts = {}) {
|
|
|
8410
8461
|
let r = await executor.execute(stmt);
|
|
8411
8462
|
const durationMs = Number(process.hrtime.bigint() - t0) / 1e6;
|
|
8412
8463
|
r = buildResultEnvelope(stmt, r, Math.round(durationMs * 1000) / 1000);
|
|
8413
|
-
r =
|
|
8464
|
+
r = finalizeResultBigInts(r);
|
|
8414
8465
|
if (r && r.columns && r.columnTypes === undefined) {
|
|
8415
8466
|
r.columnTypes = await inferColumnTypes(engine, stmt, r);
|
|
8416
8467
|
}
|
package/lib/table.js
CHANGED
|
@@ -456,24 +456,25 @@ class Table {
|
|
|
456
456
|
|
|
457
457
|
findById(id) {
|
|
458
458
|
if (!this._primaryKey) throw createError('ER_PARSE_ERROR', 'Table has no primary key');
|
|
459
|
+
// Issue #44:字符串主键查询值先按列类型归一,否则索引静默未命中
|
|
460
|
+
const key = this._coerceQueryValue(this._primaryKey, id);
|
|
459
461
|
// Hash 索引 O(1)
|
|
460
462
|
if (this._pkIndex) {
|
|
461
|
-
const rowIndex = this._pkIndex.get(
|
|
463
|
+
const rowIndex = this._pkIndex.get(key);
|
|
462
464
|
if (rowIndex !== undefined) {
|
|
463
465
|
const row = this._rows[rowIndex];
|
|
464
466
|
if (row && !row._deleted) return { ...row };
|
|
465
467
|
}
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
if (this._btrees[this._primaryKey]) {
|
|
469
|
-
const indices = this._btrees[this._primaryKey].search(id);
|
|
468
|
+
} else if (this._btrees[this._primaryKey]) {
|
|
469
|
+
const indices = this._btrees[this._primaryKey].search(key);
|
|
470
470
|
if (indices.length > 0) {
|
|
471
471
|
const row = this._rows[indices[0]];
|
|
472
472
|
if (row && !row._deleted) return { ...row };
|
|
473
473
|
}
|
|
474
|
-
return null;
|
|
475
474
|
}
|
|
476
|
-
|
|
475
|
+
// 索引未命中不等于不存在:回落主键列全表匹配(类型差异 / 索引未覆盖的行)
|
|
476
|
+
const row = this._rows.find(r => r && !r._deleted && r[this._primaryKey] === key);
|
|
477
|
+
return row ? { ...row } : null;
|
|
477
478
|
}
|
|
478
479
|
|
|
479
480
|
findAll() {
|
|
@@ -533,24 +534,25 @@ class Table {
|
|
|
533
534
|
updates = hook({ [this._primaryKey]: id }, { ...updates }) || updates;
|
|
534
535
|
}
|
|
535
536
|
updates = this._validateDataTypes(updates, true);
|
|
537
|
+
const pkKey = this._coerceQueryValue(this._primaryKey, id);
|
|
536
538
|
let rowIndex;
|
|
537
539
|
if (this._pkIndex) {
|
|
538
|
-
rowIndex = this._pkIndex.get(
|
|
539
|
-
} else {
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
540
|
+
rowIndex = this._pkIndex.get(pkKey);
|
|
541
|
+
} else if (this._btrees[this._primaryKey]) {
|
|
542
|
+
const indices = this._btrees[this._primaryKey].search(pkKey);
|
|
543
|
+
rowIndex = indices.length > 0 ? indices[0] : undefined;
|
|
544
|
+
}
|
|
545
|
+
if (rowIndex === undefined) {
|
|
546
|
+
// Issue #44:索引未命中时回落全表匹配
|
|
547
|
+
const found = this._rows.findIndex(r => r && !r._deleted && r[this._primaryKey] === pkKey);
|
|
548
|
+
if (found !== -1) rowIndex = found;
|
|
547
549
|
}
|
|
548
550
|
if (rowIndex === undefined) return 0;
|
|
549
551
|
const row = this._rows[rowIndex];
|
|
550
552
|
if (row && row._deleted) return 0;
|
|
551
553
|
// P1-3:主键被更新时传播到外键引用(CASCADE / SET NULL / RESTRICT)
|
|
552
|
-
if (updates[this._primaryKey] !== undefined && updates[this._primaryKey] !==
|
|
553
|
-
this._propagatePKUpdate(
|
|
554
|
+
if (updates[this._primaryKey] !== undefined && updates[this._primaryKey] !== pkKey) {
|
|
555
|
+
this._propagatePKUpdate(pkKey, updates[this._primaryKey]);
|
|
554
556
|
}
|
|
555
557
|
const oldData = { ...row };
|
|
556
558
|
const merged = { ...row, ...updates };
|
|
@@ -633,17 +635,19 @@ class Table {
|
|
|
633
635
|
|
|
634
636
|
removeById(id) {
|
|
635
637
|
if (!this._primaryKey) throw createError('ER_PARSE_ERROR', 'Table has no primary key');
|
|
638
|
+
// Issue #44:同 findById,先按列类型归一查询值
|
|
639
|
+
const pkKey = this._coerceQueryValue(this._primaryKey, id);
|
|
636
640
|
let rowIndex;
|
|
637
641
|
if (this._pkIndex) {
|
|
638
|
-
rowIndex = this._pkIndex.get(
|
|
639
|
-
} else {
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
642
|
+
rowIndex = this._pkIndex.get(pkKey);
|
|
643
|
+
} else if (this._btrees[this._primaryKey]) {
|
|
644
|
+
const indices = this._btrees[this._primaryKey].search(pkKey);
|
|
645
|
+
rowIndex = indices.length > 0 ? indices[0] : undefined;
|
|
646
|
+
}
|
|
647
|
+
if (rowIndex === undefined) {
|
|
648
|
+
// Issue #44:索引未命中时回落全表匹配
|
|
649
|
+
const found = this._rows.findIndex(r => r && !r._deleted && r[this._primaryKey] === pkKey);
|
|
650
|
+
if (found !== -1) rowIndex = found;
|
|
647
651
|
}
|
|
648
652
|
if (rowIndex === undefined) return 0;
|
|
649
653
|
const row = this._rows[rowIndex];
|
|
@@ -1037,32 +1041,129 @@ class Table {
|
|
|
1037
1041
|
return end > 0 ? pattern.slice(0, end) : null;
|
|
1038
1042
|
}
|
|
1039
1043
|
|
|
1040
|
-
|
|
1044
|
+
/**
|
|
1045
|
+
* Issue #44:按列声明类型归一查询值。
|
|
1046
|
+
*
|
|
1047
|
+
* 索引(B-Tree / hash)命中用的是严格 ===,所以字符串 '5' 永远搜不到
|
|
1048
|
+
* INT 列里的数字 5,表现为「静默返回空」;SQL 的 WHERE 路径早就在
|
|
1049
|
+
* 解析阶段强转过了,文档 API 这条路径没有。
|
|
1050
|
+
*
|
|
1051
|
+
* 归一结果与 insert 时 _validateDataTypes 的产物一致(整数 → number/BigInt、
|
|
1052
|
+
* 浮点 → number、布尔 → boolean、字符串列 → string)。无法归一的值原样返回。
|
|
1053
|
+
*/
|
|
1054
|
+
_coerceQueryValue(field, value) {
|
|
1055
|
+
const def = this._schema && this._schema[field];
|
|
1056
|
+
if (!def || typeof def === 'string' || value === null || value === undefined) return value;
|
|
1057
|
+
const t = String(def.type || '').toLowerCase();
|
|
1058
|
+
if (t === 'integer' || t === 'int' || t === 'bigint') {
|
|
1059
|
+
if (typeof value === 'bigint') {
|
|
1060
|
+
return (value >= SAFE_INT_MIN && value <= SAFE_INT_MAX) ? Number(value) : value;
|
|
1061
|
+
}
|
|
1062
|
+
if (typeof value === 'number') return value;
|
|
1063
|
+
if (typeof value === 'string' && DECIMAL_INT_RE.test(value.trim())) {
|
|
1064
|
+
try { return normalizeInteger(value.trim(), t); } catch (_) { return value; }
|
|
1065
|
+
}
|
|
1066
|
+
return value;
|
|
1067
|
+
}
|
|
1068
|
+
if (t === 'float' || t === 'double' || t === 'number' || t === 'real') {
|
|
1069
|
+
if (typeof value === 'number') return value;
|
|
1070
|
+
if (typeof value === 'string' && value.trim() !== '' && !Number.isNaN(Number(value))) return Number(value);
|
|
1071
|
+
return value;
|
|
1072
|
+
}
|
|
1073
|
+
if (t === 'boolean' || t === 'bool') {
|
|
1074
|
+
if (typeof value === 'boolean') return value;
|
|
1075
|
+
if (value === 0 || value === 1) return !!value;
|
|
1076
|
+
if (typeof value === 'string' && ['true', 'false', '1', '0'].includes(value.toLowerCase())) {
|
|
1077
|
+
return value.toLowerCase() === 'true' || value === '1';
|
|
1078
|
+
}
|
|
1079
|
+
return value;
|
|
1080
|
+
}
|
|
1081
|
+
if (t === 'string' || t === 'text' || t === 'varchar' || t === 'char') {
|
|
1082
|
+
if (typeof value === 'string') return value;
|
|
1083
|
+
if (typeof value === 'number' || typeof value === 'boolean' || value instanceof Date) return String(value);
|
|
1084
|
+
return value;
|
|
1085
|
+
}
|
|
1086
|
+
return value;
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
/** 按 schema 归一 query 中的等值 / $eq / $in / 范围值(Issue #44)。 */
|
|
1090
|
+
_coerceQuery(query) {
|
|
1091
|
+
let out = null;
|
|
1092
|
+
const set = (field, value) => {
|
|
1093
|
+
if (!out) out = { ...query };
|
|
1094
|
+
out[field] = value;
|
|
1095
|
+
};
|
|
1041
1096
|
for (const [field, condition] of Object.entries(query)) {
|
|
1097
|
+
if (field === '$or' || field === '$and') {
|
|
1098
|
+
if (Array.isArray(condition)) {
|
|
1099
|
+
const subs = condition.map(c => (c && typeof c === 'object' && !Array.isArray(c)) ? this._coerceQuery(c) : c);
|
|
1100
|
+
if (subs.some((s, i) => s !== condition[i])) set(field, subs);
|
|
1101
|
+
}
|
|
1102
|
+
continue;
|
|
1103
|
+
}
|
|
1104
|
+
if (field === '$not' || field[0] === '$' || !this._schema[field]) continue;
|
|
1105
|
+
const next = this._coerceCondition(field, condition);
|
|
1106
|
+
if (next !== condition) set(field, next);
|
|
1107
|
+
}
|
|
1108
|
+
return out || query;
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
_coerceCondition(field, condition) {
|
|
1112
|
+
if (condition === null || typeof condition !== 'object' || condition instanceof RegExp) {
|
|
1113
|
+
return this._coerceQueryValue(field, condition);
|
|
1114
|
+
}
|
|
1115
|
+
if (Array.isArray(condition)) {
|
|
1116
|
+
const next = condition.map(v => this._coerceQueryValue(field, v));
|
|
1117
|
+
return next.some((v, i) => v !== condition[i]) ? next : condition;
|
|
1118
|
+
}
|
|
1119
|
+
const keys = Object.keys(condition);
|
|
1120
|
+
if (keys.length === 0 || !keys.every(k => k[0] === '$')) return condition;
|
|
1121
|
+
let copy = null;
|
|
1122
|
+
for (const [op, val] of Object.entries(condition)) {
|
|
1123
|
+
let next = val;
|
|
1124
|
+
if (op === '$eq' || op === '$ne' || op === '$gt' || op === '$gte' || op === '$lt' || op === '$lte') {
|
|
1125
|
+
next = this._coerceQueryValue(field, val);
|
|
1126
|
+
} else if ((op === '$in' || op === '$nin' || op === '$between') && Array.isArray(val)) {
|
|
1127
|
+
next = val.map(v => this._coerceQueryValue(field, v));
|
|
1128
|
+
}
|
|
1129
|
+
if (next !== val) {
|
|
1130
|
+
if (!copy) copy = { ...condition };
|
|
1131
|
+
copy[op] = next;
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
return copy || condition;
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
_applyFilterOptimized(rows, query) {
|
|
1138
|
+
// Issue #44:索引查找前先把查询值归一成列声明类型,避免 '5' 查不到 5
|
|
1139
|
+
const q = this._coerceQuery(query);
|
|
1140
|
+
for (const [field, condition] of Object.entries(q)) {
|
|
1042
1141
|
if (field === '$or' || field === '$and' || field === '$not') continue;
|
|
1043
1142
|
if (typeof condition !== 'object' || condition === null || Array.isArray(condition) || condition instanceof RegExp) {
|
|
1044
1143
|
if (this._btrees[field]) {
|
|
1045
1144
|
const indices = this._btrees[field].search(condition);
|
|
1046
|
-
|
|
1047
|
-
|
|
1145
|
+
// #44:索引未命中时回落全表过滤——索引键是严格相等的,
|
|
1146
|
+
// 类型/形态不一致的行会被漏掉,不能直接断言「没有数据」。
|
|
1147
|
+
if (indices.length === 0) return this._applyFilter(rows, q);
|
|
1148
|
+
return this._applyFilter(this._rowsFromIndices(indices), q);
|
|
1048
1149
|
}
|
|
1049
1150
|
if (this._indexes[field] && this._indexes[field].has(condition)) {
|
|
1050
|
-
return this._applyFilter(this._rowsFromIndices(this._indexes[field].get(condition)),
|
|
1151
|
+
return this._applyFilter(this._rowsFromIndices(this._indexes[field].get(condition)), q);
|
|
1051
1152
|
}
|
|
1052
|
-
if (this._indexes[field]) return
|
|
1153
|
+
if (this._indexes[field]) return this._applyFilter(rows, q);
|
|
1053
1154
|
}
|
|
1054
1155
|
if (condition && typeof condition === 'object') {
|
|
1055
1156
|
if (this._btrees[field] && Array.isArray(condition.$in) && condition.$in.length > 0) {
|
|
1056
1157
|
const indices = this._btrees[field].searchMany(condition.$in);
|
|
1057
|
-
if (indices.length === 0) return
|
|
1058
|
-
return this._applyFilter(this._rowsFromIndices(indices),
|
|
1158
|
+
if (indices.length === 0) return this._applyFilter(rows, q);
|
|
1159
|
+
return this._applyFilter(this._rowsFromIndices(indices), q);
|
|
1059
1160
|
}
|
|
1060
1161
|
if (this._btrees[field] && typeof condition.$like === 'string') {
|
|
1061
1162
|
const prefix = this._likePrefix(condition.$like);
|
|
1062
1163
|
if (prefix) {
|
|
1063
1164
|
const indices = this._btrees[field].prefix(prefix);
|
|
1064
|
-
if (indices.length === 0) return
|
|
1065
|
-
return this._applyFilter(this._rowsFromIndices(indices),
|
|
1165
|
+
if (indices.length === 0) return this._applyFilter(rows, q);
|
|
1166
|
+
return this._applyFilter(this._rowsFromIndices(indices), q);
|
|
1066
1167
|
}
|
|
1067
1168
|
}
|
|
1068
1169
|
if (this._btrees[field] && (condition.$gt !== undefined || condition.$gte !== undefined ||
|
|
@@ -1086,11 +1187,11 @@ class Table {
|
|
|
1086
1187
|
} else {
|
|
1087
1188
|
indices = exclusiveMax ? this._btrees[field].lessThan(max) : this._btrees[field].lessThanEqual(max);
|
|
1088
1189
|
}
|
|
1089
|
-
return this._applyFilter(this._rowsFromIndices(indices),
|
|
1190
|
+
return this._applyFilter(this._rowsFromIndices(indices), q);
|
|
1090
1191
|
}
|
|
1091
1192
|
}
|
|
1092
1193
|
}
|
|
1093
|
-
return this._applyFilter(rows,
|
|
1194
|
+
return this._applyFilter(rows, q);
|
|
1094
1195
|
}
|
|
1095
1196
|
|
|
1096
1197
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jsql-neo",
|
|
3
|
-
"version": "6.5.
|
|
3
|
+
"version": "6.5.28",
|
|
4
4
|
"description": "JSQL-NEO \u2014 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",
|
|
@@ -50,12 +50,12 @@
|
|
|
50
50
|
"scripts": {
|
|
51
51
|
"postinstall": "node postinstall.js",
|
|
52
52
|
"build:native": "node scripts/build-native.js",
|
|
53
|
-
"test": "node test/smoke.js && node test/result-raw.test.js && node test/native.test.js && node test/json-functions.test.js && node test/sql-parser.test.js && node test/readme-audit.test.js && node test/query-opt.test.js && node test/wal-recovery.test.js && node test/mysql-compat-hijack.test.js && node test/dml-constraints.test.js && node test/star-nag.test.js && node test/plugin-xql.test.js && node test/bug-regression-2.test.js && node test/issue-34-35-36.test.js && node test/issue-g4.test.js && node test/issue-38.test.js && node test/issue-39.test.js && node test/issue-40.test.js",
|
|
53
|
+
"test": "node test/smoke.js && node test/result-raw.test.js && node test/native-install.test.js && node test/native-cli.test.js && node test/finalize-bigint.test.js && node test/native.test.js && node test/json-functions.test.js && node test/sql-parser.test.js && node test/readme-audit.test.js && node test/query-opt.test.js && node test/wal-recovery.test.js && node test/mysql-compat-hijack.test.js && node test/dml-constraints.test.js && node test/star-nag.test.js && node test/plugin-xql.test.js && node test/bug-regression-2.test.js && node test/issue-34-35-36.test.js && node test/issue-g4.test.js && node test/issue-38.test.js && node test/issue-39.test.js && node test/issue-40.test.js && node test/issue-44.test.js && node test/issue-45.test.js && node test/issue-46.test.js && node test/issue-47.test.js && node test/issue-48.test.js",
|
|
54
54
|
"test:parser": "node test/sql-parser.test.js",
|
|
55
55
|
"test:btree": "node test/btree.test.js",
|
|
56
56
|
"test:native": "node test/native.test.js",
|
|
57
57
|
"test:regress": "node test/regress-5.1.0.js",
|
|
58
|
-
"test:all": "node test/smoke.js && node test/result-raw.test.js && node test/sql-parser.test.js && node test/readme-audit.test.js && node test/native.test.js && node test/native-multi-instance.test.js && node test/wal-recovery.test.js && node test/mysql-compat-hijack.test.js && node test/wasm.test.js && node test/coverage.js && node test/regress-5.1.0.js && node test/join.test.js && node test/btree.test.js && node test/dml-constraints.test.js && node test/savepoint.test.js && node test/alter-table.test.js && node test/null-semantics.test.js && node test/identifier-keywords.test.js && node test/issue-34-35-36.test.js && node test/sql-semantics.test.js && node test/extra-coverage.test.js && node test/filter-compile.test.js && node test/recursive-cte.test.js && node test/index-ddl.test.js && node test/mongo-group.test.js && node test/multiprotocol.test.js && node test/issue-g4.test.js && node test/issue-38.test.js && node test/issue-39.test.js && node test/issue-40.test.js",
|
|
58
|
+
"test:all": "node test/smoke.js && node test/result-raw.test.js && node test/native-install.test.js && node test/native-cli.test.js && node test/finalize-bigint.test.js && node test/sql-parser.test.js && node test/readme-audit.test.js && node test/native.test.js && node test/native-multi-instance.test.js && node test/wal-recovery.test.js && node test/mysql-compat-hijack.test.js && node test/wasm.test.js && node test/coverage.js && node test/regress-5.1.0.js && node test/join.test.js && node test/btree.test.js && node test/dml-constraints.test.js && node test/savepoint.test.js && node test/alter-table.test.js && node test/null-semantics.test.js && node test/identifier-keywords.test.js && node test/issue-34-35-36.test.js && node test/sql-semantics.test.js && node test/extra-coverage.test.js && node test/filter-compile.test.js && node test/recursive-cte.test.js && node test/index-ddl.test.js && node test/mongo-group.test.js && node test/multiprotocol.test.js && node test/issue-g4.test.js && node test/issue-38.test.js && node test/issue-39.test.js && node test/issue-40.test.js && node test/issue-44.test.js && node test/issue-45.test.js && node test/issue-46.test.js && node test/issue-47.test.js && node test/issue-48.test.js",
|
|
59
59
|
"test:coverage": "c8 --all --include='lib/**' --exclude='lib/web_ui.js' --reporter=text --reporter=lcov node test/coverage.js",
|
|
60
60
|
"test:orms": "node examples/orms/run-all.js",
|
|
61
61
|
"test:dml": "node test/dml-constraints.test.js",
|