wp-knowledge 0.11.4

KnowDB loader and SQLite-backed query facade for the Warp Parse stack.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
# wp-knowledge Provider 与 Cache 重构方案

配套实施任务清单见:

- [PROVIDER_CACHE_TASKS.md]./PROVIDER_CACHE_TASKS.md

## 背景

`wp-knowledge` 当前已经同时支持三类数据源:

- 本地 KnowDB 构建出的 SQLite authority 库
- 外部 PostgreSQL
- 外部 MySQL

但现有实现仍然带有明显的 SQLite 历史包袱,尤其体现在:

- `facade` 虽然抽象出了 `QueryFacade`,但部分接口仍围绕 `rusqlite::ToSql` 组织
- 查询缓存由调用方传入 `FieldQueryCache`,生命周期和 key 语义不统一
- `COLNAME_CACHE` 是进程级无上限 `HashMap`
- `ThreadClonedMDB` 使用线程本地内存副本,reload 后没有统一的刷新语义
- `PROVIDER` 和白名单通过 `OnceLock` 绑定,初始化后不可替换

这些设计在“单进程 + SQLite + 单次计算内 lookup 优化”的场景下可以工作,但在以下目标下已经不够:

- 支持多种外部数据库
- 支持可控的缓存失效
- 支持 provider reload / 切换
- 支持统一的查询与元数据缓存
- 在不破坏现有 OML / WPL 调用方式的前提下演进

## 目标

本次重构目标:

- 建立数据源无关的 provider 抽象
- 将查询缓存从“调用期局部缓存”升级为“统一策略缓存”
- 为 provider reload、schema 变更、切换外部库引入明确失效语义
- 保留现有 `facade::query_row/query_named/cache_query` 的兼容入口
- 为 SQLite / PostgreSQL / 后续外部库提供统一接入模型

## 非目标

本方案不试图在第一阶段解决以下问题:

- 分布式缓存
- 跨进程缓存一致性
- 任意 SQL 自动分析是否可缓存
- 跨数据库 SQL 方言统一
- 事务内读写一致性抽象

第一阶段目标是把进程内架构理顺,让 provider、cache、reload 语义清晰且可扩展。

## 当前问题摘要

### 1. 结果缓存语义过弱

当前 `cache_query` 的 key 和生命周期都由调用方决定:

- 相同 SQL 在不同 provider 上可能命中同一个 key
- reload 后如果调用方仍持有旧 cache,可能继续返回旧结果
- key 只依赖 `DataField`,不携带 provider 身份和版本

### 2. 元数据缓存无边界

`COLNAME_CACHE` 只按 SQL 文本缓存列名,存在这些问题:

- 没有容量上限
- 没有 TTL
- 不区分 provider
- 不区分 schema/generation

### 3. Provider 生命周期不可替换

`PROVIDER` 使用 `OnceLock`,意味着:

- 初始化后无法安全切换数据源
- reload 不具备正式机制
- provider 状态与 cache 状态无法一起更新

### 4. 线程本地副本缺少版本感知

`ThreadClonedMDB` 为每个线程缓存一个 authority 的内存副本,这本质上是“数据库快照缓存”。

当前缺少:

- reload 后线程副本自动失效
- provider 切换后旧线程副本自动重建
- provider 版本与线程本地状态的关联

## 总体设计

### 设计原则

- Provider 负责执行查询,不负责缓存策略
- Cache 是统一基础设施,不依赖具体数据库实现
- 所有缓存 key 都必须带上数据源身份与版本
- reload 的主失效机制采用 `generation`
- TTL 只作为兜底,不作为主一致性机制
- 兼容现有 facade API,但内部逐步迁移到新的请求模型

### 目标分层

建议将内部结构分为 4 层:

1. `DatasourceRegistry`
2. `KnowledgeProvider`
3. `QueryRuntime`
4. `Facade Compatibility Layer`

职责如下:

- `DatasourceRegistry`
  - 持有当前激活的数据源
  - 管理 `source_id`
  - 管理 `generation`
  - 处理 reload / replace

