jsql-neo 6.4.3 → 6.4.4
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 +1 -1
- package/index.js +1 -1
- package/lib/sql.js +122 -21
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
> **One engine to rule them all** — a Rust-powered embedded database that speaks your language:
|
|
4
4
|
> MySQL, PostgreSQL, MongoDB, Redis, SQL, TypeScript, **and the browser**.
|
|
5
5
|
|
|
6
|
-
> **v6.4.
|
|
6
|
+
> **v6.4.4** — official release build · [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
|

|
package/index.js
CHANGED
package/lib/sql.js
CHANGED
|
@@ -2813,6 +2813,40 @@ function rowHasPrefixedKeys(row) {
|
|
|
2813
2813
|
return false;
|
|
2814
2814
|
}
|
|
2815
2815
|
|
|
2816
|
+
/**
|
|
2817
|
+
* 整棵语句树里有没有出现过「表.列」形式的限定引用。
|
|
2818
|
+
*
|
|
2819
|
+
* executeSelect 会给每一行调用 _prefixRow,把键复制成「表.列」形式。
|
|
2820
|
+
* 单表查询里这些键根本没被引用,10 万行就是 10 万个白造对象
|
|
2821
|
+
* (node --prof 显示占整个查询 30% 的 GC)。
|
|
2822
|
+
*
|
|
2823
|
+
* 但限定引用不一定长在 {type:/column/} 节点里 —— ORDER BY 的 o.column
|
|
2824
|
+
* 就是纯字符串 "t.age",HAVING / pushdown 的键也是。只扫对象节点会漏,
|
|
2825
|
+
* 漏了就少加前缀,相关子查询和限定列全部解析错。两种形态都要查。
|
|
2826
|
+
*
|
|
2827
|
+
* 消费方清单(摸清这些才敢跳过前缀):
|
|
2828
|
+
* 1. resolveOperand 的 column 分支 —— 限定引用解析
|
|
2829
|
+
* 2. rowHasPrefixedKeys —— 未知限定符的防自匹配守卫
|
|
2830
|
+
* 3. SELECT * 的 starKeys —— 单表时取 Object.keys(schema),与行前缀无关
|
|
2831
|
+
* 4. JOIN 右表 —— 多表同名列消歧,本来就强制前缀
|
|
2832
|
+
*/
|
|
2833
|
+
const QUALIFIED_REF_RE = /^[A-Za-z_][A-Za-z0-9_]*\.[A-Za-z_][A-Za-z0-9_]*$/;
|
|
2834
|
+
function hasQualifiedRef(node, depth) {
|
|
2835
|
+
if (!node || typeof node !== "object" || depth > 200) return false;
|
|
2836
|
+
if (Array.isArray(node)) {
|
|
2837
|
+
for (const n of node) if (hasQualifiedRef(n, depth + 1)) return true;
|
|
2838
|
+
return false;
|
|
2839
|
+
}
|
|
2840
|
+
if (node.type === "column" && typeof node.name === "string" && node.name.indexOf(".") !== -1) return true;
|
|
2841
|
+
for (const k in node) {
|
|
2842
|
+
if (k === "ctx" || k === "engine" || k === "session") continue;
|
|
2843
|
+
const v = node[k];
|
|
2844
|
+
if (typeof v === "string") { if (QUALIFIED_REF_RE.test(v)) return true; }
|
|
2845
|
+
else if (v && typeof v === "object" && hasQualifiedRef(v, depth + 1)) return true;
|
|
2846
|
+
}
|
|
2847
|
+
return false;
|
|
2848
|
+
}
|
|
2849
|
+
|
|
2816
2850
|
/**
|
|
2817
2851
|
* 字符串能否当作数值参与算术运算。
|
|
2818
2852
|
* MySQL 里 '5' + 5 = 10、'5' + '5' = 10、'abc' + 1 = 1(非数字按 0)。
|
|
@@ -6843,10 +6877,25 @@ class SQLExecutor {
|
|
|
6843
6877
|
return (a && String(a).includes('.') ? String(a) : `${firstPrefix}.${a}`);
|
|
6844
6878
|
}).filter(Boolean)
|
|
6845
6879
|
: null;
|
|
6880
|
+
/*
|
|
6881
|
+
* 曾经试着在「单表 + 无 JOIN」时跳过 _prefixRow 以省掉 10 万次对象分配
|
|
6882
|
+
* (node --prof 显示它占 30% GC)。实测确实快 2~3 倍,但相关子查询
|
|
6883
|
+
* (EXISTS / 标量 / IN)靠 `外层表.列` 引用外层行,没前缀就解析不到,
|
|
6884
|
+
* 而 _collectSubqueries 在这个位置抓不到它们(前面已被改写)。
|
|
6885
|
+
* 收益真实但边界没摸清,先回退,留待摸清后再上。
|
|
6886
|
+
*/
|
|
6887
|
+
/*
|
|
6888
|
+
* 单表 + 无 JOIN + 无派生表 + 整棵树没有限定引用时,跳过前缀。
|
|
6889
|
+
* 不缓存到 statement 上:语句对象可能被 prepare() 复用,挂属性会串味。
|
|
6890
|
+
*/
|
|
6891
|
+
const needPrefix = isVirtualSource
|
|
6892
|
+
|| !!(statement.from.joins && statement.from.joins.length)
|
|
6893
|
+
|| hasQualifiedRef(statement, 0);
|
|
6846
6894
|
let rows = isVirtualSource
|
|
6847
6895
|
? rowsAll.rows.map(r => this._prefixRow(r, firstPrefix))
|
|
6848
|
-
: (firstItem._virtualRows
|
|
6849
|
-
|
|
6896
|
+
: (needPrefix || firstItem._virtualRows
|
|
6897
|
+
? rowsAll.rows.map(r => this._prefixRow(r, firstPrefix))
|
|
6898
|
+
: rowsAll.rows);
|
|
6850
6899
|
for (const j of statement.from.joins) {
|
|
6851
6900
|
// 虚拟行(递归 CTE 物化的派生表)要直接取行,不能当普通子查询执行
|
|
6852
6901
|
const jVirtual = j.item._virtualRows ||
|
|
@@ -7132,28 +7181,54 @@ class SQLExecutor {
|
|
|
7132
7181
|
if (statement.orderBy) {
|
|
7133
7182
|
// ORDER BY 项可能是列名字符串、表达式节点,或列的位置序号(ORDER BY 1,1-based)
|
|
7134
7183
|
const ordNames = statement.columns.map(c => scalarColumnName(c));
|
|
7135
|
-
|
|
7184
|
+
/*
|
|
7185
|
+
* 把排序键的解析**预先编译**,比较器里只做数组下标访问。
|
|
7186
|
+
*
|
|
7187
|
+
* 此前每次比较都调 `resolveOperand({type:'column', name}, row, ctx)` ——
|
|
7188
|
+
* 那个对象字面量是每次新建的。6.8 万行排序约 230 万次比较,
|
|
7189
|
+
* 就是 230 万次白白分配,实测占掉整个 ORDER BY 的大半时间
|
|
7190
|
+
* (10 万行 WHERE+ORDER+LIMIT 从 133ms 量级大幅下降)。
|
|
7191
|
+
*
|
|
7192
|
+
* 同时按「投影列名 → 下标」解析;命中就直接 `row[idx]`,
|
|
7193
|
+
* 不再走 resolveOperand 的通用路径。
|
|
7194
|
+
*/
|
|
7195
|
+
const ordKeys = statement.orderBy.map(o => {
|
|
7196
|
+
const dir = o.dir === 'asc' ? 1 : -1;
|
|
7197
|
+
// 位置序号:ORDER BY 1
|
|
7136
7198
|
const pos = (o.column && typeof o.column === 'object' && o.column.type === 'value' && typeof o.column.value === 'number')
|
|
7137
7199
|
? o.column.value
|
|
7138
7200
|
: (typeof o.column === 'number' ? o.column : null);
|
|
7139
7201
|
if (pos !== null && pos !== undefined) {
|
|
7140
7202
|
const name = ordNames[pos - 1];
|
|
7141
|
-
|
|
7203
|
+
return { dir, at: (name === undefined || name === null) ? () => null : (r) => (r == null ? null : r[name]) };
|
|
7142
7204
|
}
|
|
7143
|
-
|
|
7144
|
-
|
|
7145
|
-
:
|
|
7146
|
-
|
|
7205
|
+
if (typeof o.column === 'string') {
|
|
7206
|
+
const f = o.column;
|
|
7207
|
+
// 节点对象提到循环外复用 —— 此前每次比较都新建一个 {type:'column',name}
|
|
7208
|
+
const node = { type: 'column', name: f };
|
|
7209
|
+
return {
|
|
7210
|
+
dir,
|
|
7211
|
+
at: (r) => {
|
|
7212
|
+
// 先按字段名直取;取不到再走 resolveOperand(点号路径、表达式等)
|
|
7213
|
+
const v = (r == null) ? undefined : r[f];
|
|
7214
|
+
return v !== undefined ? v : resolveOperand(node, r, this.ctx);
|
|
7215
|
+
},
|
|
7216
|
+
};
|
|
7217
|
+
}
|
|
7218
|
+
return { dir, at: (r) => resolveOperand(o.column, r, this.ctx) };
|
|
7219
|
+
});
|
|
7220
|
+
|
|
7147
7221
|
const cmp = (a, b) => {
|
|
7148
|
-
for (
|
|
7149
|
-
const
|
|
7150
|
-
const
|
|
7222
|
+
for (let i = 0; i < ordKeys.length; i++) {
|
|
7223
|
+
const k = ordKeys[i];
|
|
7224
|
+
const av = k.at(a);
|
|
7225
|
+
const bv = k.at(b);
|
|
7151
7226
|
if (av === bv || (av === undefined && bv === undefined)) continue;
|
|
7152
|
-
if (av === undefined || av === null) return
|
|
7153
|
-
if (bv === undefined || bv === null) return
|
|
7227
|
+
if (av === undefined || av === null) return -1 * k.dir;
|
|
7228
|
+
if (bv === undefined || bv === null) return 1 * k.dir;
|
|
7154
7229
|
// P2-1: localeCompare 对字符串排序受 locale 影响,应使用简单字符串比较
|
|
7155
7230
|
const r = typeof av === 'number' && typeof bv === 'number' ? av - bv : (String(av) < String(bv) ? -1 : String(av) > String(bv) ? 1 : 0);
|
|
7156
|
-
if (r !== 0) return
|
|
7231
|
+
if (r !== 0) return r * k.dir;
|
|
7157
7232
|
}
|
|
7158
7233
|
return 0;
|
|
7159
7234
|
};
|
|
@@ -7252,16 +7327,42 @@ class SQLExecutor {
|
|
|
7252
7327
|
if (c.expr === '*') { for (const k of starKeys) cols.push(k.includes('.') ? k.slice(k.indexOf('.') + 1) : k); }
|
|
7253
7328
|
else cols.push(scalarColumnName(c));
|
|
7254
7329
|
}
|
|
7330
|
+
/*
|
|
7331
|
+
* 预先编译每列的取值函数。
|
|
7332
|
+
*
|
|
7333
|
+
* 此前循环里每次调用都新建 `{type:'column', name}` 对象字面量,
|
|
7334
|
+
* 10 万行 × N 列就是 10 万 × N 次白分配 —— 实测光 `SELECT id FROM t`
|
|
7335
|
+
* 就要 94ms,其中绝大部分是这个。
|
|
7336
|
+
*
|
|
7337
|
+
* 最常见的「普通列名」直接走属性访问,不再过 resolveOperand 的通用路径;
|
|
7338
|
+
* 取不到值才退回(兼容点号路径、限定引用等)。
|
|
7339
|
+
*/
|
|
7340
|
+
const starIdx = [];
|
|
7341
|
+
const colGetters = statement.columns.map((c, ci) => {
|
|
7342
|
+
if (c.expr === '*') { starIdx.push(ci); return null; }
|
|
7343
|
+
if (c.over || (c.scalar && c.scalar.over)) return (r) => windowColumnValue(c, r);
|
|
7344
|
+
if (c.scalar) return (r) => projectScalar(resolveOperand(c.scalar, r, this.ctx));
|
|
7345
|
+
if (c.literal !== undefined) return () => c.literal;
|
|
7346
|
+
if (c.caseExpr) return (r) => projectScalar(evaluateCaseVal(c.caseExpr, r));
|
|
7347
|
+
if (typeof c.expr === 'string') {
|
|
7348
|
+
const f = c.expr;
|
|
7349
|
+
const node = { type: 'column', name: f };
|
|
7350
|
+
return (r) => {
|
|
7351
|
+
const v = (r == null) ? undefined : r[f];
|
|
7352
|
+
return v === undefined ? projectScalar(resolveOperand(node, r, this.ctx)) : projectScalar(v);
|
|
7353
|
+
};
|
|
7354
|
+
}
|
|
7355
|
+
const node2 = { type: 'column', name: c.expr };
|
|
7356
|
+
return (r) => projectScalar(resolveOperand(node2, r, this.ctx));
|
|
7357
|
+
});
|
|
7358
|
+
|
|
7255
7359
|
const mapped = await this._projectRows(statement, rows, (r) => {
|
|
7256
7360
|
const out = [];
|
|
7257
|
-
for (
|
|
7258
|
-
|
|
7361
|
+
for (let ci = 0; ci < colGetters.length; ci++) {
|
|
7362
|
+
const g = colGetters[ci];
|
|
7363
|
+
if (g === null) {
|
|
7259
7364
|
for (const k of starKeys) out.push(r[k] !== undefined ? r[k] : r[String(k).slice(String(k).indexOf('.') + 1)]);
|
|
7260
|
-
} else
|
|
7261
|
-
else if (c.scalar) out.push(projectScalar(resolveOperand(c.scalar, r, this.ctx)));
|
|
7262
|
-
else if (c.literal !== undefined) out.push(c.literal);
|
|
7263
|
-
else if (c.caseExpr) out.push(projectScalar(evaluateCaseVal(c.caseExpr, r)));
|
|
7264
|
-
else out.push(projectScalar(resolveOperand({ type: 'column', name: c.expr }, r, this.ctx)));
|
|
7365
|
+
} else out.push(g(r));
|
|
7265
7366
|
}
|
|
7266
7367
|
return out;
|
|
7267
7368
|
});
|