jsql-neo 6.5.11 → 6.5.13
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 +39 -1
- package/index.d.ts +16 -0
- package/index.js +1 -1
- package/lib/database.js +8 -1
- package/lib/plugins/index.js +5 -1
- package/lib/plugins/xql.js +163 -0
- package/lib/xql/NOTICE +16 -0
- package/lib/xql/index.js +0 -0
- package/lib/xql/src/compiler.js +497 -0
- package/lib/xql/src/index-store.js +186 -0
- package/lib/xql/src/jsql.js +46 -0
- package/lib/xql/src/parser.js +287 -0
- package/lib/xql/src/tokenizer.js +129 -0
- package/package.json +3 -2
- package/src/cli/index.js +12 -0
- package/src/cli/plugin.js +346 -0
- package/test/plugin-xql.test.js +170 -0
|
@@ -0,0 +1,497 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/**
|
|
3
|
+
* XQL 编译器:AST → 单个 JS 函数
|
|
4
|
+
*
|
|
5
|
+
* ## 演进过程(三次被打脸)
|
|
6
|
+
*
|
|
7
|
+
* 1. **闭包组合**:每个 AST 节点返回一个函数,互相调用。
|
|
8
|
+
* `WHERE age > 35` 每行调 3 次函数 → 比手写慢 6.4 倍。
|
|
9
|
+
*
|
|
10
|
+
* 2. **生成表达式**:把 AST 编成 JS 源码字符串,但直接拼表达式。
|
|
11
|
+
* `L == null || R == null ? null : L > R` 里 L 出现三次,
|
|
12
|
+
* `r["age"]` 每行求值三遍 → OR 查询慢 7.2 倍。
|
|
13
|
+
*
|
|
14
|
+
* 3. **生成语句 + 临时变量**:每个子表达式求值一次存入 `_vN`。
|
|
15
|
+
* 谓词本身从 7.3 倍降到 3.2 倍,但仍有天花板。
|
|
16
|
+
*
|
|
17
|
+
* 4. **现在:整条管道生成一个函数,WHERE 内联进循环。**
|
|
18
|
+
*
|
|
19
|
+
* 第 3 版遗留的 3.2 倍,隔离后发现不在生成码里,而在
|
|
20
|
+
* **`Array.prototype.filter` 的回调中介**:
|
|
21
|
+
* 同一个谓词,手动内联循环 0.180ms(比手写还快)
|
|
22
|
+
* 同一个谓词,过 filter() 0.543ms(慢 3 倍)
|
|
23
|
+
*
|
|
24
|
+
* 所以正解不是把谓词写得更漂亮,而是**根本不回调** ——
|
|
25
|
+
* WHERE 的判定语句直接生成在 for 循环体内。
|
|
26
|
+
*
|
|
27
|
+
* ## NULL 语义(与 SQL 三值逻辑一致)
|
|
28
|
+
* - 算术/比较任一操作数为 NULL → NULL
|
|
29
|
+
* - WHERE 里 NULL 一律当假(UNKNOWN 不匹配)
|
|
30
|
+
* - IS NULL / IS NOT NULL / COALESCE 显式处理 NULL
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
const { pickIndexableConjunct } = require('./index-store');
|
|
34
|
+
|
|
35
|
+
/** 生成注入给生成码的候选查找函数 */
|
|
36
|
+
function makeCandFinder(indexes) {
|
|
37
|
+
return function _ixcand(field, op, value) {
|
|
38
|
+
const ix = indexes.get(field);
|
|
39
|
+
if (!ix) return null;
|
|
40
|
+
return ix.candidates(op, value);
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** LIKE → RegExp(编译一次,运行期只 test) */
|
|
45
|
+
function likeToRegExp(pat) {
|
|
46
|
+
const esc = String(pat).replace(/[.*+?^${}()|[\]\\]/g, '\\$&').replace(/%/g, '.*').replace(/_/g, '.');
|
|
47
|
+
return new RegExp('^' + esc + '$', 'i');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** 标量函数:编译期内联 */
|
|
51
|
+
function emitCall(fn, a) {
|
|
52
|
+
switch (fn) {
|
|
53
|
+
case 'upper': return `String(${a[0]}).toUpperCase()`;
|
|
54
|
+
case 'lower': return `String(${a[0]}).toLowerCase()`;
|
|
55
|
+
case 'length': return `String(${a[0]}).length`;
|
|
56
|
+
case 'trim': return `String(${a[0]}).trim()`;
|
|
57
|
+
case 'abs': return `Math.abs(${a[0]})`;
|
|
58
|
+
case 'floor': return `Math.floor(${a[0]})`;
|
|
59
|
+
case 'ceil': return `Math.ceil(${a[0]})`;
|
|
60
|
+
case 'sqrt': return `Math.sqrt(${a[0]})`;
|
|
61
|
+
case 'round': {
|
|
62
|
+
const d = a[1] || '0';
|
|
63
|
+
return `((${d}) === 0 ? Math.round(${a[0]}) : Math.round((${a[0]}) * 10 ** (${d})) / 10 ** (${d}))`;
|
|
64
|
+
}
|
|
65
|
+
case 'coalesce': return a.map(x => `${x} != null ? ${x} : `).join('') + 'null';
|
|
66
|
+
case 'concat': return a.map(x => `String(${x})`).join(' + ');
|
|
67
|
+
case 'substr': case 'substring':
|
|
68
|
+
return `String(${a[0]}).substring((${a[1]}) - 1${a[2] ? `, (${a[1]}) - 1 + (${a[2]})` : ''})`;
|
|
69
|
+
default: return null;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const KNOWN_FNS = new Set(['upper', 'lower', 'length', 'trim', 'abs', 'floor', 'ceil',
|
|
74
|
+
'sqrt', 'round', 'coalesce', 'concat', 'substr', 'substring']);
|
|
75
|
+
|
|
76
|
+
/** 这些函数本身就是用来处理 NULL 的,不做 NULL 传播 */
|
|
77
|
+
const NULL_TOLERANT_FNS = new Set(['coalesce']);
|
|
78
|
+
|
|
79
|
+
/** 语句发射器:每个子表达式求值一次并存入临时变量 */
|
|
80
|
+
class Emitter {
|
|
81
|
+
constructor() {
|
|
82
|
+
this.body = [];
|
|
83
|
+
this.regexes = [];
|
|
84
|
+
this.sets = [];
|
|
85
|
+
this._n = 0;
|
|
86
|
+
this._usesLike = false;
|
|
87
|
+
}
|
|
88
|
+
tmp() { return '_v' + (this._n++); }
|
|
89
|
+
push(s) { this.body.push(s); }
|
|
90
|
+
|
|
91
|
+
/** 发射「值」表达式,返回存放结果的变量名(或内联常量) */
|
|
92
|
+
emit(node) {
|
|
93
|
+
switch (node.k) {
|
|
94
|
+
case 'lit': {
|
|
95
|
+
if (node.v === null) return 'null';
|
|
96
|
+
if (typeof node.v === 'number' || typeof node.v === 'boolean') return String(node.v);
|
|
97
|
+
return JSON.stringify(node.v);
|
|
98
|
+
}
|
|
99
|
+
case 'path': return this.emitPath(node);
|
|
100
|
+
case 'idx': {
|
|
101
|
+
const o = this.emit(node.obj);
|
|
102
|
+
const i = this.emit(node.idx);
|
|
103
|
+
const t = this.tmp();
|
|
104
|
+
this.push(`let ${t} = (${o} == null ? null : ${o}[Number(${i})]);`);
|
|
105
|
+
return t;
|
|
106
|
+
}
|
|
107
|
+
case 'neg': {
|
|
108
|
+
const e = this.emit(node.e);
|
|
109
|
+
const t = this.tmp();
|
|
110
|
+
this.push(`let ${t} = (${e} == null ? null : -(${e}));`);
|
|
111
|
+
return t;
|
|
112
|
+
}
|
|
113
|
+
case 'bin': return this.emitBin(node);
|
|
114
|
+
case 'log': {
|
|
115
|
+
const l = this.emitBool(node.l);
|
|
116
|
+
const r = this.emitBool(node.r);
|
|
117
|
+
const t = this.tmp();
|
|
118
|
+
this.push(`let ${t} = ${l} ${node.op === 'and' ? '&&' : '||'} ${r};`);
|
|
119
|
+
return t;
|
|
120
|
+
}
|
|
121
|
+
case 'not': {
|
|
122
|
+
const e = this.emitBool(node.e);
|
|
123
|
+
const t = this.tmp();
|
|
124
|
+
this.push(`let ${t} = !${e};`);
|
|
125
|
+
return t;
|
|
126
|
+
}
|
|
127
|
+
case 'isnull': {
|
|
128
|
+
const e = this.emit(node.e);
|
|
129
|
+
const t = this.tmp();
|
|
130
|
+
this.push(`let ${t} = ${node.neg ? `(${e} !== null && ${e} !== undefined)` : `(${e} === null || ${e} === undefined)`};`);
|
|
131
|
+
return t;
|
|
132
|
+
}
|
|
133
|
+
case 'in': return this.emitIn(node);
|
|
134
|
+
case 'between': {
|
|
135
|
+
const v = this.emit(node.e);
|
|
136
|
+
const lo = this.emit(node.lo);
|
|
137
|
+
const hi = this.emit(node.hi);
|
|
138
|
+
const t = this.tmp();
|
|
139
|
+
this.push(`let ${t} = (${v} == null ? null : (${v} >= ${lo} && ${v} <= ${hi}));`);
|
|
140
|
+
return t;
|
|
141
|
+
}
|
|
142
|
+
case 'like': {
|
|
143
|
+
const v = this.emit(node.e);
|
|
144
|
+
const t = this.tmp();
|
|
145
|
+
if (node.p.k === 'lit') {
|
|
146
|
+
const name = '_re' + this.regexes.length;
|
|
147
|
+
this.regexes.push({ name, re: likeToRegExp(node.p.v) });
|
|
148
|
+
this.push(`let ${t} = (typeof ${v} !== 'string' ? null : ${name}.test(${v}));`);
|
|
149
|
+
} else {
|
|
150
|
+
const p = this.emit(node.p);
|
|
151
|
+
this._usesLike = true;
|
|
152
|
+
this.push(`let ${t} = (typeof ${v} !== 'string' ? null : _like(${p}).test(${v}));`);
|
|
153
|
+
}
|
|
154
|
+
return t;
|
|
155
|
+
}
|
|
156
|
+
case 'call': {
|
|
157
|
+
const args = node.args.map(a => this.emit(a));
|
|
158
|
+
const inner = emitCall(node.fn, args);
|
|
159
|
+
if (inner === null) throw new Error(`XQL: 未知函数 ${node.fn}()`);
|
|
160
|
+
const t = this.tmp();
|
|
161
|
+
// NULL 传播:任一参数为 NULL → NULL。
|
|
162
|
+
// 例外:COALESCE / IFNULL 天生就是用来吃 NULL 的,不能传播。
|
|
163
|
+
if (NULL_TOLERANT_FNS.has(node.fn)) {
|
|
164
|
+
this.push(`let ${t} = (${inner});`);
|
|
165
|
+
} else {
|
|
166
|
+
this.push(`let ${t} = (${args.map(a => `${a} == null`).join(' || ')} ? null : (${inner}));`);
|
|
167
|
+
}
|
|
168
|
+
return t;
|
|
169
|
+
}
|
|
170
|
+
case 'agg': throw new Error(`XQL: 聚合函数 ${node.fn} 只能出现在 SELECT / HAVING 中`);
|
|
171
|
+
case 'star': return 'undefined';
|
|
172
|
+
default: throw new Error(`XQL: 无法编译的节点 '${node.k}'`);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
emitBin(node) {
|
|
177
|
+
const l = this.emit(node.l);
|
|
178
|
+
const r = this.emit(node.r);
|
|
179
|
+
const t = this.tmp();
|
|
180
|
+
switch (node.op) {
|
|
181
|
+
case '+': this.push(`let ${t} = (${l} == null || ${r} == null ? null : ${l} + ${r});`); break;
|
|
182
|
+
case '-': this.push(`let ${t} = (${l} == null || ${r} == null ? null : ${l} - ${r});`); break;
|
|
183
|
+
case '*': this.push(`let ${t} = (${l} == null || ${r} == null ? null : ${l} * ${r});`); break;
|
|
184
|
+
case '/': this.push(`let ${t} = (${l} == null || ${r} == null || Number(${r}) === 0 ? null : ${l} / ${r});`); break;
|
|
185
|
+
case '%': this.push(`let ${t} = (${l} == null || ${r} == null || Number(${r}) === 0 ? null : ${l} % ${r});`); break;
|
|
186
|
+
case '=': this.push(`let ${t} = (${l} == null ? null : ${l} === ${r});`); break;
|
|
187
|
+
case '!=': this.push(`let ${t} = (${l} == null ? null : ${l} !== ${r});`); break;
|
|
188
|
+
case '<': this.push(`let ${t} = (${l} == null || ${r} == null ? null : ${l} < ${r});`); break;
|
|
189
|
+
case '>': this.push(`let ${t} = (${l} == null || ${r} == null ? null : ${l} > ${r});`); break;
|
|
190
|
+
case '<=': this.push(`let ${t} = (${l} == null || ${r} == null ? null : ${l} <= ${r});`); break;
|
|
191
|
+
case '>=': this.push(`let ${t} = (${l} == null || ${r} == null ? null : ${l} >= ${r});`); break;
|
|
192
|
+
default: throw new Error(`XQL: 未知二元操作符 '${node.op}'`);
|
|
193
|
+
}
|
|
194
|
+
return t;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
emitIn(node) {
|
|
198
|
+
const v = this.emit(node.e);
|
|
199
|
+
const t = this.tmp();
|
|
200
|
+
const not = node.neg ? '!' : '';
|
|
201
|
+
const join = node.neg ? ' && ' : ' || ';
|
|
202
|
+
if (node.list.every(n => n.k === 'lit')) {
|
|
203
|
+
const name = '_set' + this.sets.length;
|
|
204
|
+
this.sets.push({ name, set: new Set(node.list.map(n => n.v)) });
|
|
205
|
+
this.push(`let ${t} = (${v} == null ? null : ${not}${name}.has(${v}));`);
|
|
206
|
+
} else {
|
|
207
|
+
const parts = node.list.map(n => `(${v} != null && ${v} === ${this.emit(n)})`);
|
|
208
|
+
this.push(`let ${t} = (${v} == null ? null : ${not}(${parts.join(join)}));`);
|
|
209
|
+
}
|
|
210
|
+
return t;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* 点号路径。**必须存临时变量**:父节点会把操作数用两遍,
|
|
215
|
+
* 内联会让 `r["age"]` 每行求值两次(第二版就是这么栽的)。
|
|
216
|
+
*/
|
|
217
|
+
emitPath(node) {
|
|
218
|
+
let expr = node.base ? this.emit(node.base) : 'r';
|
|
219
|
+
for (const seg of node.segs) {
|
|
220
|
+
const t = this.tmp();
|
|
221
|
+
this.push(`let ${t} = (${expr} == null ? undefined : ${expr}[${JSON.stringify(seg)}]);`);
|
|
222
|
+
expr = t;
|
|
223
|
+
}
|
|
224
|
+
return expr;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/** 发射「布尔」表达式,返回变量名;NULL 一律当假 */
|
|
228
|
+
emitBool(node) {
|
|
229
|
+
switch (node.k) {
|
|
230
|
+
case 'lit': return `${JSON.stringify(node.v)} === true`;
|
|
231
|
+
case 'log': {
|
|
232
|
+
const l = this.emitBool(node.l);
|
|
233
|
+
const r = this.emitBool(node.r);
|
|
234
|
+
const t = this.tmp();
|
|
235
|
+
this.push(`let ${t} = ${l} ${node.op === 'and' ? '&&' : '||'} ${r};`);
|
|
236
|
+
return t;
|
|
237
|
+
}
|
|
238
|
+
case 'not': {
|
|
239
|
+
// NOT 保留三值逻辑:NOT UNKNOWN = UNKNOWN(WHERE 里当假),不是 TRUE。
|
|
240
|
+
// true → false / false → true / null → false
|
|
241
|
+
// 走 emit(值路径)而不是 emitBool —— 后者已经把 NULL 压成 false,
|
|
242
|
+
// 那样就分不清「原来是 FALSE」和「原来是 UNKNOWN」。
|
|
243
|
+
const e = this.emit(node.e);
|
|
244
|
+
const t = this.tmp();
|
|
245
|
+
this.push(`let ${t} = (${e} === true ? false : ${e} === false);`);
|
|
246
|
+
return t;
|
|
247
|
+
}
|
|
248
|
+
case 'isnull': {
|
|
249
|
+
const e = this.emit(node.e);
|
|
250
|
+
const t = this.tmp();
|
|
251
|
+
this.push(`let ${t} = ${node.neg ? `(${e} !== null && ${e} !== undefined)` : `(${e} === null || ${e} === undefined)`};`);
|
|
252
|
+
return t;
|
|
253
|
+
}
|
|
254
|
+
case 'bin': {
|
|
255
|
+
// 比较:NULL → false,直接产布尔(不走「三值再 === true」,省一条 let)
|
|
256
|
+
if (['=', '!=', '<', '>', '<=', '>='].includes(node.op)) {
|
|
257
|
+
const l = this.emit(node.l);
|
|
258
|
+
const r = this.emit(node.r);
|
|
259
|
+
const t = this.tmp();
|
|
260
|
+
const op = node.op === '=' ? '===' : node.op;
|
|
261
|
+
this.push(`let ${t} = ${l} != null && ${r} != null && ${l} ${op} ${r};`);
|
|
262
|
+
return t;
|
|
263
|
+
}
|
|
264
|
+
const v = this.emit(node);
|
|
265
|
+
const t = this.tmp();
|
|
266
|
+
this.push(`let ${t} = ${v} === true;`);
|
|
267
|
+
return t;
|
|
268
|
+
}
|
|
269
|
+
case 'in': {
|
|
270
|
+
// NOT IN 的三值逻辑比较绕:
|
|
271
|
+
// 匹配上了 → FALSE
|
|
272
|
+
// 没匹配上 → 列表含 NULL 时 UNKNOWN(WHERE 里当假),否则 TRUE
|
|
273
|
+
// 被查找值为 NULL → UNKNOWN
|
|
274
|
+
if (node.neg) {
|
|
275
|
+
const v = this.emit(node.e);
|
|
276
|
+
const hasNullInList = node.list.some(n => n.k === 'lit' && n.v === null);
|
|
277
|
+
if (node.list.every(n => n.k === 'lit')) {
|
|
278
|
+
const name = '_set' + this.sets.length;
|
|
279
|
+
this.sets.push({ name, set: new Set(node.list.map(n => n.v)) });
|
|
280
|
+
const t = this.tmp();
|
|
281
|
+
this.push(`let ${t} = (${v} != null && !${name}.has(${v})${hasNullInList ? ' && false' : ''});`);
|
|
282
|
+
return t;
|
|
283
|
+
}
|
|
284
|
+
const parts = node.list.map(n => `(${v} != null && ${v} === ${this.emit(n)})`);
|
|
285
|
+
const t = this.tmp();
|
|
286
|
+
this.push(`let ${t} = (${v} != null && !(${parts.join(' || ')})${hasNullInList ? ' && false' : ''});`);
|
|
287
|
+
return t;
|
|
288
|
+
}
|
|
289
|
+
const v = this.emit(node);
|
|
290
|
+
const t = this.tmp();
|
|
291
|
+
this.push(`let ${t} = ${v} === true;`);
|
|
292
|
+
return t;
|
|
293
|
+
}
|
|
294
|
+
default: {
|
|
295
|
+
const v = this.emit(node);
|
|
296
|
+
const t = this.tmp();
|
|
297
|
+
this.push(`let ${t} = ${v} === true;`);
|
|
298
|
+
return t;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/** 收尾:语句缓冲 + 返回值 → 函数 */
|
|
304
|
+
build(param, resultVar, extraParams = []) {
|
|
305
|
+
const names = [];
|
|
306
|
+
const vals = [];
|
|
307
|
+
for (const r of this.regexes) { names.push(r.name); vals.push(r.re); }
|
|
308
|
+
for (const s of this.sets) { names.push(s.name); vals.push(s.set); }
|
|
309
|
+
if (this._usesLike) { names.push('_like'); vals.push(likeToRegExp); }
|
|
310
|
+
const src = `"use strict";\n${this.body.map(s => ' ' + s).join('\n')}\n return ${resultVar};`;
|
|
311
|
+
// eslint-disable-next-line no-new-func
|
|
312
|
+
const factory = new Function(...names, `return function (${[param, ...extraParams].join(', ')}) {\n${src}\n};`);
|
|
313
|
+
return factory(...vals);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/** 没写别名时的默认列名 */
|
|
318
|
+
function defaultAlias(expr) {
|
|
319
|
+
switch (expr.k) {
|
|
320
|
+
case 'path': return expr.segs.join('.');
|
|
321
|
+
case 'lit': return String(expr.v);
|
|
322
|
+
case 'call': return expr.fn;
|
|
323
|
+
case 'bin': return `${defaultAlias(expr.l)}${expr.op}${defaultAlias(expr.r)}`;
|
|
324
|
+
case 'idx': return `${defaultAlias(expr.obj)}[${expr.idx.k === 'lit' ? expr.idx.v : ''}]`;
|
|
325
|
+
default: return 'expr';
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const CMP_OPS = new Set(['=', '!=', '<', '>', '<=', '>=']);
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* 编译整条查询 → (rows) => resultRows
|
|
333
|
+
*
|
|
334
|
+
* **整条管道生成一个函数**:WHERE 的判定语句直接内联在 for 循环体内,
|
|
335
|
+
* 不经过 Array.filter 的回调(实测那一道中介要付 3 倍)。
|
|
336
|
+
* SELECT 的每列投影、ORDER BY 的每个排序键是独立编译的小函数,
|
|
337
|
+
* 作为参数注入主管道 —— 它们不在最热路径上,换来的是可读性。
|
|
338
|
+
*/
|
|
339
|
+
function compileQuery(q, opts = {}) {
|
|
340
|
+
const copy = !!opts.copy;
|
|
341
|
+
// ---- SELECT:每列编译成一个投影函数 ----
|
|
342
|
+
let project = null;
|
|
343
|
+
if (q.select && !q.select.some(s => s.expr.k === 'star')) {
|
|
344
|
+
// 只记录「别名 + 表达式」,编译留到主管道里内联完成
|
|
345
|
+
project = q.select.map(s => ({
|
|
346
|
+
expr: s.expr,
|
|
347
|
+
alias: s.alias || defaultAlias(s.expr),
|
|
348
|
+
}));
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// ---- ORDER BY:每个排序键编译成一个取值函数 ----
|
|
352
|
+
const orderBy = q.orderBy
|
|
353
|
+
? q.orderBy.map(o => {
|
|
354
|
+
const e = new Emitter();
|
|
355
|
+
const v = e.emit(o.expr);
|
|
356
|
+
return { fn: e.build('r', v), dir: o.dir === 'desc' ? -1 : 1 };
|
|
357
|
+
})
|
|
358
|
+
: null;
|
|
359
|
+
|
|
360
|
+
// ---- 主管道 ----
|
|
361
|
+
const e = new Emitter();
|
|
362
|
+
const B = e.body;
|
|
363
|
+
|
|
364
|
+
// 1) WHERE —— 内联进循环,零回调
|
|
365
|
+
//
|
|
366
|
+
// 有索引时走「候选 + 谓词确认」:索引只负责缩小要看的行,
|
|
367
|
+
// 最终判定仍然由完整谓词做 —— 所以索引给多给少都不会算错。
|
|
368
|
+
const cand = opts.indexes ? pickIndexableConjunct(q.where) : null;
|
|
369
|
+
let candField = null;
|
|
370
|
+
if (cand && opts.indexes.has(cand.field)) {
|
|
371
|
+
const ix = opts.indexes.get(cand.field);
|
|
372
|
+
// 索引类型要匹配操作符:等值用 hash,范围用 sorted
|
|
373
|
+
const fits = cand.op === '=' || cand.op === '!='
|
|
374
|
+
? ix.type === 'hash' || ix.type === 'range'
|
|
375
|
+
: ix.type === 'range';
|
|
376
|
+
if (fits) candField = cand;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
B.push(' let _out = [];');
|
|
380
|
+
const allEmitters = [e];
|
|
381
|
+
|
|
382
|
+
if (candField) {
|
|
383
|
+
// 索引路径与全扫路径各编译一份谓词 —— temp 变量名不能跨分支复用
|
|
384
|
+
const eIdx = new Emitter();
|
|
385
|
+
const predIdx = eIdx.emitBool(q.where);
|
|
386
|
+
const eScan = new Emitter();
|
|
387
|
+
const predScan = eScan.emitBool(q.where);
|
|
388
|
+
allEmitters.push(eIdx, eScan);
|
|
389
|
+
|
|
390
|
+
B.push(` const _cand = _ixcand(${JSON.stringify(candField.field)}, ${JSON.stringify(candField.op)}, ${JSON.stringify(candField.value)});`);
|
|
391
|
+
B.push(' if (_cand !== null) {');
|
|
392
|
+
B.push(' for (let _i = 0; _i < _cand.length; _i++) {');
|
|
393
|
+
B.push(' const r = rows[_cand[_i]];');
|
|
394
|
+
B.push(...eIdx.body);
|
|
395
|
+
B.push(` if (${predIdx}) _out.push(r);`);
|
|
396
|
+
B.push(' }');
|
|
397
|
+
B.push(' } else {');
|
|
398
|
+
B.push(' for (let _i = 0; _i < rows.length; _i++) {');
|
|
399
|
+
B.push(' const r = rows[_i];');
|
|
400
|
+
B.push(...eScan.body);
|
|
401
|
+
B.push(` if (${predScan}) _out.push(r);`);
|
|
402
|
+
B.push(' }');
|
|
403
|
+
B.push(' }');
|
|
404
|
+
} else if (q.where) {
|
|
405
|
+
B.push(' for (let _i = 0; _i < rows.length; _i++) {');
|
|
406
|
+
B.push(' const r = rows[_i];');
|
|
407
|
+
const predVar = e.emitBool(q.where);
|
|
408
|
+
B.push(` if (${predVar}) _out.push(r);`);
|
|
409
|
+
B.push(' }');
|
|
410
|
+
} else {
|
|
411
|
+
B.push(' for (let _i = 0; _i < rows.length; _i++) _out.push(rows[_i]);');
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// 2) SELECT —— 与 WHERE 同理,投影语句也内联进循环。
|
|
415
|
+
// 每列调一次函数的话,6.8 万行 × 2 列就是 13.6 万次调用,
|
|
416
|
+
// 实测占掉整个管道的大半时间(2.81 倍差距主要来自这里)。
|
|
417
|
+
if (project) {
|
|
418
|
+
B.push(' const _res = new Array(_out.length);');
|
|
419
|
+
B.push(' for (let _j = 0; _j < _out.length; _j++) {');
|
|
420
|
+
B.push(' const r = _out[_j];');
|
|
421
|
+
B.push(' const o = {};');
|
|
422
|
+
for (const p of project) {
|
|
423
|
+
const v = e.emit(p.expr);
|
|
424
|
+
B.push(` o[${JSON.stringify(p.alias)}] = ${v};`);
|
|
425
|
+
}
|
|
426
|
+
B.push(' _res[_j] = o;');
|
|
427
|
+
B.push(' }');
|
|
428
|
+
B.push(' _out = _res;');
|
|
429
|
+
} else {
|
|
430
|
+
// 无投影。默认**不拷贝**,直接返回内部行的引用 ——
|
|
431
|
+
// 实测:命中 7.2 万行时逐行展开对象要占掉整个查询的大半时间
|
|
432
|
+
// (OR 查询 5.7 倍差距主要来自这里)。需要防调用方改内部数据的话,
|
|
433
|
+
// 用 xql(sql, { copy: true }) 显式打开。
|
|
434
|
+
if (copy) {
|
|
435
|
+
B.push(' const _cp = new Array(_out.length);');
|
|
436
|
+
B.push(' for (let _k = 0; _k < _out.length; _k++) _cp[_k] = { ..._out[_k] };');
|
|
437
|
+
B.push(' _out = _cp;');
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
const limit = q.limit;
|
|
442
|
+
const offset = q.offset || 0;
|
|
443
|
+
|
|
444
|
+
// 3) ORDER BY
|
|
445
|
+
if (orderBy) {
|
|
446
|
+
// 每个排序键一个块。注意不能用 continue —— 块级作用域里没有循环可继续,
|
|
447
|
+
// 改成 if 包裹。
|
|
448
|
+
//
|
|
449
|
+
// 字符串用 < / > 而不是 localeCompare:后者是 locale 感知的排序,
|
|
450
|
+
// 6.8 万行排序要调 ~110 万次,实测比朴素比较多一个量级
|
|
451
|
+
// (这一项就是从 2.93 倍降到持平的关键)。
|
|
452
|
+
const cmp = orderBy.map((o, i) => ` { const av = _ok${i}(a), bv = _ok${i}(b);
|
|
453
|
+
if (av !== bv) {
|
|
454
|
+
if (av === null || av === undefined) return -1 * ${o.dir};
|
|
455
|
+
if (bv === null || bv === undefined) return 1 * ${o.dir};
|
|
456
|
+
const d = (typeof av === 'number' && typeof bv === 'number') || (typeof av === 'boolean' && typeof bv === 'boolean')
|
|
457
|
+
? (av < bv ? -1 : 1)
|
|
458
|
+
: (String(av) < String(bv) ? -1 : 1);
|
|
459
|
+
if (d !== 0) return d * ${o.dir};
|
|
460
|
+
} }`).join('\n');
|
|
461
|
+
B.push(' _out.sort((a, b) => {\n' + cmp + '\n return 0;\n });');
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// 4) OFFSET / LIMIT
|
|
465
|
+
if (offset || (limit !== null && limit !== undefined)) {
|
|
466
|
+
const end = (limit === null || limit === undefined) ? 'undefined' : String(offset + limit);
|
|
467
|
+
B.push(` _out = _out.slice(${offset}, ${end});`);
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// ---- 组装 ----
|
|
471
|
+
const injectNames = [];
|
|
472
|
+
const injectVals = [];
|
|
473
|
+
if (orderBy) for (let i = 0; i < orderBy.length; i++) { injectNames.push('_ok' + i); injectVals.push(orderBy[i].fn); }
|
|
474
|
+
|
|
475
|
+
// 索引候选查找:返回候选行下标数组;返回 null 表示「这条查询用不上索引」,
|
|
476
|
+
// 由生成码里那条 else 分支走全表扫。
|
|
477
|
+
if (candField) {
|
|
478
|
+
injectNames.push('_ixcand');
|
|
479
|
+
injectVals.push(makeCandFinder(opts.indexes));
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// 正则 / Set 收集 —— 索引路径与全扫路径的 emitter 也要算进来
|
|
483
|
+
for (const em of allEmitters) {
|
|
484
|
+
for (const r of em.regexes) { injectNames.push(r.name); injectVals.push(r.re); }
|
|
485
|
+
for (const s of em.sets) { injectNames.push(s.name); injectVals.push(s.set); }
|
|
486
|
+
if (em._usesLike) { injectNames.push('_like'); injectVals.push(likeToRegExp); }
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
const src = `"use strict";\n${e.body.join('\n')}\n return _out;`;
|
|
490
|
+
// eslint-disable-next-line no-new-func
|
|
491
|
+
const factory = new Function('rows', ...injectNames, src);
|
|
492
|
+
const run = (...args) => factory(args[0], ...injectVals, ...args.slice(1));
|
|
493
|
+
run.__source = q.source;
|
|
494
|
+
return run;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
module.exports = { compileQuery, likeToRegExp, defaultAlias, KNOWN_FNS, Emitter };
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/**
|
|
3
|
+
* XQL 索引
|
|
4
|
+
*
|
|
5
|
+
* 全表扫是「比手写更快」的终点 —— 0.91x 已经贴着理论下限了。
|
|
6
|
+
* 要再快只能**不看那些行**,这就是索引的作用。
|
|
7
|
+
*
|
|
8
|
+
* 两种:
|
|
9
|
+
* HashIndex —— 等值查找,O(1)
|
|
10
|
+
* RangeIndex —— 范围 / 有序,O(log n + k)
|
|
11
|
+
*
|
|
12
|
+
* 关键约定:**索引只负责给候选,最终判定仍由 WHERE 谓词做**。
|
|
13
|
+
* 所以索引可以是「粗」的(多给候选、少给候选由谓词兜底),
|
|
14
|
+
* 绝不会因为索引有偏差而算错结果。
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/** 归一化键:让 1 和 '1'、true 和 'true' 能对上,同时保证可稳定排序 */
|
|
18
|
+
function keyOf(v) {
|
|
19
|
+
if (v === null || v === undefined) return v === null ? '\u0000null' : undefined;
|
|
20
|
+
const t = typeof v;
|
|
21
|
+
if (t === 'number' || t === 'string' || t === 'boolean') {
|
|
22
|
+
return t[0] + ':' + String(v); // 带类型前缀,避免 1 与 '1' 混淆
|
|
23
|
+
}
|
|
24
|
+
return undefined; // 对象/数组不建索引
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function unkey(k) {
|
|
28
|
+
if (k === undefined) return undefined;
|
|
29
|
+
if (k === '\u0000null') return null;
|
|
30
|
+
return k.slice(2);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
class HashIndex {
|
|
34
|
+
constructor(field) {
|
|
35
|
+
this.field = field;
|
|
36
|
+
this.type = 'hash';
|
|
37
|
+
this.map = new Map();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
build(rows) {
|
|
41
|
+
this.map.clear();
|
|
42
|
+
for (let i = 0; i < rows.length; i++) {
|
|
43
|
+
const k = keyOf(rows[i][this.field]);
|
|
44
|
+
if (k === undefined) continue;
|
|
45
|
+
let a = this.map.get(k);
|
|
46
|
+
if (!a) { a = []; this.map.set(k, a); }
|
|
47
|
+
a.push(i);
|
|
48
|
+
}
|
|
49
|
+
this._builtLen = rows.length;
|
|
50
|
+
return this;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** 返回候选行下标;没有索引值时返回 null(调用方退回全扫) */
|
|
54
|
+
candidates(op, value) {
|
|
55
|
+
if (op !== '=' && op !== '!=') return null;
|
|
56
|
+
const a = this.map.get(keyOf(value));
|
|
57
|
+
if (a) return a;
|
|
58
|
+
// `!=` 时索引帮不上:候选应是「不在这些下标里的」,逻辑交给谓词
|
|
59
|
+
return op === '=' ? [] : null;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
class RangeIndex {
|
|
64
|
+
constructor(field) {
|
|
65
|
+
this.field = field;
|
|
66
|
+
this.type = 'range';
|
|
67
|
+
this.vals = []; // 原始值,升序(直接按它比,不用字符串键)
|
|
68
|
+
this.idx = []; // 对应的行下标
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
build(rows) {
|
|
72
|
+
const pairs = [];
|
|
73
|
+
for (let i = 0; i < rows.length; i++) {
|
|
74
|
+
const v = rows[i][this.field];
|
|
75
|
+
const k = keyOf(v);
|
|
76
|
+
if (k === undefined) continue;
|
|
77
|
+
pairs.push([v, i]);
|
|
78
|
+
}
|
|
79
|
+
// **必须按原始值排序**:字符串键 "number:5" > "number:48"(字典序),
|
|
80
|
+
// 会把 48 排到 5 后面,范围查询直接给出错误结果。
|
|
81
|
+
pairs.sort(cmpValue);
|
|
82
|
+
this.vals = pairs.map(p => p[0]);
|
|
83
|
+
this.idx = pairs.map(p => p[1]);
|
|
84
|
+
this._builtLen = rows.length;
|
|
85
|
+
return this;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** 二分找第一个 >= target 的位置 */
|
|
89
|
+
lowerBound(target) {
|
|
90
|
+
const v = this.vals;
|
|
91
|
+
let lo = 0, hi = v.length;
|
|
92
|
+
while (lo < hi) {
|
|
93
|
+
const mid = (lo + hi) >> 1;
|
|
94
|
+
if (cmpScalar(v[mid], target) < 0) lo = mid + 1; else hi = mid;
|
|
95
|
+
}
|
|
96
|
+
return lo;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* 返回候选行下标(**按原行号升序**);用不上索引时返回 null。
|
|
101
|
+
*
|
|
102
|
+
* 命中面太大时不走索引 —— 切片的分配 + 二次遍历比一次扫更贵。
|
|
103
|
+
*/
|
|
104
|
+
candidates(op, value) {
|
|
105
|
+
if (keyOf(value) === undefined) return null;
|
|
106
|
+
const n = this.vals.length;
|
|
107
|
+
if (!n) return [];
|
|
108
|
+
let start = 0, end = n;
|
|
109
|
+
if (op === '=') { start = this.lowerBound(value); end = start; while (end < n && cmpScalar(this.vals[end], value) === 0) end++; }
|
|
110
|
+
else if (op === '>') { start = this.lowerBound(value); while (start < n && cmpScalar(this.vals[start], value) === 0) start++; }
|
|
111
|
+
else if (op === '>=') { start = this.lowerBound(value); }
|
|
112
|
+
else if (op === '<') { end = this.lowerBound(value); }
|
|
113
|
+
else if (op === '<=') {
|
|
114
|
+
// 等于 value 的元素在 lowerBound **之后**(它找的是第一个 >= value),
|
|
115
|
+
// 所以要向前扩。此前写成向后扫,`age <= 21` 只会返回 age=20 那一半。
|
|
116
|
+
end = this.lowerBound(value);
|
|
117
|
+
while (end < n && cmpScalar(this.vals[end], value) === 0) end++;
|
|
118
|
+
}
|
|
119
|
+
else return null;
|
|
120
|
+
// 候选超过全表 25% 就不如直接扫:切片的分配 + 二次遍历比一次扫更贵
|
|
121
|
+
if (end - start > n * 0.25) return null;
|
|
122
|
+
const out = this.idx.slice(start, end);
|
|
123
|
+
// 索引是按字段值排的,全扫是按原行序 —— 不还原的话同一查询
|
|
124
|
+
// 「有索引」和「无索引」会给出不同的行序(SQL 里无 ORDER BY 本无序,
|
|
125
|
+
// 但这里必须一致,否则测试和调用方的 diff 都会莫名其妙地变)。
|
|
126
|
+
out.sort((a, b) => a - b);
|
|
127
|
+
return out;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* 无分配的值比较。
|
|
133
|
+
*
|
|
134
|
+
* 之前用 cmpValue([x,0],[y,0]) —— **每次比较都新建两个数组**,
|
|
135
|
+
* 2000 个候选的等值扫描就是 4000 次分配,实测占掉整个查询的一半时间。
|
|
136
|
+
*/
|
|
137
|
+
function cmpScalar(x, y) {
|
|
138
|
+
if (x === y) return 0;
|
|
139
|
+
const xn = x === null || x === undefined;
|
|
140
|
+
const yn = y === null || y === undefined;
|
|
141
|
+
if (xn || yn) return xn ? (yn ? 0 : -1) : 1;
|
|
142
|
+
const tx = typeof x, ty = typeof y;
|
|
143
|
+
if (tx === 'number' && ty === 'number') return x < y ? -1 : 1;
|
|
144
|
+
if (tx === 'boolean' && ty === 'boolean') return x ? 1 : -1;
|
|
145
|
+
const sx = String(x), sy = String(y);
|
|
146
|
+
return sx === sy ? 0 : (sx < sy ? -1 : 1);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** 带平局裁决的比较器(排序时保证稳定且与 cmpScalar 同序) */
|
|
150
|
+
function cmpValue(a, b) {
|
|
151
|
+
const c = cmpScalar(a[0], b[0]);
|
|
152
|
+
return c !== 0 ? c : a[1] - b[1];
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function createIndex(field) {
|
|
156
|
+
return field === '' ? new RangeIndex(field) : new HashIndex(field);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* 从 WHERE 里挑一个能用索引的合取项。
|
|
161
|
+
* 只看最外层的 AND 链:`(a AND b)` 里的 a/b 算合取;OR / NOT 不碰。
|
|
162
|
+
* @returns {{field, op, value}|null}
|
|
163
|
+
*/
|
|
164
|
+
function pickIndexableConjunct(where) {
|
|
165
|
+
if (!where) return null;
|
|
166
|
+
const conjs = [];
|
|
167
|
+
(function walk(n) {
|
|
168
|
+
if (!n) return;
|
|
169
|
+
if (n.k === 'log' && n.op === 'and') { walk(n.l); walk(n.r); return; }
|
|
170
|
+
conjs.push(n);
|
|
171
|
+
})(where);
|
|
172
|
+
|
|
173
|
+
for (const c of conjs) {
|
|
174
|
+
if (c.k !== 'bin') continue;
|
|
175
|
+
if (!['=', '!=', '>', '>=', '<', '<='].includes(c.op)) continue;
|
|
176
|
+
// field 必须是裸路径(无点号嵌套),value 必须是常量
|
|
177
|
+
const isPlainPath = c.l && c.l.k === 'path' && !c.l.base && c.l.segs.length === 1;
|
|
178
|
+
const isConst = c.r && c.r.k === 'lit';
|
|
179
|
+
if (isPlainPath && isConst) {
|
|
180
|
+
return { field: c.l.segs[0], op: c.op, value: c.r.v };
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
module.exports = { HashIndex, RangeIndex, createIndex, pickIndexableConjunct, keyOf, unkey };
|