- `KnowledgeProvider`
  - 执行只读查询
  - 执行字典表读取
  - 可选返回 schema / metadata 信息

- `QueryRuntime`
  - 执行缓存判定
  - 统一生成 cache key
  - 持有结果缓存、元数据缓存
  - 提供 query plan / query request 执行

- `Facade Compatibility Layer`
  - 暴露现有 `query/query_row/query_named/cache_query`
  - 把老接口适配到新 runtime

## 核心抽象

### DatasourceId

每个 provider 实例必须有稳定身份:

```rust
pub struct DatasourceId(String);
```

建议取值:

- SQLite authority 文件路径 hash
- PostgreSQL 连接配置 hash
- 未来 HTTP / MySQL / ClickHouse 等数据源的配置 hash

`DatasourceId` 只用于区分数据源,不直接暴露凭据。

### Generation

引入单调递增版本:

```rust
pub struct Generation(u64);
```

它是整个失效机制的核心。

以下事件必须提升 generation:

- 重新加载 knowdb
- authority.sqlite 被重建
- provider 从 SQLite 切到 PostgreSQL
- PostgreSQL 连接配置变化
- 白名单变化
- 明确的 schema refresh

所有 cache key、线程本地状态、元数据缓存都必须带 generation。

### QueryRequest

内部统一查询模型,不再让 facade 直接依赖 `rusqlite::ToSql`:

```rust
pub struct QueryRequest {
    pub sql: String,
    pub params: Vec<QueryParam>,
    pub mode: QueryMode,
    pub cache_policy: CachePolicy,
}
```

```rust
pub enum QueryMode {
    Many,
    FirstRow,
    CipherTable { table: String },
}
```

```rust
pub enum QueryParam {
    Null { name: String },
    Bool { name: String, value: bool },
    Int { name: String, value: i64 },
    Float { name: String, value: f64 },
    Text { name: String, value: String },
}
```

第一阶段不需要把 `DataField` 的全部值类型都映射成数据库原生类型。对数据库参数来说,优先支持真正参与 bind 的类型即可。

### CachePolicy

缓存不应该对所有查询默认开启。建议显式建模:

```rust
pub enum CachePolicy {
    Bypass,
    UseGlobal { ttl: Option<std::time::Duration> },
    UseCallScope,
}
```

含义:

- `Bypass`
  - 不走结果缓存
- `UseGlobal`
  - 使用统一进程内缓存
- `UseCallScope`
  - 兼容历史 `cache_query` 的调用期局部缓存

这样可以兼容旧逻辑,也能逐步把热点 lookup 迁移到统一缓存。

## Provider 抽象

建议新增内部 trait:

```rust
pub trait KnowledgeProvider: Send + Sync {
    fn provider_kind(&self) -> ProviderKind;
    fn datasource_id(&self) -> &DatasourceId;
    fn execute(&self, req: &QueryRequest) -> KnowledgeResult<QueryResult>;
    fn generation_hint(&self) -> Option<Generation>;
}
```

其中:

- `provider_kind` 用于日志、指标、调试
- `datasource_id` 用于缓存隔离
- `execute` 是统一入口
- `generation_hint` 在第一阶段可选,后续可用于 provider 自报内部版本

SQLite 与 PostgreSQL 的差异应收敛在 provider 内部:

- SQLite provider 自己完成 `:name` 参数绑定
- PostgreSQL provider 自己把 `:name` 改写成 `$1/$2/...`
- metadata 获取逻辑也各自收敛

外层 runtime 不应该关心它们的方言差异。

## Cache 设计

### 术语与边界

当前实现里需要明确区分 3 类 cache。后续讨论中的 `cache`,如果不特别说明,不应混用这 3 个概念。

