pure-js-db 2.54.0 → 2.55.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 CHANGED
@@ -3,11 +3,11 @@
3
3
  > **零依赖嵌入式数据库引擎 —— 单文件部署,同时支持向量搜索、MVCC 并发写入、CDC 变更捕获。**
4
4
  > 如果你的 Node.js 应用需要 PostgreSQL + Milvus + Debezium 的能力但不想要三个独立服务,这里一个文件就够。
5
5
 
6
- > 📌 **当前状态**:v2.54.0,成熟度 **Beta+(工程验证成熟)**。历史版本公告(E26 查询缓存/时间旅行、F1-F10 接线、安全审计加固等)详见 [CHANGELOG.md](CHANGELOG.md) 与 [docs/internal/audit/](docs/internal/audit/) 审计档案。安全模块默认 fail-closed,生产部署必须显式配置认证/RBAC/加密(见 [SECURITY.md](SECURITY.md) §2.2)。
6
+ > 📌 **当前状态**:v2.55.1,成熟度 **Beta+(工程验证成熟)**。历史版本公告(E26 查询缓存/时间旅行、F1-F10 接线、安全审计加固等)详见 [CHANGELOG.md](CHANGELOG.md) 与 [docs/internal/audit/](docs/internal/audit/) 审计档案。安全模块默认 fail-closed,生产部署必须显式配置认证/RBAC/加密(见 [SECURITY.md](SECURITY.md) §2.2)。
7
7
 
