jsql-neo 6.5.4 → 6.5.6
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 +3 -3
- package/index.js +1 -1
- package/lib/native_client.js +100 -18
- package/lib/wasm_client.js +38 -3
- package/native/jsql-neo-native.node +0 -0
- package/nativesrc/jsql-neo-core/src/engine/memory.rs +14 -5
- package/nativesrc/jsql-neo-core/src/engine/table.rs +52 -1
- package/package.json +3 -3
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.6** — 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
|

|
|
@@ -41,7 +41,7 @@ npm install jsql-neo
|
|
|
41
41
|
- **Four wire protocols on one port** — `jsql serve` accepts MySQL, PostgreSQL, MongoDB and Redis clients; `mysql2`, `pg`, `mongodb` and `ioredis` connect directly
|
|
42
42
|
- **Zero dependencies** — no native libs to compile, no system deps to install
|
|
43
43
|
- **Three engines, same API** — `native` (Rust N-API), `wasm` (same Rust core), `js` (pure JS)
|
|
44
|
-
-
|
|
44
|
+
- **O(1) primary-key lookups and updates** — hash index, no full-table scan; unindexed filters run in Rust
|
|
45
45
|
|
|
46
46
|
> **Wire protocols** — MySQL, PostgreSQL, MongoDB and Redis share a single listener
|
|
47
47
|
> (`jsql serve`, default port 3306). The server sniffs the first packet of each
|
|
@@ -61,7 +61,7 @@ JSQL-NEO: MySQL + PostgreSQL + MongoDB + Redis + SQL
|
|
|
61
61
|
|
|
62
62
|
| Feature | Detail |
|
|
63
63
|
|---|---|
|
|
64
|
-
| **Rust Core** | N-API native addon,
|
|
64
|
+
| **Rust Core** | N-API native addon — O(1) primary-key point lookups, binary bulk inserts, Rust-side row filtering |
|
|
65
65
|
| **WASM Build** | Same engine runs in Node.js and any browser |
|
|
66
66
|
| **MySQL** | `mysql2`, Sequelize, Knex, TypeORM — just work |
|
|
67
67
|
| **PostgreSQL** | `pg`, `psql`, pgAdmin — SCRAM-SHA-256, JSONB, ILIKE |
|
package/index.js
CHANGED
package/lib/native_client.js
CHANGED
|
@@ -32,6 +32,27 @@ function _loadNativeOrStub() {
|
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
const native = _loadNativeOrStub();
|
|
35
|
+
|
|
36
|
+
// updateById / removeById 走 opBuffer + 定时批量 flush。进程直接退出(没调 stop())
|
|
37
|
+
// 时定时器来不及跑,写操作会丢。这里用一个进程级 exit 钩子,在退出前同步 flush
|
|
38
|
+
// 所有仍然存活的实例;flush 本身是同步原生调用,可以安全地在 exit 事件里执行。
|
|
39
|
+
const _liveInstances = new Set();
|
|
40
|
+
let _exitFlushInstalled = false;
|
|
41
|
+
function _trackInstance(inst) {
|
|
42
|
+
_liveInstances.add(inst);
|
|
43
|
+
if (_exitFlushInstalled) return;
|
|
44
|
+
_exitFlushInstalled = true;
|
|
45
|
+
process.once('exit', () => {
|
|
46
|
+
for (const db of _liveInstances) {
|
|
47
|
+
try {
|
|
48
|
+
db._flushOpsNow();
|
|
49
|
+
// disk/hybrid 模式还有一层「脏表落盘」,同样在退出前同步补一次。
|
|
50
|
+
if (db._mode !== 'memory' && db._handle !== null) db._n('jsqlFlushDirty');
|
|
51
|
+
} catch (e) { /* 进程已在退出,尽力而为 */ }
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
35
56
|
const INT64_TAG = 1;
|
|
36
57
|
const FLOAT_TAG = 2;
|
|
37
58
|
const STR_TAG = 3;
|
|
@@ -258,6 +279,30 @@ function encodeField(buf, off, v) {
|
|
|
258
279
|
return off + 5 + sl;
|
|
259
280
|
}
|
|
260
281
|
|
|
282
|
+
/**
|
|
283
|
+
* 二进制批量格式只写一份字段名列表,因此要求所有行列集合完全一致,
|
|
284
|
+
* 且没有 undefined 值(undefined 在 JSON 中表示「不提供该列」,会触发默认值)。
|
|
285
|
+
*/
|
|
286
|
+
function _canEncodeBatch(rows) {
|
|
287
|
+
if (!rows.length) return false;
|
|
288
|
+
const keys = Object.keys(rows[0]);
|
|
289
|
+
for (const row of rows) {
|
|
290
|
+
if (!row || typeof row !== 'object') return false;
|
|
291
|
+
const rk = Object.keys(row);
|
|
292
|
+
if (rk.length !== keys.length) return false;
|
|
293
|
+
for (const k of keys) {
|
|
294
|
+
if (!Object.prototype.hasOwnProperty.call(row, k)) return false;
|
|
295
|
+
if (row[k] === undefined) return false;
|
|
296
|
+
// JSON.stringify 会把 NaN/Infinity 变成 null,二进制会原样写入浮点值。
|
|
297
|
+
if (typeof row[k] === 'number' && !Number.isFinite(row[k])) return false;
|
|
298
|
+
// 对象/数组走 JSON 路径由 Rust 侧按原语义处理;JSON 列在 _insertBatch
|
|
299
|
+
// 的前置映射里已经转成字符串,不会落到这里。
|
|
300
|
+
if (row[k] !== null && typeof row[k] === 'object') return false;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
return true;
|
|
304
|
+
}
|
|
305
|
+
|
|
261
306
|
function encodeBatch(rows) {
|
|
262
307
|
if (rows.length === 0) return new Uint8Array(0);
|
|
263
308
|
const fieldNames = Object.keys(rows[0]);
|
|
@@ -334,6 +379,7 @@ class JSQL {
|
|
|
334
379
|
const { ModuleManager } = require('./mod');
|
|
335
380
|
new ModuleManager().applyTo(this);
|
|
336
381
|
}
|
|
382
|
+
_trackInstance(this);
|
|
337
383
|
}
|
|
338
384
|
|
|
339
385
|
/**
|
|
@@ -477,9 +523,29 @@ class JSQL {
|
|
|
477
523
|
return id;
|
|
478
524
|
}
|
|
479
525
|
|
|
526
|
+
/**
|
|
527
|
+
* 自增整型主键时,内部行 id 与主键值一致,可直接走 O(1) 的 jsqlFindById;
|
|
528
|
+
* 非自增主键(如字符串主键)才退化为 filter 查询。
|
|
529
|
+
*/
|
|
530
|
+
_isAutoIntPk(table) {
|
|
531
|
+
if (!this._pkAutoIncrement(table)) return false;
|
|
532
|
+
const def = this._schemas[table][this._pkOf(table)];
|
|
533
|
+
const t = typeof def === 'string' ? def.toLowerCase()
|
|
534
|
+
: (def && def.type ? String(def.type).toLowerCase() : '');
|
|
535
|
+
return ['integer', 'int', 'bigint'].includes(t);
|
|
536
|
+
}
|
|
537
|
+
|
|
480
538
|
_findRawById(table, id) {
|
|
481
539
|
const pk = this._pkOf(table);
|
|
482
540
|
if (pk) {
|
|
541
|
+
if (this._isAutoIntPk(table)) {
|
|
542
|
+
const n = Number(id);
|
|
543
|
+
if (Number.isFinite(n)) {
|
|
544
|
+
const r = safeParse(this._n('jsqlFindById', table, n));
|
|
545
|
+
if (r && r.error) return r;
|
|
546
|
+
return r || null;
|
|
547
|
+
}
|
|
548
|
+
}
|
|
483
549
|
const filter = {};
|
|
484
550
|
filter[pk] = this._coercePkId(table, id);
|
|
485
551
|
const r = safeParse(this._n('jsqlFind', table, JSON.stringify(filter), 1, 0));
|
|
@@ -492,6 +558,14 @@ class JSQL {
|
|
|
492
558
|
_seqIdByPk(table, id) {
|
|
493
559
|
const pk = this._pkOf(table);
|
|
494
560
|
if (pk) {
|
|
561
|
+
if (this._isAutoIntPk(table)) {
|
|
562
|
+
const n = Number(id);
|
|
563
|
+
if (Number.isFinite(n)) {
|
|
564
|
+
const r = safeParse(this._n('jsqlFindById', table, n));
|
|
565
|
+
if (r && !r.error && r.id !== undefined) return r.id;
|
|
566
|
+
}
|
|
567
|
+
return null;
|
|
568
|
+
}
|
|
495
569
|
const filter = {};
|
|
496
570
|
filter[pk] = this._coercePkId(table, id);
|
|
497
571
|
const r = safeParse(this._n('jsqlFind', table, JSON.stringify(filter), 1, 0));
|
|
@@ -539,16 +613,18 @@ class JSQL {
|
|
|
539
613
|
return out;
|
|
540
614
|
});
|
|
541
615
|
}
|
|
542
|
-
|
|
543
|
-
//
|
|
544
|
-
//
|
|
616
|
+
// 批量插入优先走二进制格式:整批只在 JS 侧 encode 一次,Rust 直接按
|
|
617
|
+
// tag 解析成 FieldValue,省掉「JS JSON.stringify → Rust serde 解析 →
|
|
618
|
+
// HashMap 再转 FieldValue」这一整条链路。20 万行实测约 2× 于 JSON 路径。
|
|
619
|
+
// 只有「所有行列集合一致且无 undefined」时才可用(二进制头只带一份字段名,
|
|
620
|
+
// 且 undefined 在 JSON 里是省略键、在二进制里是 null,语义不同)。
|
|
545
621
|
let r;
|
|
546
|
-
if (typeof native.
|
|
547
|
-
r = safeParse(this._n('
|
|
622
|
+
if (typeof native.jsqlInsertBuf === 'function' && _canEncodeBatch(rows)) {
|
|
623
|
+
r = safeParse(this._n('jsqlInsertBuf', table, encodeBatch(rows)));
|
|
624
|
+
} else if (typeof native.jsqlInsertRaw === 'function') {
|
|
625
|
+
r = safeParse(this._n('jsqlInsertRaw', table, JSON.stringify(rows)));
|
|
548
626
|
} else {
|
|
549
|
-
|
|
550
|
-
const bin = encodeBatch(rows);
|
|
551
|
-
r = safeParse(this._n('jsqlInsertBuf', table, bin));
|
|
627
|
+
r = safeParse(this._n('jsqlInsertBuf', table, encodeBatch(rows)));
|
|
552
628
|
}
|
|
553
629
|
if (r && r.error) throw new Error(r.error);
|
|
554
630
|
if (r && Array.isArray(r) && !this._pkAutoIncrement(table)) {
|
|
@@ -724,7 +800,10 @@ class JSQL {
|
|
|
724
800
|
this._flushOpsNow();
|
|
725
801
|
if (!(await this._runHooks('beforeFind', [table, { ids }]))) return null;
|
|
726
802
|
let r;
|
|
727
|
-
if (this.
|
|
803
|
+
if (this._isAutoIntPk(table) && ids.every(id => Number.isFinite(Number(id)))) {
|
|
804
|
+
// 自增主键:内部 id 即主键值,一次批量调用即可,避免 N 次往返。
|
|
805
|
+
r = safeParse(this._n('jsqlFindByIds', table, JSON.stringify(ids.map(Number))));
|
|
806
|
+
} else if (this._pkOf(table)) {
|
|
728
807
|
r = ids.map(id => this._findRawById(table, id)).filter(x => x !== null && !(x && x.error));
|
|
729
808
|
} else {
|
|
730
809
|
r = safeParse(this._n('jsqlFindByIds', table, JSON.stringify(ids)));
|
|
@@ -810,19 +889,21 @@ class JSQL {
|
|
|
810
889
|
// 比读整张表还贵,而它本来只需要定位一行。
|
|
811
890
|
const touched = fields.filter(f => Object.prototype.hasOwnProperty.call(data, f));
|
|
812
891
|
if (!touched.length) return;
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
892
|
+
// 只取回被更新的这一行(自增主键下 O(1)),逐字段与现值比较;
|
|
893
|
+
// 不再把整张表导出成 JSON 到 JS 里做全量比对。
|
|
894
|
+
const self = this._findRawById(table, id);
|
|
895
|
+
if (!self || self.error) return;
|
|
816
896
|
const pk = this._pkOf(table) || 'id';
|
|
817
|
-
// jsqlFind 返回的行结构是 { id, fields:{...} } —— 值在 fields 里,不在顶层。
|
|
818
897
|
const valOf = (r, f) => (r && r.fields && typeof r.fields === 'object' ? r.fields[f] : r ? r[f] : undefined);
|
|
819
898
|
const idOf = (r) => (r && r.fields ? valOf(r, pk) : r ? r[pk] : undefined);
|
|
820
|
-
for (const f of
|
|
821
|
-
if (!Object.prototype.hasOwnProperty.call(data, f)) continue;
|
|
899
|
+
for (const f of touched) {
|
|
822
900
|
const nv = data[f];
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
901
|
+
if (valOf(self, f) === nv) continue; // 值没变
|
|
902
|
+
// 只需知道「是否还存在另一行占用了这个键值」:limit 2 让原生层命中即停,
|
|
903
|
+
// 输出量恒定,不随表规模增长。
|
|
904
|
+
const probe = safeParse(this._n('jsqlFind', table, JSON.stringify({ [f]: nv }), 2, 0));
|
|
905
|
+
if (!Array.isArray(probe)) continue;
|
|
906
|
+
if (probe.some(r => String(idOf(r)) !== String(id))) {
|
|
826
907
|
const e = new Error(`Duplicate entry '${nv}' for key '${f}'`);
|
|
827
908
|
e.code = 'ER_DUP_ENTRY';
|
|
828
909
|
e.errno = 1062;
|
|
@@ -893,6 +974,7 @@ class JSQL {
|
|
|
893
974
|
} catch (e) { /* ignore */ }
|
|
894
975
|
this._handle = null;
|
|
895
976
|
}
|
|
977
|
+
_liveInstances.delete(this);
|
|
896
978
|
await this._runHooks('onStop', []);
|
|
897
979
|
this._emit('stop', {});
|
|
898
980
|
}
|
package/lib/wasm_client.js
CHANGED
|
@@ -725,24 +725,59 @@ class JSQL {
|
|
|
725
725
|
/**
|
|
726
726
|
* 校验 UPDATE 后的主键 / UNIQUE 值不与其它行冲突。
|
|
727
727
|
* 逐行比对(未建索引也正确);值未变化时跳过。
|
|
728
|
+
*
|
|
729
|
+
* 6.5.5 性能修复(对齐 native_client.js 的 6.4.8 修复,那次漏了 wasm 侧):
|
|
730
|
+
* 旧写法无条件 `this.find(table, {}, {limit: 1e9})` 把整张表导出成 JSON,
|
|
731
|
+
* 再逐字段 find/some。updateById 于是在 10 万行的表上要 ~590ms/次 ——
|
|
732
|
+
* 比读整张表还贵,而它本来只需要定位一行。
|
|
733
|
+
* UPDATE t SET age = 30 WHERE id = 1 ← age 不是键,不可能冲突,却付了全表钱
|
|
734
|
+
*
|
|
735
|
+
* 新逻辑(性能从 O(表行数) 降回 O(1)):
|
|
736
|
+
* 1. 只筛出 pk + unique 列,且**确实出现在 updateData 里**的(touched);
|
|
737
|
+
* 一个都没碰 → 直接 return,整张表一个字都不读;
|
|
738
|
+
* 2. 先用主键把**自己那一行**取回来(一条走 pk 的 find,limit 1,O(1))。
|
|
739
|
+
* 所有 touched 列的新值都和现值相同 → 值没变,同样直接 return
|
|
740
|
+
* (幂等更新不误报,也不再触发全表扫描);
|
|
741
|
+
* 3. 真的动了键,才退化成整表扫描逐字段比对(正确性优先)。
|
|
728
742
|
*/
|
|
729
743
|
async _assertNoKeyConflict(table, id, data, selfSeq) {
|
|
730
744
|
const schema = this._schemas[table];
|
|
731
745
|
if (!schema) return;
|
|
732
746
|
const fields = Object.keys(schema).filter(f => schema[f] && (schema[f].primaryKey || schema[f].unique));
|
|
733
747
|
if (!fields.length) return;
|
|
748
|
+
// 只在**确实要改主键 / UNIQUE 列**时才做冲突检查。
|
|
749
|
+
const touched = fields.filter(f => Object.prototype.hasOwnProperty.call(data, f));
|
|
750
|
+
if (!touched.length) return;
|
|
751
|
+
|
|
752
|
+
// 自己那一行:走主键的 find(limit 1),不整表导出。
|
|
753
|
+
let self = null;
|
|
754
|
+
try {
|
|
755
|
+
const pk = this._pkOf(table);
|
|
756
|
+
if (pk) {
|
|
757
|
+
const one = await this.find(table, { [pk]: this._coercePkId(table, id) }, { limit: 1, offset: 0 });
|
|
758
|
+
self = Array.isArray(one) ? (one[0] || null) : null;
|
|
759
|
+
}
|
|
760
|
+
} catch (_) {
|
|
761
|
+
self = null; // 取不到就走下面的全量比对
|
|
762
|
+
}
|
|
763
|
+
// 所有 touched 列的值都没变 → 幂等更新,不可能产生新冲突,无需扫表。
|
|
764
|
+
if (self && touched.every(f => self[f] === data[f])) return;
|
|
765
|
+
|
|
734
766
|
let rows;
|
|
735
767
|
try {
|
|
736
768
|
rows = await this.find(table, {}, { limit: 1e9, offset: 0 });
|
|
737
769
|
} catch (_) {
|
|
738
770
|
return; // 读不到就不拦,交给底层报错
|
|
739
771
|
}
|
|
772
|
+
if (!Array.isArray(rows)) return;
|
|
773
|
+
// 把自己那一行找出来并从比对中排除。注意:self 来自上一次 find,
|
|
774
|
+
// 与 rows 里的对象不是同一个引用,所以必须按 id 重新定位,不能直接 r !== self。
|
|
775
|
+
const selfRow = self || rows.find(r => r && String(r.id) === String(id)) || null;
|
|
740
776
|
for (const f of fields) {
|
|
741
777
|
if (!Object.prototype.hasOwnProperty.call(data, f)) continue;
|
|
742
778
|
const nv = data[f];
|
|
743
|
-
|
|
744
|
-
if (
|
|
745
|
-
if (rows.some(r => r && r !== self && r[f] === nv)) {
|
|
779
|
+
if (selfRow && selfRow[f] === nv) continue; // 值没变
|
|
780
|
+
if (rows.some(r => r && r !== selfRow && r[f] === nv)) {
|
|
746
781
|
const e = new Error(`Duplicate entry '${nv}' for key '${f}'`);
|
|
747
782
|
e.code = 'ER_DUP_ENTRY';
|
|
748
783
|
e.errno = 1062;
|
|
Binary file
|
|
@@ -132,11 +132,16 @@ impl Engine for MemoryEngine {
|
|
|
132
132
|
let t = self.tables.get_mut(table).ok_or_else(|| format!("table '{}' not found", table))?;
|
|
133
133
|
let ids: Vec<u64> = match filter {
|
|
134
134
|
Some(f) => {
|
|
135
|
-
|
|
136
|
-
t.
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
.
|
|
135
|
+
// 主键等值过滤:直接命中索引,不再逐行比较。
|
|
136
|
+
if let Some(id) = t.pk_eq_id(f) {
|
|
137
|
+
if t.pk_index.contains_key(&id) { vec![id] } else { Vec::new() }
|
|
138
|
+
} else {
|
|
139
|
+
let conds = t.parse_filter(f);
|
|
140
|
+
t.rows.iter()
|
|
141
|
+
.filter(|r| matches_filter(t.get_row_values(r), &conds))
|
|
142
|
+
.map(|r| r.id)
|
|
143
|
+
.collect()
|
|
144
|
+
}
|
|
140
145
|
}
|
|
141
146
|
None => t.rows.iter().map(|r| r.id).collect(),
|
|
142
147
|
};
|
|
@@ -158,6 +163,10 @@ impl Engine for MemoryEngine {
|
|
|
158
163
|
}
|
|
159
164
|
Some(f) => f,
|
|
160
165
|
};
|
|
166
|
+
// 主键等值删除:直接命索引,O(1)。
|
|
167
|
+
if let Some(id) = t.pk_eq_id(f) {
|
|
168
|
+
return Ok(if t.remove_row(id) { 1 } else { 0 });
|
|
169
|
+
}
|
|
161
170
|
let conds = t.parse_filter(f);
|
|
162
171
|
let to_remove: Vec<usize> = t.rows.iter().enumerate()
|
|
163
172
|
.filter(|(_, r)| matches_filter(t.get_row_values(r), &conds))
|
|
@@ -102,6 +102,9 @@ pub struct Table {
|
|
|
102
102
|
pub next_id: u64,
|
|
103
103
|
pub pk_field: Option<String>,
|
|
104
104
|
pub pk_index: FxHashMap<u64, usize>,
|
|
105
|
+
/// 主键是否为自增整型:只有这时内部行 id 才等于主键值,
|
|
106
|
+
/// `pk_index` 才能直接用于 O(1) 主键点查。
|
|
107
|
+
pub pk_auto: bool,
|
|
105
108
|
}
|
|
106
109
|
|
|
107
110
|
pub fn default_value(ft: &FieldType) -> FieldValue {
|
|
@@ -192,6 +195,10 @@ impl Table {
|
|
|
192
195
|
let pk_field = schema.iter()
|
|
193
196
|
.find(|(_, f)| f.primary_key)
|
|
194
197
|
.map(|(name, _)| name.clone());
|
|
198
|
+
let pk_auto = schema.iter()
|
|
199
|
+
.find(|(_, f)| f.primary_key)
|
|
200
|
+
.map(|(_, f)| f.auto_increment)
|
|
201
|
+
.unwrap_or(false);
|
|
195
202
|
|
|
196
203
|
let field_order: Vec<String> = schema.iter().map(|(n, _)| n.clone()).collect();
|
|
197
204
|
let field_index: HashMap<String, usize> = field_order.iter()
|
|
@@ -202,8 +209,22 @@ impl Table {
|
|
|
202
209
|
Self {
|
|
203
210
|
name, schema, field_order, field_index,
|
|
204
211
|
rows: Vec::new(), values: Vec::new(), next_id: 1, pk_field,
|
|
205
|
-
pk_index: FxHashMap::default(),
|
|
212
|
+
pk_index: FxHashMap::default(), pk_auto,
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/// 若过滤条件恰好是「自增主键 = 整数」则返回该主键值。
|
|
217
|
+
/// 这种查询可以直接走 `pk_index`,把 O(n) 全表扫描降到 O(1)。
|
|
218
|
+
pub(crate) fn pk_eq_id(&self, filter: &HashMap<String, serde_json::Value>) -> Option<u64> {
|
|
219
|
+
if !self.pk_auto || filter.len() != 1 {
|
|
220
|
+
return None;
|
|
221
|
+
}
|
|
222
|
+
let pk = self.pk_field.as_ref()?;
|
|
223
|
+
let id = filter.get(pk)?.as_i64()?;
|
|
224
|
+
if id <= 0 {
|
|
225
|
+
return None;
|
|
206
226
|
}
|
|
227
|
+
Some(id as u64)
|
|
207
228
|
}
|
|
208
229
|
|
|
209
230
|
pub fn to_row_map(&self, values: &[FieldValue]) -> HashMap<String, serde_json::Value> {
|
|
@@ -743,6 +764,23 @@ impl Table {
|
|
|
743
764
|
order_by: &Option<String>, order: &Option<String>) -> (String, bool) {
|
|
744
765
|
let o = offset.unwrap_or(0);
|
|
745
766
|
let cap = limit.unwrap_or(usize::MAX);
|
|
767
|
+
|
|
768
|
+
// 主键点查直通:一次哈希查找定位到行,不再逐行 parse_filter/matches_filter。
|
|
769
|
+
if let Some(id) = filter.as_ref().and_then(|f| self.pk_eq_id(f)) {
|
|
770
|
+
let mut out = String::with_capacity(160);
|
|
771
|
+
out.push('[');
|
|
772
|
+
if o == 0 && cap > 0 {
|
|
773
|
+
if let Some(&idx) = self.pk_index.get(&id) {
|
|
774
|
+
if let Some(rs) = self.rows.get(idx) {
|
|
775
|
+
let mut tc = TimeCache::new();
|
|
776
|
+
self.write_row_json_into(rs, &mut out, &mut tc);
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
out.push(']');
|
|
781
|
+
return (out, false);
|
|
782
|
+
}
|
|
783
|
+
|
|
746
784
|
let conds = filter.as_ref().map(|f| self.parse_filter(f)).unwrap_or_default();
|
|
747
785
|
|
|
748
786
|
let mut out = String::with_capacity(128);
|
|
@@ -797,6 +835,19 @@ impl Table {
|
|
|
797
835
|
order_by: &Option<String>, order: &Option<String>) -> Vec<Row> {
|
|
798
836
|
let o = offset.unwrap_or(0);
|
|
799
837
|
let l = limit.unwrap_or(usize::MAX);
|
|
838
|
+
|
|
839
|
+
// 主键点查直通(与 find_rows_json 一致)。
|
|
840
|
+
if let Some(id) = filter.as_ref().and_then(|f| self.pk_eq_id(f)) {
|
|
841
|
+
if o == 0 && l > 0 {
|
|
842
|
+
if let Some(&idx) = self.pk_index.get(&id) {
|
|
843
|
+
if let Some(rs) = self.rows.get(idx) {
|
|
844
|
+
return vec![self.to_row(rs)];
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
return Vec::new();
|
|
849
|
+
}
|
|
850
|
+
|
|
800
851
|
let conds = filter.as_ref().map(|f| self.parse_filter(f)).unwrap_or_default();
|
|
801
852
|
|
|
802
853
|
if let Some(field) = order_by.as_ref().filter(|f| self.field_index.contains_key(*f)) {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jsql-neo",
|
|
3
|
-
"version": "6.5.
|
|
4
|
-
"description": "JSQL-NEO
|
|
3
|
+
"version": "6.5.6",
|
|
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",
|
|
7
7
|
"exports": {
|
|
@@ -97,4 +97,4 @@
|
|
|
97
97
|
"devDependencies": {
|
|
98
98
|
"c8": "^10.1.2"
|
|
99
99
|
}
|
|
100
|
-
}
|
|
100
|
+
}
|