| 名称 | 文档统一称呼 | 当前主要实现 | 作用域 | 缓存内容 | 容量单位 | 默认容量 | 谁控制是否使用 |
| --- | --- | --- | --- | --- | --- | --- | --- |
| 结果缓存 | `result cache` | `KnowledgeRuntime.result_cache` | 进程级、provider 级 | 查询结果 `QueryResponse` | 条目数 | `1024 entries` | `CachePolicy::UseGlobal` |
| 调用期缓存 | `local cache` | `FieldQueryCache` / `QueryLocalCache` | 单次计算 / 单次调用链 | 查询结果 `RowData` | 条目数 | `100 entries` | 调用方是否传入并走 `cache_query` |
| 元数据缓存 | `metadata cache` | `COLNAME_CACHE` | 进程级、provider 级 | 列名 / metadata | 条目数 | `512 entries` | SQLite / PostgreSQL / MySQL 查询路径内部自动使用 |

必须注意:

- `result cache` 不是字节容量,而是“最多缓存多少条查询结果”
- `local cache` 不是全局共享缓存,而是调用方持有的局部对象
- `metadata cache` 不是业务数据缓存,它缓存的是列名 / schema 辅助信息

### 三类 cache 的职责定义

#### 1. result cache

这是“跨调用复用”的主缓存。

职责:

- 缓存热点查询结果
- 避免重复访问外部数据库或 SQLite provider
- 为跨请求、跨规则执行的重复 lookup 提供复用

边界:

- 它缓存的是“查询结果”
- 它不负责单次计算内的临时去重
- 它不缓存列名 metadata

当前实现特征:

- 作用域:进程级
- key:`datasource_id + generation + query_hash + params_hash + mode`
- 容量:`1024 entries`
- 启用方式:只有 `CachePolicy::UseGlobal` 才会使用
- 默认策略:普通 `query/query_row/query_fields` 默认不走它

#### 2. local cache

这是“单次计算内去重”的兼容层缓存。

职责:

- 消除一次规则执行 / 一次转换中的重复 lookup
- 降低同一批数据处理过程中的重复 query 开销
- 作为历史 `cache_query(...)` 语义的承载者继续存在

边界:

- 它不承担跨请求一致性责任
- 它不是主缓存
- 它只对持有这个 cache 对象的调用链可见

当前实现特征:

- 作用域:调用方局部
- key:调用方给出的 `DataField` 参数组合
- 容量:默认 `100 entries`,也可 `with_capacity(size)`
- 启用方式:调用方显式走 `cache_query/cache_query_fields`
-`result cache` 的关系:`local miss` 后会继续走 `global result cache`

#### 3. metadata cache

这是“查询辅助信息缓存”,不是业务结果缓存。

职责:

- 缓存列名或 schema 辅助信息
- 降低重复 `prepare/describe` 或列信息提取的开销
- 给结果映射提供稳定的 metadata 复用

边界:

- 它不缓存查询返回的数据行
- 它不能替代 `result cache`
- 命中它并不意味着业务查询结果命中

当前实现特征:

- 作用域:进程级
- key:`datasource_id + generation + query_hash`
- 容量:`512 entries`
- 启用方式:SQLite / PostgreSQL / MySQL 内部查询路径自动使用
- 当前 value:主要是列名集合,仍偏轻量

### 三类 cache 的开关语义

这 3 类 cache 当前没有一个统一的总开关,必须分别理解:

- `result cache`
  - 代码路径开关
  - 只有 `CachePolicy::UseGlobal` 才启用
- `local cache`
  - 调用方式开关
  - 只有 `cache_query(...)` 或显式传入 `FieldQueryCache` 才启用
- `metadata cache`
  - 当前默认始终启用
  - 没有单独配置项

因此,当前“开 cache”并不是一个布尔配置,而是“调用路径是否进入对应 cache 层”。

### 当前配置契约

当前已经配置化的只有 `result cache`,并且配置必须遵守 `knowdb.toml -> EnvTomlLoad -> KnowDbConf` 的现有加载契约。

也就是说:

- 配置位置在 `knowdb.toml`
-`parse_knowdb_conf(...)` 一次性加载
-`init_thread_cloned_from_knowdb(...)` 在 provider 初始化成功后应用到 runtime
- 不额外引入独立的环境变量解析或旁路配置源

当前支持的配置如下:

```toml
[cache]
enabled = true
capacity = 1024
ttl_ms = 30000
```