8
8
  ![Node.js](https://img.shields.io/badge/node-%3E%3D18.0.0-brightgreen)
9
9
  ![License](https://img.shields.io/badge/license-MIT-blue)
10
- ![Version](https://img.shields.io/badge/version-2.54.0-blue)
10
+ ![Version](https://img.shields.io/badge/version-2.55.1-blue)
11
11
  ![Tests](https://img.shields.io/badge/tests-826%20files-brightgreen)
12
12
  ![LOC](https://img.shields.io/badge/loc-~225k-blue)
13
13
  ![Maturity](https://img.shields.io/badge/maturity-Beta%2B%20engineering%20validated-orange)
@@ -48,6 +48,34 @@ console.log(result.rows);
48
48
 
49
49
  ---
50
50
 
51
+ ## 🤖 给 AI Agent 一个本地记忆库(3 步)
52
+
53
+ 零依赖纯 JS + 内置 MCP Server + 数据 100% 留在本地磁盘——这是 AI Agent 本地存储的最短路径(无需编译链、无需云服务、无需 Docker)。
54
+
55
+ **① 安装**:`npm i pure-js-db`
56
+
57
+ **② 配置 Claude Desktop / Cursor**(MCP stdio 直连):
58
+
59
+ ```json
60
+ {
61
+ "mcpServers": {
62
+ "pure-js-db": {
63
+ "command": "node",
64
+ "args": [
65
+ "<项目路径>/node_modules/pure-js-db/scripts/mcp-server.mjs",
66
+ "--db-path", "./mcpdata", "--allow-write"
67
+ ]
68
+ }
69
+ }
70
+ }
71
+ ```
72
+
73
+ **③ 对 AI 说**:"建一张 memories 表,把我的偏好存进去,下次会话能查到。" —— 完成。数据落盘 `./mcpdata`,跨会话持久,26 个 MCP 工具(SQL/事务/向量检索/图查询/备份/CDC)全部可用。
74
+
75
+ 📘 完整教程(含探针脚本与 FAQ):[《用 Pure-JS-DB 给 Claude 一个本地记忆库》](docs/tutorials/ai-agent-local-memory.md) · MCP 详情见下文 [MCP Server 章节](#mcp-server600-行)。
76
+
77
+ ---
78
+
51
79
  ## 按场景快速入门
52
80
 
53
81
  ### 场景 1:Web 服务需要并发写入
@@ -173,6 +201,27 @@ await db.save();
173
201
 
174
202
  参见 [src/storage/persisted-db.js](src/storage/persisted-db.js) — `STABLE`
175
203
 
204
+ ### 场景 5b:持久化到磁盘(文件系统 + 跨进程重启恢复)
205
+
206
+ > 用 `FileKVAdapter` 把数据真正落到磁盘目录,进程重启 / 换新实例后 `load()` 完整恢复数据 + 模式 + 索引。
207
+
208
+ ```javascript
209
+ import { PersistedDatabase, FileKVAdapter } from 'pure-js-db';
210
+
211
+ // 建库,数据写到 ./mydb 目录(数据页 / 索引 / 元数据 / manifest)
212
+ const db = new PersistedDatabase('app', new FileKVAdapter('./mydb'));
213
+ await db.load();
214
+ // ... 写入 / 建索引 ...
215
+ await db.save();
216
+ await db.close();
217
+
218
+ // 模拟进程重启:指向同一目录即可完整恢复
219
+ const db2 = new PersistedDatabase('app', new FileKVAdapter('./mydb'));
220
+ await db2.load();
221
+ ```
222
+
223
+ 可复现演示(建库 → 写 → 关 → 新实例 load 恢复):`node scripts/demo-persist-roundtrip.mjs`(`npm run demo:persist`)
224
+
176
225
  ### 场景 6:用 ActiveRecord 风格 ORM 操作数据库(v1.29.0+)
177
226
 
178
227
  > ✅ **v2.50.0 已接线**:ORM 模块已从主入口导出 `defineModel` / `Schema` / `QueryBuilder` / `transaction` 等 API(F5 接线 2026-08-17),以下示例可直接运行。npm 子路径导入请使用 `pure-js-db/src/orm/index.js`。
@@ -661,7 +710,7 @@ Pure-JS-DB 提供 **双引擎模式**,根据运行环境自动选择最优执
661
710
  | 引擎 | 适用环境 | 性能特点 | 加速方式 |
662
711
  |------|---------|---------|---------|
663
712
  | **纯 JS 引擎** | 浏览器/小程序/边缘函数 | 零依赖,v2.27.0 优化后 DELETE 0.56ms/1K行 | 批量触发器合并 + WHERE 条件下推 |
664
- | **Rust NAPI 加速** | Node.js + Rust 模块 | SIMD 加速,聚合计算旧基准宣称最高 **45x**(2026-10-27 独立性能评测发现加速层多数路径未在生产接线,实际收益有限;评测暴露的 AVX2 正确性 bug 已于 2026-10-28 修复;见 `docs/评估/Pure-JS-DB-独立第三方综合评测报告-2026-10-27.md`) | AVX2/AVX-512 指令集 |
713
+ | **Rust NAPI 加速** | Node.js + Rust 模块 | SIMD 加速。**2026-11-06 更新:2026-10-27 评测指出的全部未接线项已闭环**——TopN(性能门禁 12.2x,1M 行 top-10 完整路径 16x)、向量化过滤(列存 BitmapFilter 接线,低选择率 2.1x)、F32 批量聚合(sum/min/max Rust 实现补齐 + countGtI32 绑定名失配修复)、单组聚合(sumF64/minF64/maxF64)。旧基准"最高 45x"口径已废弃,以门禁与实测为准(`node scripts/perf-baseline-gate.cjs` 可复现) | AVX2 指令集 |
665
714
 
666
715
  ### 自动引擎选择
667
716
 
@@ -699,7 +748,7 @@ const backend = getAccelerationBackend();
699
748
 
700
749
  ### Rust NAPI 加速引擎性能 (SIMD)
701
750
 
702
- > ⚠️ **2026-10-27 独立评测修正(2026-10-28 部分已修复)**:下表数据来自 v1.32 时代的旧基准。独立评测实测发现:Rust 加速层多数路径未在生产代码接线(4/5 性能声称失效);评测同时暴露三处 AVX2 正确性 bug(utf8 批量编码、Float64 min/max 全 NaN、null bitmap ≥0x80),该三处已在 2026-10-28 修复并重建 `native/index.node`、相关 Native 路径已恢复。下表加速比仅供参考,不构成当前版本(v2.54.0)的性能承诺。
751
+ > ⚠️ **2026-10-27 独立评测修正(2026-10-28 部分已修复)**:下表数据来自 v1.32 时代的旧基准。独立评测实测发现:Rust 加速层多数路径未在生产代码接线(4/5 性能声称失效);评测同时暴露三处 AVX2 正确性 bug(utf8 批量编码、Float64 min/max 全 NaN、null bitmap ≥0x80),该三处已在 2026-10-28 修复并重建 `native/index.node`、相关 Native 路径已恢复。下表加速比仅供参考,不构成当前版本(v2.55.1)的性能承诺。
703
752
  >
704
753
  > 详见 [SIMD 优化报告](docs/simd/simd-final-optimization-report.md)
705
754
 
package/native/index.node CHANGED
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pure-js-db",
3
- "version": "2.54.0",
3
+ "version": "2.55.1",
4
4
  "description": "Pure JavaScript 关系型数据库引擎 - 面向小程序/受限运行时环境。零 C/C++/WASM 依赖,支持 SQL 解析、B+树索引、查询优化、物化视图、数据分片、分布式事务、Worker Pool 并行、SharedArrayBuffer 共享内存、HTAP 混合存储、多语言客户端 SDK。可选 Rust NAPI 原生加速层(编译后性能提升 2-5x)。",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -15,6 +15,7 @@
15
15
  "./internal": {
16
16
  "default": "./src/internal/public-api.js"
17
17
  },
18
+ "./version": "./src/version.js",
18
19
  "./v3": "./src/index-v3.js"
19
20
  },
20
21
  "sideEffects": false,
@@ -53,6 +54,7 @@
53
54
  "start:prod": "node --max-old-space-size=4096 --max-semi-space-size=64 --expose-gc scripts/start-server.js",
54
55
  "cli": "node --max-old-space-size=4096 --max-semi-space-size=64 src/cli.js",
55
56
  "demo": "node src/cli.js --init",
57
+ "demo:persist": "node scripts/demo-persist-roundtrip.mjs",
56
58
  "load-test": "node test/benchmark/load-test.mjs --init --warmup",
57
59
  "bench:storage": "node test/bench_binary_storage.js",
58
60
  "bench:random-read": "node test/bench-random-read-compare.js",
@@ -208,3 +208,7 @@ export { LineageStore, extractSourceTables, extractSourceTablesFromSQL, extractC
208
208
  // 部署配置与启动 (生产环境)
209
209
  export { config as productionConfig } from '../../config/production.config.js';
210
210
  export { setupSoADaemon } from '../daemon/soa-daemon-integration.js';
211
+
212
+ // v2.55.1: 批量聚合器(Rust/JS 自动选择)——batchSumF32 系列此前不在任何公开导出面,
213
+ // exports 收紧后成为死导出;经 engine 子入口对用户可达。
214
+ export { createBatchAggregator } from '../native/native-acceleration.js';
@@ -507,6 +507,8 @@ export class Executor {
507
507
  /** @type {string[]} v2.12 E16 M2-T2: 当前事务已创建的 SAVEPOINT 名栈(新库 join 时补录) */
508
508
  this._txSavepoints = [];
509
509
  this.optimizer = null; // 延迟初始化成本优化器
510
+ /** @type {Object|null} TPC-H 慢查询专项:JOIN 哈希表跨查询缓存(懒创建,见 _getJoinHashTableCache) */
511
+ this._joinHashTableCache = null;
510
512
  /** @type {ViewManager|null} */
511
513
  this.viewManager = new ViewManager(this.db, this);
512
514
  /** @type {LineageStore} v2.47.0 P7-M1: 数据血缘存储(表级血缘自动记录) */
@@ -2659,6 +2661,26 @@ export class Executor {
2659
2661
  return this.execSelectParallel(stmt, options);
2660
2662
  }
2661
2663
 
2664
+ // TPC-H 慢查询专项:JOIN 复合行列裁剪——仅顶层 SELECT 分发注入
2665
+ // _requiredColumns;INSERT...SELECT / 子查询等内部 execSelect 调用不携带
2666
+ // 该标记 → 自动维持全列行为(安全边界见 _computeRequiredColumns 注释)。
2667
+ if (!options._requiredColumns) {
2668
+ const requiredColumns = this._computeRequiredColumns(stmt);
2669
+ if (requiredColumns) {
2670
+ options = { ...options, _requiredColumns: requiredColumns };
2671
+ // 无限定列引用 ⇒ __table__ 命名空间零消费 → 跳过赋值(复合行再降 ~40% 宽度);
2672
+ // 含子查询时保守保留 __table__(相关子查询提升/物化链路的隐式依赖,Q20 回归)
2673
+ if (!requiredColumns.hasQualified && !requiredColumns.hasSubquery) {
2674
+ options._skipTableNamespaces = true;
2675
+ }
2676
+ }
2677
+ }
2678
+ // TPC-H 慢查询专项:JOIN 哈希表跨查询缓存——v1.43.0 的 _hashTableCache
2679
+ // 机制一直无生产者,同一查询重复执行每次重建右表哈希表(SF=1 lineitem
2680
+ // 60K 行 ≈ 5-10ms/次)。失效以表对象 + Table._dataVersion(DML 必递增)为键。
2681
+ if (!options._hashTableCache && typeof this._getJoinHashTableCache === 'function') {
2682
+ options._hashTableCache = this._getJoinHashTableCache();
2683
+ }
2662
2684
  result = this.execSelect(stmt, options);
2663
2685
  } else if (stmt instanceof InsertStatement) {
2664
2686
  result = this.execInsert(stmt, options);
@@ -3023,6 +3045,153 @@ export class Executor {
3023
3045
  * @param {Object} [db] - 数据库实例(用于查目标表触发器)
3024
3046
  * @returns {boolean}
3025
3047
  */
3048
+ /**
3049
+ * JOIN 复合行列裁剪(TPC-H 慢查询专项,2026-11-06)
3050
+ *
3051
+ * 计算整个 SELECT 语句引用的列名集合(全 AST 递归:SELECT/WHERE/GROUP BY/
3052
+ * HAVING/ORDER BY/JOIN ON/子查询/窗口),供 JoinEngine._hashJoin 在构建
3053
+ * 复合行时按需拷贝右表新键——复合行宽度从"全部表列"降到"查询实际引用列",
3054
+ * 展开成本随行宽线性下降(Q5 实测 JOIN 步耗时 -60%+)。
3055
+ *
3056
+ * 保守禁用(返回 null → 不裁剪,维持全列拷贝):
3057
+ * - 投影含 Star(SELECT * / 任何层 SELECT 的 columns 含 Star,含 COUNT(*) 场景按
3058
+ * 投影 Star 判定)——消费方可能按位置/全列使用;
3059
+ * - 集合操作(UNION/EXCEPT/INTERSECT,setOps)——列按位置对应,裁剪有错位风险;
3060
+ * - 无任何列引用(SELECT 1 等)——无收益。
3061
+ *
3062
+ * 适用边界:仅 execute() 的顶层 SELECT 分发注入 _requiredColumns;INSERT...SELECT、
3063
+ * 标量/EXISTS 子查询等内部 execSelect 调用不携带该标记 → 自动维持全列行为。
3064
+ *
3065
+ * @param {Object} stmt - SELECT 语句 AST
3066
+ * @returns {Set<string>|null} 需要的列名集合;null = 不裁剪
3067
+ */
3068
+ _computeRequiredColumns(stmt) {
3069
+ const required = new Set();
3070
+ let hasStar = false;
3071
+ let hasSetOp = false;
3072
+ let hasQualified = false;
3073
+ let hasSubquery = false;
3074
+ const walk = (node, depth) => {
3075
+ if (!node || typeof node !== 'object' || depth > 64) return;
3076
+ if (Array.isArray(node)) {
3077
+ for (const item of node) walk(item, depth + 1);
3078
+ return;
3079
+ }
3080
+ const cname = node.constructor && node.constructor.name;
3081
+ if (cname === 'ColumnRef') {
3082
+ required.add(node.columnName);
3083
+ if (node.tableName) hasQualified = true;
3084
+ return;
3085
+ }
3086
+ // 子查询节点(InSubquery/ScalarSubquery/ExistsSubquery 等):含子查询的语句
3087
+ // 禁用 _skipTableNamespaces——相关子查询的提升/物化链路(flattener temp 表、
3088
+ // _replaceColumnRefs 外层绑定)对复合行结构有隐式依赖(Q20 回归实测),
3089
+ // 保守保留 __table__ 命名空间。
3090
+ if (cname && cname.includes('Subquery')) hasSubquery = true;
3091
+ // 集合操作节点(UNION/EXCEPT/INTERSECT 解析为 {left, operation, right})→ 禁裁剪
3092
+ if (typeof node.operation === 'string' && node.left && node.right) {
3093
+ hasSetOp = true;
3094
+ }
3095
+ // 投影 Star 检测:任何层 SELECT 的 columns 数组(元素为 SelectItem{expression} 或 Star);
3096
+ // COUNT(*) 的 args Star 不产生输出列,忽略
3097
+ if (Array.isArray(node.columns)) {
3098
+ for (const c of node.columns) {
3099
+ const expr = c && c.expression ? c.expression : c;
3100
+ if (expr && expr.constructor && expr.constructor.name === 'Star') hasStar = true;
3101
+ }
3102
+ }
3103
+ for (const key of Object.keys(node)) {
3104
+ if (key === 'loc' || key === '_parent' || key === '_optimized' || key === '__rowId') continue;
3105
+ const v = node[key];
3106
+ if (v && typeof v === 'object') walk(v, depth + 1);
3107
+ }
3108
+ };
3109
+ walk(stmt, 0);
3110
+ if (hasStar || hasSetOp || required.size === 0) return null;
3111
+ // hasQualified/hasSubquery 挂在返回 Set 上:无限定列时 execute() 额外注入
3112
+ // _skipTableNamespaces(_hashJoin 跳过 __table__ 命名空间赋值,复合行再降
3113
+ // ~40% 宽度)。无限定列 ⇒ __table__ 的全部消费点(限定列解析/_getTableRow/
3114
+ // graph-table)均不被触发。**含子查询时仅禁 ns 跳过、保留列裁剪**(相关子查询
3115
+ // 的提升/物化链路对复合行结构有隐式依赖,Q20 回归实测)。
3116
+ required.hasQualified = hasQualified;
3117
+ required.hasSubquery = hasSubquery;
3118
+ return required;
3119
+ }
3120
+
3121
+ /**
3122
+ * JOIN 哈希表跨查询缓存(TPC-H 慢查询专项,2026-11-07)
3123
+ *
3124
+ * 背景:JoinEngine._hashJoin/_buildHashTable 的 `_hashTableCache` 机制(v1.43.0)
3125
+ * 一直只有消费者没有生产者——SELECT 链路从未注入,导致同一查询重复执行时每次
3126
+ * 重建右表哈希表(SF=1 下 lineitem 60K 行 build ≈ 5-10ms/次)。
3127
+ *
3128
+ * 失效正确性(本实现与旧"行数版"的关键差异):
3129
+ * 旧接口用 rightRows.length 做版本号——UPDATE/DELETE 不改行数时返回脏哈希表
3130
+ * (错误结果)。本缓存以【表对象(WeakMap)+ Table._dataVersion(DML 必递增)+
3131
+ * 键列名】为键:任何 DML 递增版本 → 自动失效;DROP 后 getTable 为空 → 不命中;
3132
+ * CREATE 同名新表为新对象(WeakMap 无旧缓存)→ 不命中。
3133
+ *
3134
+ * 缓存范围:仅键表达式为单列 ColumnRef(columnName 可提取)的等值 JOIN——复杂
3135
+ * 键表达式不缓存(保守)。上限 64 条,超限整段清空。
3136
+ *
3137
+ * @returns {Object} { get(tableName, rowLen, keyExpr), set(tableName, rowLen, keyExpr, ht, bloom) }
3138
+ */
3139
+ _getJoinHashTableCache() {
3140
+ if (this._joinHashTableCache) {
3141
+ return this._joinHashTableCache;
3142
+ }
3143
+ const db = this.db;
3144
+ const MAX_ENTRIES = 64;
3145
+ const store = new Map(); // WeakMap<tableObj, Map<"version|colKey", {ht, bloom}>>
3146
+
3147
+ function keyOf(tableName, keyExpr, rowLen) {
3148
+ if (!keyExpr || keyExpr.constructor?.name !== 'ColumnRef' || !keyExpr.columnName) {
3149
+ return null; // 复杂键表达式不缓存
3150
+ }
3151
+ // 别名右表(FROM supplier s2)的 tableName 是别名而非基表名——getTable 对
3152
+ // 不存在的表抛错 → 缓存键解析失败必须保守返回 null(不缓存,走原路径)
3153
+ let table = null;
3154
+ try {
3155
+ table = db && typeof db.getTable === 'function' ? db.getTable(tableName) : null;
3156
+ } catch {
3157
+ return null;
3158
+ }
3159
+ if (!table || typeof table._dataVersion !== 'number') {
3160
+ return null; // 表不存在(DROP)/别名/版本不可用 → 不缓存
3161
+ }
3162
+ // 仅缓存全表扫描行集:下推/过滤后的部分行集(rightRows ⊂ 表行)其内容取决于
3163
+ // 具体 WHERE,仅按表+版本+键列做键会跨查询脏撞(不同 WHERE 同行数)→ 保守
3164
+ // 只在 build 行数 === 表行数 时缓存。
3165
+ if (rowLen !== table.rows.length) {
3166
+ return null;
3167
+ }
3168
+ return { table, version: table._dataVersion, colKey: keyExpr.columnName };
3169
+ }
3170
+
3171
+ function lookup(tableName, rowLen, keyExpr) {
3172
+ const k = keyOf(tableName, keyExpr, rowLen);
3173
+ if (!k) return null;
3174
+ const byVersion = store.get(k.table);
3175
+ if (!byVersion) return null;
3176
+ return byVersion.get(`${k.version}|${k.colKey}`) || null;
3177
+ }
3178
+
3179
+ function put(tableName, rowLen, keyExpr, ht, bloom) {
3180
+ const k = keyOf(tableName, keyExpr, rowLen);
3181
+ if (!k) return;
3182
+ let byVersion = store.get(k.table);
3183
+ if (!byVersion) {
3184
+ byVersion = new Map();
3185
+ store.set(k.table, byVersion);
3186
+ }
3187
+ if (byVersion.size >= MAX_ENTRIES) byVersion.clear();
3188
+ byVersion.set(`${k.version}|${k.colKey}`, { ht, bloom });
3189
+ }
3190
+
3191
+ this._joinHashTableCache = { get: lookup, set: put };
3192
+ return this._joinHashTableCache;
3193
+ }
3194
+
3026
3195
  _isImmutableCacheableInsert(ast, db) {
3027
3196
  const stmts = ast && ast.statements;
3028
3197
  if (!stmts || stmts.length !== 1) {
@@ -3481,10 +3650,11 @@ export class Executor {
3481
3650
  // 从 table.rows 取对应行(保持行存格式,确保兼容性)
3482
3651
  const rows = indices.map(i => table.rows[i]);
3483
3652
 
3484
- // 构造投影后的行
3653
+ // 构造投影后的行({} 原型,与行存路径 _rowPool 的 {} 原型行一致——
3654
+ // 原 Object.create(null) 与行存输出原型不同,deepStrictEqual 可区分)
3485
3655
  const projectedRows = [];
3486
3656
  for (const row of rows) {
3487
- const projRow = Object.create(null);
3657
+ const projRow = {};
3488
3658
  let hasStar = false;
3489
3659
  for (const item of selectItems) {
3490
3660
  if (item.expression instanceof Star) {
@@ -3496,13 +3666,24 @@ export class Executor {
3496
3666
  projRow[outputName] = row[colName];
3497
3667
  }
3498
3668
  if (hasStar) {
3499
- // SELECT * — 直接复制整行(去掉 __rowId)
3500
- projectedRows.push({ ...row });
3669
+ // SELECT * — 与行存 Star 语义一致:仅输出表声明列(排除 __rowId 等
3670
+ // 系统键;原 {...row} 整行拷贝泄漏 __rowId,行存路径不携带)
3671
+ const starRow = {};
3672
+ for (const col of table.columns) {
3673
+ if (col.name in row) {
3674
+ starRow[col.name] = row[col.name];
3675
+ }
3676
+ }
3677
+ projectedRows.push(starRow);
3501
3678
  } else {
3502
3679
  projectedRows.push(projRow);
3503
3680
  }
3504
3681
  }
3505
3682
 
3683
+ // 投影类型格式化(JSON/JSONB/ARRAY/GEOMETRY/TIMESTAMPTZ)——与行存路径一致;
3684
+ // 修复前返回列存原始值(ARRAY/JSON 为数组/对象、TIMESTAMPTZ 无会话时区)
3685
+ const formattedRows = this._formatTypedColumns(projectedRows, table);
3686
+
3506
3687
  // 构造输出列元信息
3507
3688
  const outputColumns = [];
3508
3689
  for (const item of selectItems) {
@@ -3522,9 +3703,9 @@ export class Executor {
3522
3703
 
3523
3704
  return {
3524
3705
  type: 'SELECT',
3525
- rows: projectedRows,
3706
+ rows: formattedRows,
3526
3707
  columns: outputColumns,
3527
- rowCount: projectedRows.length,
3708
+ rowCount: formattedRows.length,
3528
3709
  _columnarAccelerated: true,
3529
3710
  };
3530
3711
  }
@@ -5332,63 +5513,82 @@ export class Executor {
5332
5513
  });
5333
5514
 
5334
5515
  // JSON/JSONB/ARRAY/GEOMETRY/TIMESTAMPTZ 类型输出:自动序列化
5335
- if (table && table.columns) {
5336
- const jsonColumns = new Set(table.columns.filter(c => c.type === 'JSON' || c.type === 'JSONB').map(c => c.name));
5337
- if (jsonColumns.size > 0) {
5338
- resultRows = resultRows.map(row => {
5339
- const formatted = { ...row };
5340
- for (const colName of jsonColumns) {
5341
- if (colName in formatted && typeof formatted[colName] === 'object' && formatted[colName] !== null) {
5342
- formatted[colName] = formatJSON(formatted[colName]);
5343
- }
5516
+ // (2026-11-07 抽取为 _formatTypedColumns 共享方法,供列存模式 B 复用)
5517
+ return this._formatTypedColumns(resultRows, table);
5518
+ }
5519
+
5520
+ /**
5521
+ * 投影类型格式化(JSON/JSONB/ARRAY/GEOMETRY/TIMESTAMPTZ 输出形态)
5522
+ *
5523
+ * 2026-11-07 从 _applyProjection/_projectRowsCore 的两份内联块抽取为共享方法,
5524
+ * 并接线列存加速模式 B(_tryColumnarFilterProject)——此前模式 B 返回列存原始值
5525
+ * (ARRAY/JSON 为数组/对象、TIMESTAMPTZ 无会话时区格式化),与行存路径输出类型
5526
+ * 不一致(同一列不同查询形状返回类型不同)。
5527
+ * 语义与原内联块逐行一致:按表声明列收集类型列,逐行浅拷贝后格式化;
5528
+ * 表无类型列时原样返回(零开销)。行键为输出列名(别名替换后与表列名不同的
5529
+ * 列不参与格式化——与原两处内联块行为一致,未收窄)。
5530
+ * @param {Object[]} resultRows 投影后的行
5531
+ * @param {Object|null} table
5532
+ * @returns {Object[]} 格式化后的行
5533
+ */
5534
+ _formatTypedColumns(resultRows, table) {
5535
+ if (!table || !table.columns) {
5536
+ return resultRows;
5537
+ }
5538
+ const jsonColumns = new Set(table.columns.filter(c => c.type === 'JSON' || c.type === 'JSONB').map(c => c.name));
5539
+ if (jsonColumns.size > 0) {
5540
+ resultRows = resultRows.map(row => {
5541
+ const formatted = { ...row };
5542
+ for (const colName of jsonColumns) {
5543
+ if (colName in formatted && typeof formatted[colName] === 'object' && formatted[colName] !== null) {
5544
+ formatted[colName] = formatJSON(formatted[colName]);
5344
5545
  }
5345
- return formatted;
5346
- });
5347
- }
5348
- const arrayColumns = new Set(table.columns.filter(c => c.type === 'ARRAY').map(c => c.name));
5349
- if (arrayColumns.size > 0) {
5350
- resultRows = resultRows.map(row => {
5351
- const formatted = { ...row };
5352
- for (const colName of arrayColumns) {
5353
- if (colName in formatted && Array.isArray(formatted[colName])) {
5354
- formatted[colName] = JSON.stringify(formatted[colName]);
5355
- }
5546
+ }
5547
+ return formatted;
5548
+ });
5549
+ }
5550
+ const arrayColumns = new Set(table.columns.filter(c => c.type === 'ARRAY').map(c => c.name));
5551
+ if (arrayColumns.size > 0) {
5552
+ resultRows = resultRows.map(row => {
5553
+ const formatted = { ...row };
5554
+ for (const colName of arrayColumns) {
5555
+ if (colName in formatted && Array.isArray(formatted[colName])) {
5556
+ formatted[colName] = JSON.stringify(formatted[colName]);
5356
5557
  }
5357
- return formatted;
5358
- });
5359
- }
5360
- const geomColumns = new Set(table.columns.filter(c => c.type === 'GEOMETRY').map(c => c.name));
5361
- if (geomColumns.size > 0) {
5362
- resultRows = resultRows.map(row => {
5363
- const formatted = { ...row };
5364
- for (const colName of geomColumns) {
5365
- if (colName in formatted && formatted[colName] && typeof formatted[colName].toWKT === 'function') {
5366
- formatted[colName] = formatted[colName].toWKT();
5367
- }
5558
+ }
5559
+ return formatted;
5560
+ });
5561
+ }
5562
+ const geomColumns = new Set(table.columns.filter(c => c.type === 'GEOMETRY').map(c => c.name));
5563
+ if (geomColumns.size > 0) {
5564
+ resultRows = resultRows.map(row => {
5565
+ const formatted = { ...row };
5566
+ for (const colName of geomColumns) {
5567
+ if (colName in formatted && formatted[colName] && typeof formatted[colName].toWKT === 'function') {
5568
+ formatted[colName] = formatted[colName].toWKT();
5368
5569
  }
5369
- return formatted;
5370
- });
5371
- }
5372
- const tstzColumns = new Set(table.columns.filter(c => c.type === 'TIMESTAMPTZ').map(c => c.name));
5373
- if (tstzColumns.size > 0) {
5374
- const tz = (this.db && typeof this.db._sessionTimeZoneOffset === 'number')
5375
- ? this.db._sessionTimeZoneOffset : 0;
5376
- resultRows = resultRows.map(row => {
5377
- const formatted = { ...row };
5378
- for (const colName of tstzColumns) {
5379
- if (colName in formatted && typeof formatted[colName] === 'string') {
5380
- const p = parseTimestampTZ(formatted[colName]);
5381
- const base = p ? p.date : parseWallTimeUTC(formatted[colName]);
5382
- if (base && !isNaN(base.getTime())) {
5383
- formatted[colName] = formatTimestampTZ(base, tz);
5384
- }
5570
+ }
5571
+ return formatted;
5572
+ });
5573
+ }
5574
+ const tstzColumns = new Set(table.columns.filter(c => c.type === 'TIMESTAMPTZ').map(c => c.name));
5575
+ if (tstzColumns.size > 0) {
5576
+ const tz = (this.db && typeof this.db._sessionTimeZoneOffset === 'number')
5577
+ ? this.db._sessionTimeZoneOffset : 0;
5578
+ resultRows = resultRows.map(row => {
5579
+ const formatted = { ...row };
5580
+ for (const colName of tstzColumns) {
5581
+ if (colName in formatted && typeof formatted[colName] === 'string') {
5582
+ const p = parseTimestampTZ(formatted[colName]);
5583
+ const base = p ? p.date : parseWallTimeUTC(formatted[colName]);
5584
+ if (base && !isNaN(base.getTime())) {
5585
+ formatted[colName] = formatTimestampTZ(base, tz);
5385
5586
  }
5386
5587
  }
5387
- return formatted;
5388
- });
5389
- }
5588
+ }
5589
+ return formatted;
5590
+ });
5390
5591
  }
5391
-
5392
5592
  return resultRows;
5393
5593
  }
5394
5594
 
@@ -5537,68 +5737,9 @@ export class Executor {
5537
5737
  return clean;
5538
5738
  });
5539
5739
 
5540
- // JSON/JSONB/ARRAY 类型输出:自动序列化
5541
- if (table && table.columns) {
5542
- const jsonColumns = new Set(table.columns.filter(c => c.type === 'JSON' || c.type === 'JSONB').map(c => c.name));
5543
- if (jsonColumns.size > 0) {
5544
- resultRows = resultRows.map(row => {
5545
- const formatted = { ...row };
5546
- for (const colName of jsonColumns) {
5547
- if (colName in formatted && typeof formatted[colName] === 'object' && formatted[colName] !== null) {
5548
- formatted[colName] = formatJSON(formatted[colName]);
5549
- }
5550
- }
5551
- return formatted;
5552
- });
5553
- }
5554
- const arrayColumns = new Set(table.columns.filter(c => c.type === 'ARRAY').map(c => c.name));
5555
- if (arrayColumns.size > 0) {
5556
- resultRows = resultRows.map(row => {
5557
- const formatted = { ...row };
5558
- for (const colName of arrayColumns) {
5559
- if (colName in formatted && Array.isArray(formatted[colName])) {
5560
- formatted[colName] = JSON.stringify(formatted[colName]);
5561
- }
5562
- }
5563
- return formatted;
5564
- });
5565
- }
5566
-
5567
- // GEOMETRY 类型输出:自动序列化为 WKT
5568
- const geomColumns = new Set(table.columns.filter(c => c.type === 'GEOMETRY').map(c => c.name));
5569
- if (geomColumns.size > 0) {
5570
- resultRows = resultRows.map(row => {
5571
- const formatted = { ...row };
5572
- for (const colName of geomColumns) {
5573
- if (colName in formatted && formatted[colName] && typeof formatted[colName].toWKT === 'function') {
5574
- formatted[colName] = formatted[colName].toWKT();
5575
- }
5576
- }
5577
- return formatted;
5578
- });
5579
- }
5580
-
5581
- // v2.0-12 G12: TIMESTAMPTZ 类型输出 — 内部以 UTC 存储(无后缀字符串),
5582
- // 展示时按会话时区偏移转换为 'YYYY-MM-DD HH:MM:SS±HH:MM'
5583
- const tstzColumns = new Set(table.columns.filter(c => c.type === 'TIMESTAMPTZ').map(c => c.name));
5584
- if (tstzColumns.size > 0) {
5585
- const tz = (this.db && typeof this.db._sessionTimeZoneOffset === 'number')
5586
- ? this.db._sessionTimeZoneOffset : 0;
5587
- resultRows = resultRows.map(row => {
5588
- const formatted = { ...row };
5589
- for (const colName of tstzColumns) {
5590
- if (colName in formatted && typeof formatted[colName] === 'string') {
5591
- const p = parseTimestampTZ(formatted[colName]);
5592
- const base = p ? p.date : parseWallTimeUTC(formatted[colName]);
5593
- if (base && !isNaN(base.getTime())) {
5594
- formatted[colName] = formatTimestampTZ(base, tz);
5595
- }
5596
- }
5597
- }
5598
- return formatted;
5599
- });
5600
- }
5601
- }
5740
+ // JSON/JSONB/ARRAY/GEOMETRY/TIMESTAMPTZ 类型输出:自动序列化
5741
+ // (2026-11-07 抽取为 _formatTypedColumns 共享方法,与 _projectRowsCore 共用)
5742
+ resultRows = this._formatTypedColumns(resultRows, table);
5602
5743
 
5603
5744
  // 清理子查询展开创建的临时表(已缓存的保留,供后续查询复用)
5604
5745
  for (const alias of _flattenTempTables) {
@@ -368,7 +368,7 @@ export class JoinExecutor {
368
368
  */
369
369
  _pushJoinPredicates(leftRows, rightRows, leftAlias, rightAlias, stmt, options, joinType = null) {
370
370
  const conditions = this._flattenAND(stmt.where);
371
- if (conditions.length <= 1) {
371
+ if (conditions.length === 0) {
372
372
  return { leftRows, rightRows };
373
373
  }
374
374
 
@@ -397,6 +397,47 @@ export class JoinExecutor {
397
397
  continue;
398
398
  }
399
399
  }
400
+ // TPC-H 慢查询专项(2026-11-06):未限定列名的单表条件下推
401
+ // Q3/Q5 的 WHERE 全部使用未限定列名(r_name/o_orderdate/l_shipdate),上方
402
+ // `refs.every(r => r.tableName)` 全不满足 → 下推完全失效 → JOIN 中间行数翻倍
403
+ // (实测 Q5 第 1 步 orders 未过滤 100→50、region 未过滤 5→1)。
404
+ // 归属判定:未限定列按「列名在右表行 / 左行中的存在性」解析——
405
+ // 全部列仅在右表行 → 下推右表(OUTER 由 canPushRight 保护);
406
+ // 全部列仅在左行 → 下推左行(INNER 语义安全,OUTER 由 canPushLeft 保护);
407
+ // 歧义(左右都有)/空行 → 保持原位(保守,不改变语义)。
408
+ if (refs.length > 0 && refs.every(r => !r.tableName)) {
409
+ // TPC-H 慢查询专项边界:条件中含子查询(Subquery/SubqueryExpr 等节点)时,
410
+ // evaluate 需要 context._subqueryExecutor(_pushJoinPredicates 的 options
411
+ // 不保证携带),且子查询引用外层列时按存在性归属判定也可能误判 → 保守保持
412
+ // 原位(由 JOIN 后的 WHERE 阶段正常求值,语义不变)。
413
+ let hasSubquery = false;
414
+ this._walkExpr(cond, (n) => {
415
+ const cn = n.constructor && n.constructor.name;
416
+ // InSubquery/ExistsSubquery 等子查询类节点:evaluate 需要
417
+ // context._subqueryExecutor(本方法 options 不保证携带)→ 保守保持原位
418
+ if (cn && (cn.includes('Subquery') || cn === 'ExistsExpr')) {
419
+ hasSubquery = true;
420
+ }
421
+ });
422
+ if (!hasSubquery) {
423
+ const cols = refs.map(r => r.columnName);
424
+ const l0 = leftRows[0];
425
+ const r0 = rightRows[0];
426
+ if (l0 && r0) {
427
+ const inRight = cols.every(c => c !== '__rowId' && Object.prototype.hasOwnProperty.call(r0, c));
428
+ const inLeft = cols.every(c => c !== '__rowId' && Object.prototype.hasOwnProperty.call(l0, c));
429
+ if (inRight !== inLeft) {
430
+ if (inRight && canPushRight) {
431
+ rRows = rRows.filter(r => evaluate(cond, r, { params }) === true);
432
+ continue;
433
+ } else if (inLeft && canPushLeft) {
434
+ lRows = lRows.filter(r => evaluate(cond, r, { params }) === true);
435
+ continue;
436
+ }
437
+ }
438
+ }
439
+ }
440
+ }
400
441
  remaining.push(cond);
401
442
  }
402
443
 
@@ -326,6 +326,12 @@ export class SelectExecutor {
326
326
  const joinOpts = {
327
327
  params,
328
328
  _subqueryExecutor: options._subqueryExecutor,
329
+ // TPC-H 慢查询专项:透传列裁剪/命名空间跳过/哈希表缓存标记——此前白名单式
330
+ // 重建 joinOpts 丢弃这三个标记,导致 _computeRequiredColumns 的列裁剪、
331
+ // _skipTableNamespaces 与 _hashTableCache 三项优化在 JOIN 主路径从未生效。
332
+ _requiredColumns: options._requiredColumns,
333
+ _skipTableNamespaces: options._skipTableNamespaces,
334
+ _hashTableCache: options._hashTableCache,
329
335
  ...(useParallelJoin
330
336
  ? {
331
337
  parallel: true,
@@ -36,7 +36,7 @@
36
36
  // 2026-10-27 性能评审修复:Rust 端仅实现 F64 聚合原语(sumF64/minF64/maxF64),
37
37
  // 此前绑定层检查的 sumF32Batch 等导出在 Rust 中不存在,导致 F32 路径恒走 JS。
38
38
  // 切换为 F64 原语使 Rust SIMD 聚合真正生效(2.5-5x),同时消除 Float32 累加精度风险。
39
- import { sumF64, minF64, maxF64 } from '../native/native-acceleration.js';
39
+ import { sumF64, minF64, maxF64, filterI32Eq, filterI32Lt, filterI32Gt, filterI32Le, filterI32Ge, filterF64Eq, filterF64Lt, filterF64Gt, filterF64Le, filterF64Ge } from '../native/native-acceleration.js';
40
40
 
41
41
  // === 常量定义 ============================================================
42
42
 
@@ -232,12 +232,41 @@ export class BitmapFilter {
232
232
 
233
233
  /**
234
234
  * 单列数值快速谓词(生成位图)
235
+ * v2.55.0: TypedArray 输入优先走 Rust 向量化过滤(filterI32 系 / filterF64 系原语,
236
+ * 2026-10-27 评测指出的"native 2.67x~7.55x 但零生产调用方"由此接线),返回索引
237
+ * 后转位图;Rust 不可用或非 TypedArray 回退纯 JS 循环。'!=' 无 Rust 导出,恒走 JS。
235
238
  * @param {Float64Array|Int32Array|Array} column 列数据
236
239
  * @param {string} op 操作符 '>', '<', '>=', '<=', '=', '!='
237
240
  * @param {number} value 阈值
238
241
  * @returns {Uint8Array} 位图
239
242
  */
240
243
  static numeric(column, op, value) {
244
+ const isI32 = column instanceof Int32Array;
245
+ const isF64 = column instanceof Float64Array;
246
+ if (isI32 || isF64) {
247
+ const rustFn = isI32
248
+ ? { '>': filterI32Gt, '<': filterI32Lt, '>=': filterI32Ge, '<=': filterI32Le, '=': filterI32Eq }[op]
249
+ : { '>': filterF64Gt, '<': filterF64Lt, '>=': filterF64Ge, '<=': filterF64Le, '=': filterF64Eq }[op];
250
+ if (rustFn) {
251
+ try {
252
+ const indices = rustFn(column, value);
253
+ const n = column.length;
254
+ // 选择率门槛(2026-11-06 实测):索引→位图是稀疏随机写(每 cache line 仅 1/64 有效),
255
+ // matches ≤ n/8 时全链(Rust 扫描 + 转换)约 3.6x 快于 JS 扫描;
256
+ // 高选择率时转换成本反超(50% 时合计 5.5ms vs JS 4.4ms),退回 JS 单次扫描。
257
+ if (indices.length <= n / 8) {
258
+ const bitmap = new Uint8Array(n);
259
+ for (let i = 0; i < indices.length; i++) {
260
+ bitmap[indices[i]] = 1;
261
+ }
262
+ return bitmap;
263
+ }
264
+ // matches 过多 → 落到下方 JS 扫描(已付出的 Rust 扫描最多 ~2.9ms/1M,可接受)
265
+ } catch {
266
+ // native 调用异常 → 回退 JS
267
+ }
268
+ }
269
+ }
241
270
  const n = column.length;
242
271
  const bitmap = new Uint8Array(n);
243
272
  switch (op) {
@@ -1473,8 +1473,10 @@ export function batchMaxF32(data) {
1473
1473
  export function batchCountGtI32(data, threshold) {
1474
1474
  const arr = data instanceof Int32Array ? data : new Int32Array(data);
1475
1475
  const mod = getNativeModule();
1476
- if (mod && typeof mod.countGtI32Batch === 'function') {
1477
- return mod.countGtI32Batch(arr, threshold);
1476
+ // v2.55.0 修复:Rust 导出名为 countGtI32(vec_agg.rs count_gt_i32),
1477
+ // 此前检查的 countGtI32Batch 不存在 → 永远走 JS 回退(2026-10-27 评测"绑定名失配"的最后一处)。
1478
+ if (mod && typeof mod.countGtI32 === 'function') {
1479
+ return mod.countGtI32(arr, threshold);
1478
1480
  }
1479
1481
  return jsCountGtI32(arr, threshold);
1480
1482
  }
@@ -7,7 +7,7 @@
7
7
 
8
8
  <groupId>com.purejsdb</groupId>
9
9
  <artifactId>pure-client</artifactId>
10
- <version>2.54.0</version>
10
+ <version>2.55.1</version>
11
11
  <packaging>jar</packaging>
12
12
 
13
13
  <name>pure-js-db Java SDK</name>
@@ -62,7 +62,7 @@ import java.util.NoSuchElementException;
62
62
  public class PureClient {
63
63
 
64
64
  /** SDK 版本号。 */
65
- public static final String SDK_VERSION = "2.54.0";
65
+ public static final String SDK_VERSION = "2.55.1";
66
66
 
67
67
  /** 默认请求超时(秒)。 */
68
68
  public static final long DEFAULT_TIMEOUT_SECONDS = 30;
@@ -114,7 +114,7 @@ class PureClientTest {
114
114
  @Order(1)
115
115
  @DisplayName("J1: SDK 版本号应为 1.10.0")
116
116
  void testSdkVersion() {
117
- assertEquals("2.54.0", PureClient.SDK_VERSION,
117
+ assertEquals("2.55.1", PureClient.SDK_VERSION,
118
118
  "SDK 版本应为 1.10.0");
119
119
  }
120
120
 
@@ -2,7 +2,7 @@
2
2
  "name": "pure-js-db/php-sdk",
3
3
  "description": "PHP SDK for pure-js-db — pure JavaScript database engine, via HTTP RESTful API.",
4
4
  "type": "library",
5
- "version": "2.54.0",
5
+ "version": "2.55.1",
6
6
  "keywords": ["database", "sdk", "pure-js-db", "rest", "curl"],
7
7
  "license": "MIT",
8
8
  "require": {
@@ -34,7 +34,7 @@ namespace PureJSDB\Sdk;
34
34
  class PureClient
35
35
  {
36
36
  /** @var string SDK 版本号 */
37
- public const SDK_VERSION = '2.54.0';
37
+ public const SDK_VERSION = '2.55.1';
38
38
 
39
39
  /** @var float 默认请求超时(秒) */
40
40
  private const DEFAULT_TIMEOUT = 30.0;
@@ -35,7 +35,7 @@ import urllib.parse
35
35
  from typing import Any, Dict, Iterator, List, Optional, Union
36
36
 
37
37
 
38
- __version__ = "2.54.0"
38
+ __version__ = "2.55.1"
39
39
 
40
40
 
41
41
  class PureError(Exception):
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "pure-js-db"
3
- version = "2.54.0"
3
+ version = "2.55.1"
4
4
  edition = "2021"
5
5
  rust-version = "1.75"
6
6
  description = "Rust SDK for pure-js-db — HTTP RESTful API 客户端 + HNSWPQ 向量索引管理"
@@ -29,8 +29,8 @@ grep -E 'SDK 版本:\*\*[0-9]+\.[0-9]+\.[0-9]+\*\*' README.md # 期望: SDK
29
29
 
30
30
  | 文件 | 当前值 |
31
31
  |------|--------|
32
- | `pure-js-db/package.json` | `2.54.0` |
33
- | `pure-js-db/src/index.js` `VERSION` | `'2.54.0'` |
32
+ | `pure-js-db/package.json` | `2.55.1` |
33
+ | `pure-js-db/src/index.js` `VERSION` | `'2.55.1'` |
34
34
 
35
35
  ---
36
36
 
@@ -57,7 +57,7 @@ pub use models::{
57
57
  pub use transaction::Transaction;
58
58
 
59
59
  /// SDK 版本号
60
- pub const SDK_VERSION: &str = "2.54.0";
60
+ pub const SDK_VERSION: &str = "2.55.1";
61
61
 
62
62
  /// 默认 API 前缀
63
63
  pub const DEFAULT_API_PREFIX: &str = "/api/v1";
@@ -58,6 +58,20 @@ export const batchMaxF64 = noop;
58
58
  export const sumF64 = noop;
59
59
  export const minF64 = noop;
60
60
  export const maxF64 = noop;
61
+ // Rust 向量化过滤原语(v2.55.0 列存 BitmapFilter 接线,微信 stub 同步提供 noop)
62
+ export const filterI32Eq = noop;
63
+ export const filterI32Lt = noop;
64
+ export const filterI32Gt = noop;
65
+ export const filterI32Le = noop;
66
+ export const filterI32Ge = noop;
67
+ export const filterF64Eq = noop;
68
+ export const filterF64Lt = noop;
69
+ export const filterF64Gt = noop;
70
+ export const filterF64Le = noop;
71
+ export const filterF64Ge = noop;
72
+ // Rust TopN 堆选择原语(v2.54.0 ORDER BY+LIMIT 快速路径,微信 stub 同步提供 noop)
73
+ export const topnF64Asc = noop;
74
+ export const topnF64Desc = noop;
61
75
 
62
76
  // 常见的常量导出
63
77
  export const PARALLEL_THRESHOLD = 0;
@@ -449,6 +449,13 @@ export class JoinEngine {
449
449
  if (leftKeyExtractor) {
450
450
  // 快速路径:预编译 extractor
451
451
  const maxRows = options.maxRows;
452
+ // TPC-H 慢查询专项:复合行列裁剪——按 options._requiredColumns(全 AST 列集合,
453
+ // 由 Executor.execute 顶层 SELECT 注入)按需拷贝右表新键,复合行宽度从"全部表列"
454
+ // 降到"查询实际引用列",展开成本随行宽线性下降。null = 未启用(全列拷贝)。
455
+ const required = options._requiredColumns || null;
456
+ // 无限定列查询(_skipTableNamespaces)跳过 __table__ 赋值:该命名空间的全部
457
+ // 消费点(限定列解析/_getTableRow/graph-table)在此查询形状下均不被触发。
458
+ const skipNs = options._skipTableNamespaces === true;
452
459
  for (const leftRow of leftRows) {
453
460
  const key = leftKeyExtractor(leftRow);
454
461
  if (key === null || key === undefined) {
@@ -459,14 +466,26 @@ export class JoinEngine {
459
466
  if (matches) {
460
467
  for (const rightRow of matches) {
461
468
  const combined = { ...leftRow };
462
- // v2.11 性能: for...in 替代 Object.entries(免每次 entries 数组分配)
463
- for (const k in rightRow) {
464
- if (k !== '__rowId' && !(k in leftRow)) {
465
- combined[k] = rightRow[k];
469
+ if (required) {
470
+ // 列裁剪路径:仅拷贝 required 列(flat);__table__ 引用随 spread 完整保留,
471
+ // 限定列访问(t.col)不受影响;未在 required 的列不进入复合行。
472
+ for (const k in rightRow) {
473
+ if (k !== '__rowId' && !(k in leftRow) && required.has(k)) {
474
+ combined[k] = rightRow[k];
475
+ }
476
+ }
477
+ } else {
478
+ // v2.11 性能: for...in 替代 Object.entries(免每次 entries 数组分配)
479
+ for (const k in rightRow) {
480
+ if (k !== '__rowId' && !(k in leftRow)) {
481
+ combined[k] = rightRow[k];
482
+ }
466
483
  }
467
484
  }
468
- combined[rightTableKey] = rightRow;
469
- combined[leftTableKey] = leftRow;
485
+ if (!skipNs) {
486
+ combined[rightTableKey] = rightRow;
487
+ combined[leftTableKey] = leftRow;
488
+ }
470
489
  joined.push(combined);
471
490
  // LIMIT 短路(2026-10-27 #4):仅当查询形状安全(无 ORDER BY/GROUP BY 等,
472
491
  // 由 select-executor 判定后传入 maxRows)时提前终止,避免物化全部匹配对。
package/src/version.js CHANGED
@@ -14,6 +14,6 @@
14
14
  * 并校验所有 JS 文件不再硬编码版本号。
15
15
  */
16
16
 
17
- export const VERSION = '2.54.0';
17
+ export const VERSION = '2.55.1';
18
18
 
19
19
  export const PHASE = 'v2.53.2 可靠性验证与发版演练:Raft 3 节点容器集群实测通过(Leader 选举 + 日志复制 + 宕机重选三项 PASS,raft-node-entry.js 新增 POST /raft/propose 提案端点 + raft-extended-verify.mjs 两阶段扩展验证 + run-raft-extended.sh 一键编排);可靠性周期重跑(2026-11-03)五项全 PASS(3 节点 2PC 30/30 TPS 95.85、网络分区注入 PARTITION_TEST_PASS、MVCC 同行冲突 SAME_ROW_CONFLICT_PASS、7 分钟 soak 7 cycle 0 abort 资金守恒、崩溃重启 6/6、断电模拟 8/8);npm 周下载 486 破零记录;' + 'v2.53.0 性能服务化(P0-5/P0-6 请求级查询取消与超时):执行器新增 options 级取消检查 _abortCheck(直接读 options.signal,避免共享 Executor 并发异步查询互相覆盖实例级取消状态),接线到全部分片让出检查点(分片过滤/分片 GROUP BY/_finishSelectAfterRows 阶段边界/JOIN 派发前/parallelHashJoinWithWorker 键计算让出点);executeSQLAsync 与 parallelHashJoinWithWorker 对取消错误(QUERY_ABORTED)上抛而非静默回退同步;routeSQL(sql, params, res) 为每个请求创建 AbortController——客户端断开(res 提前 close)即中止 + this._timeout 超时中止,取消在让出检查点真实生效(P0-6);query handler 主查询与 SELECT 端点传递 res;新增 query-cancel.test.js 6 项(预中止/JOIN 路径/运行中/routeSQL 断开/超时/正常回归)已注册 run-all;' + 'v2.52.0 性能服务化(P0-1 Worker 并行 JOIN + P0-3 HTTP WorkerPool):JoinEngine.parallelHashJoinWithWorker 把 Hash Join 探测(JOIN_PROBE)真实分发到 Worker 线程(主线程预计算键 + 浅拷贝行附加 __joinKey + execAll 分发 + 合并 + LEFT 补行 + 失败回退同步 _parallelHashJoin);Executor._performJoinAsync 委托 JoinExecutor 供异步路径使用;execSelectShardedAsync 支持单普通表等值 INNER/LEFT JOIN 的 Worker 并行(_canShardAsyncSelect 扩展 + WHERE 不得引用右表列的安全限制);DBServer 启动时初始化 WorkerPool(lazy,默认 CPU-1,可 enableWorkerPool=false 关闭)并在 stop 时终止,使 HTTP 服务中 JOIN/SORT/FILTER/GROUP_BY 并行算子真实可用;新增 worker-join.test.js 5 项(INNER/LEFT/无池回退/右表列回退/任务分发断言)已注册 run-all;' + 'v2.51.0 缺陷修复(物化视图注册为可查询关系 + WAL 大批次分块):物化视图作为只读关系注册到 Database(materializedViews 注册表 + hasTable/getTable 识别 + 只读表外观 + 命名冲突双向拒绝 + INFORMATION_SCHEMA.TABLES/COLUMNS 与 pg_class 可见 + CLI/.tables 与 HTTP 表列表纳入 + 持久化重建后重新注册 + SELECT/JOIN/EXPLAIN/跨库可解析),修复 CREATE MATERIALIZED VIEW 通过 SQL 调用时校验错误变为未处理 Promise 拒绝的问题(createMaterializedView 同步抛出 + execCreateMaterializedView 透传异步刷新);事务 WAL 按 maxEntriesPerBatch 分块,消除同步突发写入 >65535 条时 serializeBatch 的 RangeError 崩溃路径(守卫保留供防御测试);物化视图测试 +4 项(注册/冲突/只读/持久化),wal-count-bound 7/7 全绿;' + 'v2.50.0 T-05/T-06 架构清理:优化器 barrel 补齐 5 个生产可达类导出(ExplainVisualizer/JoinOrderOptimizer/QueryPlanCache/Histogram·HistogramBucket/TimestampQueryRewriter,经 engine/public-api.js 接主入口)+ StorageEngine 统一存储引擎接口(STORAGE_ENGINE_METHODS 契约 + isStorageEngine/assertStorageEngine 校验 + STORAGE_BACKENDS 四后端工厂 memory/file/binary/buffered + StorageEngine.switchBackend 运行时切换 + createStorageEngine)+ T-06 lockfree-worker.js 占位桩整体移除(lockfree-manager 移除 _startWorkers/useWorkers,单线程 COW 模式收敛);storage-engine.test 16 项(含绕过)+ optimizer-exports.test 4 项(含主入口接线)已注册 run-all,全量 806/806 全绿、幻影审计 A 类 0/B 类 0、dist 22/22 含新符号;' + 'v2.49.0 E34 数据血缘收尾(UPDATE 源追踪 + CREATE VIEW 事前环拒绝 + Mermaid 导出):extractUpdateColumnLineage(UPDATE SET 列级血缘,直接列/表达式/函数,常量不产生依赖,多表 UPDATE...FROM 无前缀 unresolved)+ recordStatement UpdateStatement 分支;wouldCreateCycle + DDLExecutor._assertNoLineageCycle(CREATE VIEW/CREATE MATERIALIZED VIEW 事前环拒绝,仅目标视图已存在时预检,保留 v2.11 E15 M2 T3 前向引用后补行为);toMermaid/getLineageMermaid(Mermaid flowchart 导出);生产入口导出 + lineage-e34 19 项 + 生产接线 4 项,run-all 804/804 全绿;' + 'v2.48.0 数据血缘全链路落地(P7 M1-M4 + P8 深化 + P9 版本历史):表级/列级血缘自动记录(CREATE VIEW/MATERIALIZED VIEW/INSERT...SELECT)、LINEAGE SQL 语句(LINEAGE TABLE/COLUMN + lineage-executor + MCP 工具)、任务级登记(CopyStream/Migrator)、持久化(meta v5 随 PersistedDatabase 落盘)、环检测 detectCycles、CTE 列级推导(collectCtes)、HTML 可视化 toHTML、版本历史(getLineageHistory/getLineageAtTime/getLineageEvolution)、变更影响分析(analyzeImpact),生产入口全导出 + 常驻测试 70 项;run-all 全量通过 + vitest 123/123 + c8 Lines 91.99%/Branches 88.43%;' + 'v2.45.2 双缺陷修复(多轮门禁盘点发现):① REGEXP (?g)/(?y) 全局/粘性标志剥离——编译产物跨行 LRU 缓存复用而 g/y 的 lastIndex 状态使求值行依赖((?g)a 四行 [1,0,1,0]),REGEXP 逐行独立求值语义下 g/y 无意义且有害,编译期剥离(i/m/s/u 保持);② WASM min/max 对齐区 NaN 传播——f32x4.min/max 按 IEEE 语义传播 NaN 与"NaN 被忽略"文档语义相悖(v2.45.1 激活 SIMD 路径后成为实际行为),NaN 结果 ⟺ 对齐区有 NaN lane,JS 重扫跳过 NaN 与降级路径对齐;绕过测试 v2450 +2(29 项)、wasm-simd-engine E3 守卫(23 项);' + 'v2.45.1 WASM SIMD 常量错位修复 + 覆盖率债务清零:f32x4_max 误写 0xEA(实为 f32x4.pmin)致 init 运行时验证恒败、引擎自引入即静默降级(SIMD 路径从未激活,全部流量走 TypedArray fallback)——修正 0xE9 并经验解码验证(0xE7=div/0xE8=min/0xE9=max/0xEA=pmin/0xEB=pmax),修复后 READY/WASM_SIMD 五算子精确正确;wasm-simd-engine 移出 c8 exclude 并补全路径测试(初始化/SIMD 数值/降级/扩容/单例 + 修复守卫);lockfree-worker.js 人工评估归档(确认占位桩:主路径不经 Worker,保持 exclude 豁免);' + 'v2.45.0 安满分诊 P1/P2 治理落地(eslint-security 58 errors 分诊第四节立项 ①②③④ 全部修复):REGEXP 用户正则防护(security/regex-guard.js compileUserRegex:模式长度上限 1024 + 嵌套量词灾难回溯编译期筛查,bitmap-filter _getRegex 接线)+ LIKE/GLOB/KEYS→regex 转换入口统一模式长度上限 1024(bitmap-filter _likeToRegex/_buildEscapeRegex、btree/offheap 部分索引 LikeExpr、cache-adapter KEYS、sqlite-kv-adapter LIKE、expression.js GLOB 共 7 处)+ oracle-adapter DECODE 线性解析重写(消除指数回溯,修复多分支 CASE 语义 + 括号/引号感知 + 嵌套 DECODE 递归转换)+ 输入长度上限(sql-injection-detector 256KB fail-closed、schema-versioning DDL 256KB + 顶层逗号线性切分);绕过测试 v2450-p0-bypass-boundary 通过;v2.0 长期路线图收官:v2.0-1..6 全部落地(JSONB 二进制 JSON / Lambda 表达式 x->x+1 / UNNEST 多列并行 / DEFERRABLE 约束 / EXCLUDE 排他约束 / LISTEN-NOTIFY 异步通知,DoD 5/5 各 27-39 用例);v2.0-7 WAL flush 慢 seq 初始化 flaky 修复 + wal-crash-validation 测试加固(10/10 稳定 + 绕过测试有效性验证)+ flush-timeout-simulation FT2/FT4 libuv 时钟边界修复;全量回归 run-all 459/459 全绿(历史首次);v2.0-8 SQL:2016 阶段1 聚合补全(PERCENTILE_CONT/DISC + STRING_AGG/ARRAY_AGG + MODE + BOOL_AND/OR/EVERY + WIDTH_BUCKET + REGR_*,DoD 5/5 37/37);v2.0-9 SQL:2016 阶段2 MATCH_RECOGNIZE 行模式识别(Part 2 §21.6 新执行器,DoD 5/5 30/30);v2.0-10 SQL:2016 阶段3 多态表函数 PTF(Part 2 §10.9:ANY TABLE/COLUMN/MAX(n) + RETURNS TABLE + 函数体 AST 重写 + 物化临时表 + JOIN 内调用 + 幂等性/零副作用,DoD 5/5 25/25,run-all 461/462 + audit-log A8 时间竞态 flaky 修复);v2.0-11 SQL:2016 阶段4 RATIO_TO_REPORT 分析函数(Part 2 §9.6 G9:分区求和比例 + NULL/除零语义 + 参数校验,DoD 5/5 14/14)+ 窗口函数源表零副作用 P0 修复(window-functions.js 浅拷贝行,156 项窗口回归全绿);v2.0-12 SQL:2016 阶段5 类型系统深化(Part 2 §4.4 G11 INTERVAL 完整类型:YEAR/MONTH/DAY/HOUR/MINUTE/SECOND 字段子句 + 单位内嵌 + 纯时间/天数自动判断 + DAY TO MINUTE HH:MM + DAY TO HOUR/MINUTE/SECOND 时间部分列宽 + date/timestamp±interval 日历月 clamp + 算术/比较/CAST/一元运算;G12 TIMESTAMP WITH TIME ZONE:带偏移解析→UTC 内部存储 + 会话时区显示 + SET TIME ZONE/SHOW TIME ZONE + INSERT/UPDATE 归一化 + 混合比较,DoD 5/5 62/62);v1.42.x 基础:STRUCT 类型 / LIST-ARRAY 完整操作 / WITHOUT ROWID 表 / VACUUM-REINDEX / user_version-application_id 持久化 + v1.42.1 全量回归闭环(16 个预存失败清零 + 2 个源码回归修复:query-isolation 原型链 / select-executor ORDER BY 聚合别名);v1.41.x 基础:ASOF JOIN / ATTACH DATABASE / COMMENT ON / PIVOT 多聚合 / WITH CHECK OPTION;v1.40.4 基础:PRAGMA 完整集(25+ SQLite 标准)+ 版本号多 SDK 单一来源同步;v1.40.3 基础:3 个 P0 安全修复真实现 + SECURITY DEFINER 真实现 + 循环依赖 0;v2.11(E15)遗留清零:WAL 恢复数据版本一致性 + 视图 lossy 根治 + 多视图 CASCADED 链 + SECURITY DEFINER 真实现 + RedisCacheAdapter 真实现 + Arrow 有限宽度 guard + WAL count 上界 + executor 拆分(sql-reconstructor);v2.12(E16)稳定性与分布式闭环:flaky 三件套根治(2pc-perf/index-build/soak-test + vector-executor 门槛统一)+ 跨库 DDL/事务 + WAL hot-standby 探针 + 分布式真实网络验证(raft/3PC/2PC + PK fastpath WAL 缺陷修复 + 2PC rejection 修复)+ executor PRAGMA 族拆离(pragma-executor,-550 行)+ 全量回归 612/612 + DL-D3 时序 flaky 修复;v2.13(E17)收尾治理与 SQL/XML:版本号全量对齐 + SQL:2016 阶段6 G10(XMLSERIALIZE/XMLAGG/XMLTABLE)+ 全量 c8 复验 + 跨库 TPC-H 数据刷新 + SQL:2023 roadmap 收敛;v2.14(E18)列存文件格式 M1+M2:PJDC 二进制列文件格式(表签名三层校验:schemaHash + rowCount + columnCount)+ save/load 持久化加速缓存 + 列存 roundtrip 持久化验证(22+8 用例全绿);v2.15(E19)列存文件格式 M3:WAL _invalidateTableIndexes 修复(checkpoint 后索引失效重建)+ run-all 618/618 全绿;v2.15(E20)工程化基线清零:parquet.js 3 处 setUint32 guard 补零 + 超限绕过测试 + 版本号同步 2.13.0→2.15.0 + c8 覆盖率实测 + 审计章节归档;';
@@ -1,68 +0,0 @@
1
- import purejsdb.PureJsDBClient;
2
- import purejsdb.TransactionClient;
3
-
4
- public class TestClient {
5
- public static void main(String[] args) throws Exception {
6
- testBasic();
7
- System.out.println();
8
- testTransaction();
9
- }
10
-
11
- static void testBasic() throws Exception {
12
- PureJsDBClient client = new PureJsDBClient();
13
- System.out.println("[Java SDK Test]");
14
-
15
- String result = client.query("SELECT 1 AS test");
16
- assert result != null && result.contains("data") : "query should return data";
17
- System.out.println(" ✅ query: SELECT 1");
18
-
19
- String tables = client.getTables();
20
- assert tables != null : "getTables should return data";
21
- System.out.println(" ✅ getTables");
22
-
23
- String status = client.getStatus();
24
- assert status != null : "getStatus should return data";
25
- System.out.println(" ✅ getStatus");
26
-
27
- System.out.println("\nAll Java SDK tests passed!");
28
- }
29
-
30
- static void testTransaction() throws Exception {
31
- TransactionClient client = new TransactionClient();
32
- System.out.println("[Java Transaction SDK Test]");
33
-
34
- String txResult = client.begin();
35
- assert txResult != null : "begin should return data";
36
- System.out.println(" ✅ begin");
37
-
38
- // Extract transaction id from response (simplified)
39
- String txId = extractTxId(txResult);
40
-
41
- String execResult = client.executeInTx("SELECT 1 AS tx_test", txId);
42
- assert execResult != null : "executeInTx should return data";
43
- System.out.println(" ✅ executeInTx");
44
-
45
- String commitResult = client.commit(txId);
46
- assert commitResult != null : "commit should return data";
47
- System.out.println(" ✅ commit");
48
-
49
- System.out.println("\nAll Java Transaction SDK tests passed!");
50
- }
51
-
52
- private static String extractTxId(String response) {
53
- // Simple extraction - looks for "sessionId" or "transactionId" in JSON
54
- if (response.contains("\"sessionId\"")) {
55
- int start = response.indexOf("\"sessionId\"") + 12;
56
- start = response.indexOf('"', start) + 1;
57
- int end = response.indexOf('"', start);
58
- return response.substring(start, end);
59
- }
60
- if (response.contains("\"transactionId\"")) {
61
- int start = response.indexOf("\"transactionId\"") + 16;
62
- start = response.indexOf('"', start) + 1;
63
- int end = response.indexOf('"', start);
64
- return response.substring(start, end);
65
- }
66
- return "unknown";
67
- }
68
- }
@@ -1,63 +0,0 @@
1
- """Python SDK 测试"""
2
- from purejsdb.client import PureJsDBClient, TransactionClient
3
- import sys
4
-
5
-
6
- def test_basic():
7
- client = PureJsDBClient()
8
- print('[Python SDK Test]')
9
-
10
- # 测试 get_status
11
- status = client.get_status()
12
- assert status is not None, 'get_status should return data'
13
- print(f' ✅ get_status: {status.get("status", "OK")}')
14
-
15
- # 测试 get_tables
16
- tables = client.get_tables()
17
- assert tables is not None, 'get_tables should return data'
18
- print(f' ✅ get_tables')
19
-
20
- # 测试 query
21
- result = client.query('SELECT 1 AS test')
22
- assert result is not None, 'query should return data'
23
- print(f' ✅ query: SELECT 1')
24
-
25
- # 测试 health
26
- health = client.health()
27
- assert health is not None, 'health should return data'
28
- print(f' ✅ health')
29
-
30
- print('\nAll Python SDK tests passed!')
31
-
32
-
33
- def test_transaction():
34
- """测试事务客户端(需要服务器支持事务)"""
35
- client = TransactionClient()
36
- print('[Python Transaction SDK Test]')
37
-
38
- # 开始事务
39
- tx = client.begin()
40
- assert tx is not None, 'begin should return data'
41
- tx_id = tx.get('sessionId') or tx.get('transactionId')
42
- print(f' ✅ begin: {tx_id}')
43
-
44
- # 在事务中执行
45
- result = client.execute_in_tx('SELECT 1 AS tx_test', tx_id)
46
- assert result is not None, 'execute_in_tx should return data'
47
- print(f' ✅ execute_in_tx')
48
-
49
- # 提交事务
50
- result = client.commit(tx_id)
51
- assert result is not None, 'commit should return data'
52
- print(f' ✅ commit')
53
-
54
- print('\nAll Python Transaction SDK tests passed!')
55
-
56
-
57
- if __name__ == '__main__':
58
- test_basic()
59
- print()
60
- try:
61
- test_transaction()
62
- except Exception as e:
63
- print(f' ⚠️ Transaction test skipped (server may not support transactions): {e}')