语义:

- `enabled`
  - 控制 `result cache` 总开关
  - `false` 时,所有 `CachePolicy::UseGlobal` 都退化为 `Bypass`
- `capacity`
  - 单位是条目数 `entries`
  - 表示最多缓存多少条查询结果
- `ttl_ms`
  - 单位是毫秒
  - 表示 result cache entry 的最大存活时间

明确不在这组配置里的内容:

- `local cache`
  - 仍由调用方持有 `FieldQueryCache` 时自行决定
- `metadata cache`
  - 当前仍为内部固定配置,不受 `[cache]` 影响

因此,当前 `[cache]` 的准确含义应理解为:

- `result cache config`
- 不是整个 `wp-knowledge` 所有缓存层的总配置

### 三类 cache 的失效语义

#### 共同点

- 主失效机制都是 `generation`
- provider reload / knowdb reload / provider replace 后,`generation` 会递增
- 因此新查询不会再命中旧 generation 的 key

#### 差异点

`result cache`:

- 旧 entry 不会立刻物理删除
- 但因为 key 带 `generation`,逻辑上已经失效
- 后续由 LRU 淘汰

`local cache`:

- `prepare_generation(...)` 时发现 generation 变化,会直接 `reset()`
- 也就是“逻辑失效 + 物理清空”

`metadata cache`:

- `result cache` 类似,旧 key 因 generation 变化而不再命中
- 后续由容量淘汰

### 源数据变化时,哪一层会自动失效

这是最容易混淆的一点,必须单独说明。

当前只有“provider generation 变化”会触发可靠失效。

也就是说:

- 如果发生 `reload`
- 或重新安装 provider
- 或 authority 重建

那么:

- `result cache` 会因为新 generation 自然失效
- `local cache` 会直接清空
- `metadata cache` 会因为新 generation 自然失效

但如果是“外部数据库里的源表数据被别人改了”,而 `wp-knowledge` 本身没有 reload:

- `result cache` 不会自动失效
- `local cache` 也不会自动知道外部源数据变化
- `metadata cache` 通常也不会变化,除非 schema 本身变化且伴随 reload

所以当前一致性边界必须定义为:

- cache 一致性跟随 `provider generation`
- 不跟随外部数据库实时数据变化

换句话说,当前没有这些机制:

- TTL 自动过期
- CDC 驱动失效
- 表版本号探测
- 后台数据变化自动 invalidate

后续如果要配置化,建议也按这 3 层分别配置,而不是只给一个笼统的 `cache=true/false`。

### 1. 结果缓存

建议使用成熟并发缓存库 `moka::sync::Cache` 替代当前自定义 `FieldQueryCache` 作为全局缓存实现。

#### Key 结构

```rust
pub struct ResultCacheKey {
    pub datasource_id: DatasourceId,
    pub generation: Generation,
    pub query_hash: u64,
    pub params_hash: u64,
    pub mode: QueryModeTag,
}
```

要求:

- 必须包含 `datasource_id`
- 必须包含 `generation`
- 不能只按 SQL 文本缓存
- 参数需要标准化后再 hash

#### 标准化规则

- 保留参数名
- 保留参数值
- 不保留 `DataField` 的格式化元信息
-`NULL`、布尔、数字、文本做稳定编码

#### 适用场景

- 字典 lookup
- 规则评估中的热点小查询
- 白名单表上的纯读查询

#### 默认策略

- 结果缓存默认关闭
- 由调用方或规则层显式启用

#### 失效机制

- 主失效:`generation`
- 次失效:TTL
- 容量保护:`max_capacity`

### 2. 调用期缓存

当前 `FieldQueryCache` 不应直接删除,而应降级为“兼容层局部缓存”。

保留原因:

- OML/规则引擎里同一次转换会多次重复 lookup
- 局部 cache 仍然是很高 ROI 的优化
- 这层不需要并发

但它需要被重新定位:

- 不再承担全局一致性责任
- 不再被视为主缓存
- 只服务于单次计算内去重

建议后续改名为:

- `QueryLocalCache`

以避免和统一全局缓存混淆。

### 3. 列名 / 元数据缓存

当前 `COLNAME_CACHE` 应升级为统一 metadata cache,同样建议使用 `moka`。

#### Key 结构

```rust
pub struct MetadataCacheKey {
    pub datasource_id: DatasourceId,
    pub generation: Generation,
    pub query_hash: u64,
}
```

#### Value

```rust
pub struct QueryMetadata {
    pub columns: Arc<Vec<ColumnMeta>>,
}
```

```rust
pub struct ColumnMeta {
    pub name: String,
    pub logical_type: Option<String>,
}
```

第一阶段可以只填列名,先完全覆盖当前 `COLNAME_CACHE` 能力。

#### 策略

- 必须有容量上限
- 可选 TTL
- 与结果缓存一样受 `generation` 控制

## ThreadClonedMDB 的改造

这是整个方案里不能绕过的一点。

当前线程本地内存库是 provider 自己的“隐式大缓存”。如果不把它纳入 generation 体系,外层 cache 再成熟也无法保证 reload 后读到新数据。

建议将线程本地状态改成:

```rust
struct ThreadLocalState {
    generation: Generation,
    conn: Connection,
}
```

访问逻辑:

- 线程第一次访问:构建 `conn`
- 如果线程本地 generation 与 registry 当前 generation 不一致:
  - 丢弃旧 `conn`
  - 从新的 authority/provider 重建

这样才能保证:

- authority 重建后线程会自动切换到新快照
- provider 切换后线程不会继续读旧 SQLite 副本

## Facade 演进方案

### 对外兼容目标

以下 API 第一阶段继续保留:

- `query`
- `query_row`
- `query_named`
- `query_named_fields`
- `cache_query`

但内部改造为:

- 统一构造 `QueryRequest`
- 交给 `QueryRuntime`
- 由 runtime 决定是否查全局缓存

### `cache_query` 的重新定义

建议把 `cache_query` 明确定位成兼容接口:

- 继续支持旧的调用期 cache
- 内部可选叠加全局缓存
- 长期目标是弱化它的存在感

建议执行顺序:

1. 查调用期 local cache
2. 若 miss 且允许全局缓存,则查 global result cache
3. 若仍 miss,则执行 provider 查询
4. 回填 global cache
5. 回填 local cache

这样既保留单次计算内去重,也让跨请求热点查询可复用。

## Reload 与运行时管理

建议新增一个运行时状态对象:

```rust
pub struct KnowledgeRuntime {
    pub registry: ArcSwap<ProviderHandle>,
    pub result_cache: moka::sync::Cache<ResultCacheKey, Arc<QueryResult>>,
    pub metadata_cache: moka::sync::Cache<MetadataCacheKey, Arc<QueryMetadata>>,
}
```

其中 `ProviderHandle` 包含:

- provider 实例
- datasource_id
- generation
- 白名单

reload 流程:

1. 构建新 provider
2. 生成新 `datasource_id` 或沿用原 id
3. `generation += 1`
4. 原子替换 registry 当前 handle
5. 不必同步清理所有 cache,依赖 generation 自然失效

必要时可异步触发:

- `invalidate_entries_if(...)`
- `run_pending_tasks()`

但这不是 correctness 的前提。

## 观测性

如果没有指标,这套方案后续很难评估。

建议新增以下指标与日志:

- `provider.kind`
- `provider.datasource_id`
- `provider.generation`
- `cache.result.hit`
- `cache.result.miss`
- `cache.metadata.hit`
- `cache.metadata.miss`
- `cache.local.hit`
- `cache.local.miss`
- `provider.reload.success`
- `provider.reload.failure`

至少应有 debug 日志能回答:

- 当前查询打到了哪个 provider
- 当前 generation 是多少
- 是 local cache 命中还是 global cache 命中
- 是否发生了线程本地副本重建

## 性能验证结果

为避免只停留在架构层面,当前仓库已经补了 3 组可重复执行的 cache 性能测试:

- 内存 provider 对比:`tests/test-cache-perf.sh`
- PostgreSQL 真 provider 对比:`tests/test-postgres-provider.sh`
- MySQL 真 provider 对比:`tests/test-mysql-provider.sh`

对比口径统一为 3 条路径:

- `bypass`
  - 完全不使用结果缓存
- `global_cache`
  - 使用 runtime 级 result cache
- `local_cache`
  - 先查调用期 local cache,miss 时再落到 runtime result cache

### 默认样本参数

默认使用以下参数:

- `WP_KDB_PERF_ROWS=10000`
- `WP_KDB_PERF_OPS=10000`
- `WP_KDB_PERF_HOTSET=128`

含义:

- 表中准备 `10000` 条数据
- 单轮执行 `10000` 次 lookup
- 实际热点 key 集合为 `128`,故缓存命中率很高,适合评估 lookup 场景收益

### 实测结果

以下结果来自当前仓库实现、当前开发机、本地 Docker provider 集成测试实跑输出。

#### 1. 内存 provider

运行入口:

```bash
./tests/test-cache-perf.sh
```

一次实测结果:

| Scenario | Elapsed | QPS | Result Cache | Local Cache |
| --- | ---: | ---: | --- | --- |
| `bypass` | `1204 ms` | `99,658` | `hit=0 miss=0` | `hit=0 miss=0` |
| `global_cache` | `116 ms` | `1,034,157` | `hit=119872 miss=128` | `hit=0 miss=0` |
| `local_cache` | `71 ms` | `1,686,998` | `hit=0 miss=128` | `hit=119872 miss=128` |

加速比:

- `global_cache vs bypass`: `10.38x`
- `local_cache vs bypass`: `16.93x`

说明:

- 内存 provider 本身已经很快,因此 cache 主要减少的是重复 SQL 执行与重复 metadata/row 组装开销
- `local_cache` 略快于 `global_cache`,符合“同次计算内重复 lookup”场景预期

#### 2. PostgreSQL provider

运行入口:

```bash
./tests/test-postgres-provider.sh
```

一次实测结果:

| Scenario | Elapsed | QPS | Result Cache | Local Cache |
| --- | ---: | ---: | --- | --- |
| `bypass` | `5201 ms` | `1,923` | `hit=0 miss=0` | `hit=0 miss=0` |
| `global_cache` | `79 ms` | `125,212` | `hit=9872 miss=128` | `hit=0 miss=0` |
| `local_cache` | `73 ms` | `136,901` | `hit=0 miss=128` | `hit=9872 miss=128` |

加速比:

- `global_cache vs bypass`: `65.13x`
- `local_cache vs bypass`: `71.21x`

说明:

- 外部数据库场景下,cache 的收益远高于内存 provider
- 主因不是 SQL 本身复杂,而是绕开了驱动绑定、连接 checkout、网络往返与结果反序列化
- 对热点小查询,runtime result cache 已经能显著削平外部库成本

#### 3. MySQL provider

运行入口:

```bash
./tests/test-mysql-provider.sh
```

一次实测结果:

| Scenario | Elapsed | QPS | Result Cache | Local Cache |
| --- | ---: | ---: | --- | --- |
| `bypass` | `5757 ms` | `1,737` | `hit=0 miss=0` | `hit=0 miss=0` |
| `global_cache` | `82 ms` | `121,016` | `hit=9872 miss=128` | `hit=0 miss=0` |
| `local_cache` | `73 ms` | `136,587` | `hit=0 miss=128` | `hit=9872 miss=128` |

加速比:

- `global_cache vs bypass`: `69.67x`
- `local_cache vs bypass`: `78.63x`

说明:

- MySQL 与 PostgreSQL 的结论一致:在热点 lookup 场景下,cache 带来的收益是数量级级别的
- `local_cache` 继续略优于 `global_cache`,说明对单次规则执行内的重复 key,调用期缓存仍然有意义

### 结果解读

从这 3 组结果可以得到几个明确结论:

- `cache` 不是“可能有帮助”,而是对热点 lookup 场景有决定性价值
- 对外部 provider,单纯依赖连接池并不足够,真正的大头仍是重复查询路径本身
- `global_cache` 已经能覆盖跨调用复用价值
- `local_cache` 仍然值得保留,尤其适合一次转换内大量重复 key 的规则
- 因为当前实现已经把 `generation` 编入 runtime cache 失效语义,所以收益并不是以 correctness 为代价换来的

### 注意事项

- 这些数据是热点分布 workload,不代表“所有 SQL 都应该默认缓存”
- 对非确定性查询、低复用查询、结果集大的查询,cache 收益会下降,甚至可能不划算
- 当前默认参数适合日常回归;若要更接近生产场景,应按真实热点分布重跑
- 当前文档记录的是“现有实现的实测值”,不是长期性能承诺

### 建议的工程结论

基于当前实测结果,建议保持以下策略:

- 结果缓存继续默认关闭,只对明确热点查询显式启用
- 兼容层 `cache_query` 保留,作为 OML/规则执行内 local cache 的主要入口
- 对 PostgreSQL/MySQL provider,优先把高频 lookup 类 SQL 显式迁移到 `UseGlobal``cache_query`
- 后续若要继续优化,应优先做 cache 策略收敛与配置化,而不是先去微调驱动层 SQL 开销

## 迁移步骤

建议拆成 5 个阶段,避免一次性重写。

### 阶段 1:引入运行时与 generation

- 新增 `KnowledgeRuntime`
- `PROVIDER``OnceLock` 升级为可替换运行时句柄
- provider handle 显式带 `datasource_id/generation`
- 现有 facade 改为经 runtime 分发

### 阶段 2:引入统一 QueryRequest

- 新增 `QueryRequest/QueryParam/QueryResult`
- SQLite / PostgreSQL provider 迁移到统一 `execute`
- `query_named``ToSql` 兼容层适配到 `QueryParam`

### 阶段 3:引入全局结果缓存与 metadata cache

- 接入 `moka`
- metadata cache 先替换 `COLNAME_CACHE`
- result cache 先只给显式声明可缓存的 query 使用

### 阶段 4:改造 ThreadClonedMDB

- 线程本地连接带 generation
- generation 变化时自动重建副本
- 统一接入 runtime 的 provider handle

### 阶段 5:缩减历史兼容层

- 弱化 `cache_query` 的主路径地位
-`FieldQueryCache` 重命名为 `QueryLocalCache`
- 将更多热点 lookup 显式迁移到 global cache 策略

## 风险与取舍

### 风险 1:缓存污染

如果漏掉 `datasource_id` 或 `generation`,不同 provider 结果会串。

结论:

- 这两个字段必须进所有全局 cache key

### 风险 2:reload 后仍读旧数据

如果只替换 provider,不重建线程本地副本,SQLite 线程快照仍可能陈旧。

结论:

- `ThreadClonedMDB` 必须纳入 generation 体系

### 风险 3:默认缓存过度

外部数据库上的只读 SQL 并不一定是确定性的。

结论:

- 结果缓存默认关闭
- 对缓存行为采用显式策略

### 风险 4:接口一次性改太大

直接删除现有 facade API 会导致上游大面积改动。

结论:

- 保留 facade
- 先重构内部 runtime

## 建议依赖

建议新增:

```toml
moka = { version = "0.12", features = ["sync"] }
arc-swap = "1"
```

说明:

- `moka` 用于结果缓存与 metadata cache
- `arc-swap` 用于低成本原子切换当前 provider handle

## 最终判断

`wp-knowledge` 后续如果要稳定支持 SQLite 与外部数据库,必须把架构中心从“SQLite 查询工具集”转成“带版本语义的数据源查询运行时”。

这次重构的关键不是单纯替换缓存库,而是一起完成:

- provider 抽象去 SQLite 化
- runtime 接管 cache 策略
- generation 接管失效语义
- thread local snapshot 纳入统一版本管理

只有这四件事一起成立,`wp-knowledge` 才能在支持外部数据库的同时,真正拥有可维护的 cache 机制。