jsql-neo 6.5.9 → 6.5.10
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 +197 -30
- 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.5.
|
|
6
|
+
> **v6.5.10** — stable release · [github.com/vexify-org/JSQL-neo](https://github.com/vexify-org/JSQL-neo) · [jsql.vexify.top](https://jsql.vexify.top)
|
|
7
7
|
|
|
8
8
|

|
|
9
9
|

|
package/index.js
CHANGED
package/lib/sql.js
CHANGED
|
@@ -1079,6 +1079,22 @@ class Parser {
|
|
|
1079
1079
|
}
|
|
1080
1080
|
|
|
1081
1081
|
parseValueAtom() {
|
|
1082
|
+
// MySQL 前缀运算符:BINARY expr —— 强制大小写敏感比较。
|
|
1083
|
+
// 仅当后面确实是值起始 token 才当作运算符,避免误吞裸列名 `binary`
|
|
1084
|
+
// (`SELECT binary FROM t` / `WHERE binary = 1` 仍按列名解析)。
|
|
1085
|
+
if (this.isWord('BINARY')) {
|
|
1086
|
+
const nt = this.peek(1);
|
|
1087
|
+
const startVal = nt && (nt.type === 'number' || nt.type === 'string' || nt.type === 'ident' || nt.type === 'param' ||
|
|
1088
|
+
(nt.type === 'op' && (nt.value === '(' || nt.value === '-' || nt.value === '+')) ||
|
|
1089
|
+
(nt.type === 'keyword' && (nt.value === 'NULL' || nt.value === 'TRUE' || nt.value === 'FALSE' || nt.value === 'DEFAULT')));
|
|
1090
|
+
if (startVal) {
|
|
1091
|
+
this.next(); // 消费 BINARY
|
|
1092
|
+
const lt = this.peek();
|
|
1093
|
+
if (lt.type === 'string' || lt.type === 'number') { const lit = this.next(); return { type: 'binary', operand: { type: 'literal', value: lit.value } }; }
|
|
1094
|
+
const operand = this.parseValue();
|
|
1095
|
+
return { type: 'binary', operand };
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1082
1098
|
const t = this.next();
|
|
1083
1099
|
if (t.type === 'number' || t.type === 'string') return t.value;
|
|
1084
1100
|
// MySQL: ON DUPLICATE KEY UPDATE col = VALUES(col) —— 引用"本次本应插入的值"
|
|
@@ -1583,9 +1599,30 @@ class Parser {
|
|
|
1583
1599
|
let where = null;
|
|
1584
1600
|
if (this.isKeyword('WHERE')) { this.next(); where = this.parseExpr(); }
|
|
1585
1601
|
else console.warn(`⚠️ UPDATE without WHERE clause — this will modify ALL rows in '${table}'`);
|
|
1602
|
+
// UPDATE ... ORDER BY ... LIMIT n(与 DELETE 对称;按排序后的前 n 行更新)
|
|
1603
|
+
let orderBy = null, limit = null;
|
|
1604
|
+
if (this.isKeyword('ORDER')) {
|
|
1605
|
+
this.next(); this.expectKeyword('BY');
|
|
1606
|
+
orderBy = [];
|
|
1607
|
+
for (;;) {
|
|
1608
|
+
const col = this.parseColumnRef();
|
|
1609
|
+
let dir = 'asc';
|
|
1610
|
+
if (this.isKeyword('ASC')) this.next();
|
|
1611
|
+
else if (this.isKeyword('DESC')) { this.next(); dir = 'desc'; }
|
|
1612
|
+
orderBy.push({ column: col, dir });
|
|
1613
|
+
if (this.peek().type === 'op' && this.peek().value === ',') { this.next(); continue; }
|
|
1614
|
+
break;
|
|
1615
|
+
}
|
|
1616
|
+
}
|
|
1617
|
+
if (this.isKeyword('LIMIT')) {
|
|
1618
|
+
this.next();
|
|
1619
|
+
limit = this.parseValue();
|
|
1620
|
+
if (typeof limit === 'bigint') limit = Number(limit <= BigInt(Number.MAX_SAFE_INTEGER) ? limit : BigInt(Number.MAX_SAFE_INTEGER));
|
|
1621
|
+
if (typeof limit === 'number' && limit < 0) throw new Error(`LIMIT must be a non-negative integer, got ${limit}`);
|
|
1622
|
+
}
|
|
1586
1623
|
const returning = this.parseOptionalReturning();
|
|
1587
1624
|
this.optionalTailSemicolon();
|
|
1588
|
-
return { type: 'update', table, assignments, where, returning };
|
|
1625
|
+
return { type: 'update', table, assignments, where, orderBy, limit, returning };
|
|
1589
1626
|
}
|
|
1590
1627
|
|
|
1591
1628
|
parseDelete() {
|
|
@@ -2191,6 +2228,37 @@ class Parser {
|
|
|
2191
2228
|
}
|
|
2192
2229
|
|
|
2193
2230
|
parseOperand() {
|
|
2231
|
+
// MySQL 前缀运算符:BINARY expr —— 强制大小写敏感比较。
|
|
2232
|
+
if (this.isWord('BINARY')) {
|
|
2233
|
+
const nt = this.peek(1);
|
|
2234
|
+
const startVal = nt && (nt.type === 'number' || nt.type === 'string' || nt.type === 'ident' || nt.type === 'param' ||
|
|
2235
|
+
(nt.type === 'op' && (nt.value === '(' || nt.value === '-' || nt.value === '+')) ||
|
|
2236
|
+
(nt.type === 'keyword' && (nt.value === 'NULL' || nt.value === 'TRUE' || nt.value === 'FALSE' || nt.value === 'DEFAULT')));
|
|
2237
|
+
if (startVal) {
|
|
2238
|
+
this.next(); // 消费 BINARY
|
|
2239
|
+
// 字面量要包成 literal,否则 resolveOperand 把 'ABC' 当列名
|
|
2240
|
+
const lt = this.peek();
|
|
2241
|
+
if (lt.type === 'string' || lt.type === 'number') { const lit = this.next(); return { type: 'binary', operand: { type: 'literal', value: lit.value } }; }
|
|
2242
|
+
const operand = this.parseValue();
|
|
2243
|
+
return { type: 'binary', operand };
|
|
2244
|
+
}
|
|
2245
|
+
}
|
|
2246
|
+
// MySQL 区间字面量:INTERVAL n UNIT(date + INTERVAL 3 DAY / date - INTERVAL 1 MONTH)
|
|
2247
|
+
if (this.isWord('INTERVAL')) {
|
|
2248
|
+
this.next();
|
|
2249
|
+
const nTok = this.peek();
|
|
2250
|
+
let nVal;
|
|
2251
|
+
if (nTok.type === 'op' && (nTok.value === '-' || nTok.value === '+')) {
|
|
2252
|
+
this.next();
|
|
2253
|
+
const numTok = this.next();
|
|
2254
|
+
nVal = (nTok.value === '-' ? -1 : 1) * Number(numTok.value);
|
|
2255
|
+
} else {
|
|
2256
|
+
this.next();
|
|
2257
|
+
nVal = Number(nTok.value);
|
|
2258
|
+
}
|
|
2259
|
+
const unitTok = this.next();
|
|
2260
|
+
return { type: 'interval', value: nVal, unit: String(unitTok.value).toUpperCase() };
|
|
2261
|
+
}
|
|
2194
2262
|
const t = this.next();
|
|
2195
2263
|
// P1-20: SELECT -col 报错——parseOperand 不处理负号
|
|
2196
2264
|
if (t.type === 'op' && t.value === '-') {
|
|
@@ -2871,6 +2939,8 @@ function resolveOperand(operand, row, ctx) {
|
|
|
2871
2939
|
case 'value':
|
|
2872
2940
|
case 'literal':
|
|
2873
2941
|
return operand.value;
|
|
2942
|
+
case 'binary':
|
|
2943
|
+
return resolveOperand(operand.operand, row, ctx);
|
|
2874
2944
|
case 'sysvar': {
|
|
2875
2945
|
const s = (ctx && ctx.session && ctx.session.sysvars) || {};
|
|
2876
2946
|
const name = String(operand.name).toLowerCase();
|
|
@@ -2909,6 +2979,32 @@ function resolveOperand(operand, row, ctx) {
|
|
|
2909
2979
|
return undefined;
|
|
2910
2980
|
}
|
|
2911
2981
|
case 'arith': {
|
|
2982
|
+
// date + INTERVAL n UNIT / date - INTERVAL n UNIT(MySQL 区间算术)
|
|
2983
|
+
const lIv = operand.left && operand.left.type === 'interval';
|
|
2984
|
+
const rIv = operand.right && operand.right.type === 'interval';
|
|
2985
|
+
if ((operand.op === '+' || operand.op === '-') && (lIv || rIv)) {
|
|
2986
|
+
if (lIv && operand.op === '-') return null; // INTERVAL - date 无意义
|
|
2987
|
+
const ivNode = lIv ? operand.left : operand.right;
|
|
2988
|
+
const dateSide = lIv ? resolveOperand(operand.right, row, ctx) : resolveOperand(operand.left, row, ctx);
|
|
2989
|
+
const base = parseSqlDate(dateSide);
|
|
2990
|
+
if (!base) return null;
|
|
2991
|
+
const sign = operand.op === '-' ? -1 : 1;
|
|
2992
|
+
const isDateOnly = String(dateSide).trim().length <= 10;
|
|
2993
|
+
const d = new Date(base.getTime());
|
|
2994
|
+
const n = sign * Number(ivNode.value || 0);
|
|
2995
|
+
switch (String(ivNode.unit || '').toUpperCase()) {
|
|
2996
|
+
case 'DAY': case 'DAYS': d.setDate(d.getDate() + n); break;
|
|
2997
|
+
case 'WEEK': case 'WEEKS': d.setDate(d.getDate() + n * 7); break;
|
|
2998
|
+
case 'MONTH': case 'MONTHS': addMonthsClamped(d, n); break;
|
|
2999
|
+
case 'QUARTER': addMonthsClamped(d, n * 3); break;
|
|
3000
|
+
case 'YEAR': case 'YEARS': addMonthsClamped(d, n * 12); break;
|
|
3001
|
+
case 'HOUR': case 'HOURS': d.setHours(d.getHours() + n); break;
|
|
3002
|
+
case 'MINUTE': case 'MINUTES': d.setMinutes(d.getMinutes() + n); break;
|
|
3003
|
+
case 'SECOND': case 'SECONDS': d.setSeconds(d.getSeconds() + n); break;
|
|
3004
|
+
default: return null;
|
|
3005
|
+
}
|
|
3006
|
+
return fmtSqlDate(isDateOnly, d);
|
|
3007
|
+
}
|
|
2912
3008
|
const l = resolveOperand(operand.left, row, ctx);
|
|
2913
3009
|
const r = resolveOperand(operand.right, row, ctx);
|
|
2914
3010
|
if (l === null || r === null || l === undefined || r === undefined) return null;
|
|
@@ -4313,6 +4409,19 @@ function normalizeCmpValue(a, other) {
|
|
|
4313
4409
|
return otherIsNumeric ? (a ? 1 : 0) : a;
|
|
4314
4410
|
}
|
|
4315
4411
|
|
|
4412
|
+
// MySQL 默认 collation(utf8mb4_general_ci)下字符串比较不区分大小写,
|
|
4413
|
+
// 与 LIKE 保持一致。cs=true 表示显式 BINARY/COLLATE 强制大小写敏感。
|
|
4414
|
+
function ciEq(a, b, cs) {
|
|
4415
|
+
if (cs) return a === b;
|
|
4416
|
+
if (typeof a === 'string' && typeof b === 'string') return a.toLowerCase() === b.toLowerCase();
|
|
4417
|
+
return String(a) === String(b);
|
|
4418
|
+
}
|
|
4419
|
+
function ciCompare(a, b, cs) {
|
|
4420
|
+
if (cs) return a === b ? 0 : (a < b ? -1 : 1);
|
|
4421
|
+
const as = String(a).toLowerCase(), bs = String(b).toLowerCase();
|
|
4422
|
+
return as === bs ? 0 : (as < bs ? -1 : 1);
|
|
4423
|
+
}
|
|
4424
|
+
|
|
4316
4425
|
function evaluateExpr(expr, row, ctx) {
|
|
4317
4426
|
if (expr === null || expr === undefined) return false;
|
|
4318
4427
|
switch (expr.type) {
|
|
@@ -4329,12 +4438,14 @@ function evaluateExpr(expr, row, ctx) {
|
|
|
4329
4438
|
case 'compare': {
|
|
4330
4439
|
const l = resolveOperand(expr.left, row, ctx);
|
|
4331
4440
|
const r = resolveOperand(expr.right, row, ctx);
|
|
4441
|
+
// BINARY 限定符:任一侧是 BINARY 操作数即强制大小写敏感
|
|
4442
|
+
const cs = !!(expr.cs || (expr.left && expr.left.type === 'binary') || (expr.right && expr.right.type === 'binary'));
|
|
4332
4443
|
// MySQL NULL 安全相等:NULL <=> NULL 为真;NULL <=> 值 为假
|
|
4333
4444
|
if (expr.op === '<=>') {
|
|
4334
4445
|
const ln = l === null || l === undefined;
|
|
4335
4446
|
const rn = r === null || r === undefined;
|
|
4336
4447
|
if (ln || rn) return ln && rn;
|
|
4337
|
-
return l
|
|
4448
|
+
return ciEq(l, r, cs);
|
|
4338
4449
|
}
|
|
4339
4450
|
// SQL 标准:与 NULL 的比较结果是 UNKNOWN,这里用 null 表示。
|
|
4340
4451
|
// 不能返回 false —— 否则 NOT (a > 1) 会把 a 为 NULL 的行错误纳入结果。
|
|
@@ -4349,11 +4460,19 @@ function evaluateExpr(expr, row, ctx) {
|
|
|
4349
4460
|
const mixedNum = (isNum(nl) && isNumStr(nr)) || (isNumStr(nl) && isNum(nr));
|
|
4350
4461
|
if (expr.op === '=') {
|
|
4351
4462
|
if (mixedNum) return Number(nl) === Number(nr);
|
|
4352
|
-
return nl
|
|
4463
|
+
return ciEq(nl, nr, cs);
|
|
4464
|
+
}
|
|
4465
|
+
if (mixedNum) { const fn = OPERATORS[expr.op]; return fn(Number(nl), Number(nr)); }
|
|
4466
|
+
if (typeof nl === 'number' && typeof nr === 'number') { const fn = OPERATORS[expr.op]; return fn(nl, nr); }
|
|
4467
|
+
const c = ciCompare(nl, nr, cs);
|
|
4468
|
+
switch (expr.op) {
|
|
4469
|
+
case '!=': case '<>': return c !== 0;
|
|
4470
|
+
case '<': return c < 0;
|
|
4471
|
+
case '<=': return c <= 0;
|
|
4472
|
+
case '>': return c > 0;
|
|
4473
|
+
case '>=': return c >= 0;
|
|
4474
|
+
default: return c === 0;
|
|
4353
4475
|
}
|
|
4354
|
-
const fn = OPERATORS[expr.op];
|
|
4355
|
-
if (mixedNum) return fn(Number(nl), Number(nr));
|
|
4356
|
-
return typeof nl === 'number' && typeof nr === 'number' ? fn(nl, nr) : fn(String(nl), String(nr));
|
|
4357
4476
|
}
|
|
4358
4477
|
case 'isNull': {
|
|
4359
4478
|
const v = resolveOperand(expr.operand, row, ctx);
|
|
@@ -4365,6 +4484,8 @@ function evaluateExpr(expr, row, ctx) {
|
|
|
4365
4484
|
if (v === null || v === undefined) return null; // NULL IN (...) → NULL(三值逻辑)
|
|
4366
4485
|
if (expr.subquery && expr.subquery._values !== undefined) expr.list = expr.subquery._values;
|
|
4367
4486
|
if (!expr.list) return expr.not ? true : false;
|
|
4487
|
+
const cs = !!(expr.cs || (expr.operand && expr.operand.type === 'binary') ||
|
|
4488
|
+
(expr.list && expr.list.some(x => x && x.type === 'binary')));
|
|
4368
4489
|
|
|
4369
4490
|
// 先找匹配:只要有一个非 NULL 元素等于 v,整个 IN 就是 TRUE ——
|
|
4370
4491
|
// 列表里别的位置有没有 NULL 都不影响。
|
|
@@ -4372,9 +4493,12 @@ function evaluateExpr(expr, row, ctx) {
|
|
|
4372
4493
|
// 此前是先判「列表里有 NULL 就返回 NULL」,于是 `v IN (10, NULL)`
|
|
4373
4494
|
// 连 v=10 的行都匹配不上(实测返回 0 行,应为 2 行)。
|
|
4374
4495
|
// 三值逻辑里 NULL 只在**没匹配上**的时候才让结果变 UNKNOWN。
|
|
4375
|
-
const matched = expr.list.some(x =>
|
|
4376
|
-
|
|
4377
|
-
|
|
4496
|
+
const matched = expr.list.some(x => {
|
|
4497
|
+
// list 元素可能是表达式节点(如 IN (BINARY 'ABC')),需先求值;
|
|
4498
|
+
// 原始字面量('ABC' / 10)直接比较,不能走 resolveOperand(会把字符串当列名)。
|
|
4499
|
+
const xv = (x && typeof x === 'object') ? resolveOperand(x, row, ctx) : x;
|
|
4500
|
+
return xv !== null && xv !== undefined && (xv === v || ciEq(xv, v, cs));
|
|
4501
|
+
});
|
|
4378
4502
|
if (matched) return expr.not ? false : true;
|
|
4379
4503
|
|
|
4380
4504
|
// 没匹配上:列表里有 NULL → UNKNOWN,否则是确定的 FALSE
|
|
@@ -5255,22 +5379,38 @@ class SQLExecutor {
|
|
|
5255
5379
|
throw createError('ER_PARSE_ERROR', `Table '${statement.table}' has no primary key, UPDATE requires a primary key`);
|
|
5256
5380
|
}
|
|
5257
5381
|
const all = (await this.engine.find(statement.table, {}, { limit: 1e9, offset: 0 })).map(r => normalizeRow(r, schema));
|
|
5382
|
+
// UPDATE ... ORDER BY ... LIMIT n:先过滤、排序、截取,再逐行更新
|
|
5383
|
+
let candidates = all.filter(row => !statement.where || evaluateExpr(statement.where, row, this.ctx));
|
|
5384
|
+
if (statement.orderBy) {
|
|
5385
|
+
const cmp = (a, b) => {
|
|
5386
|
+
for (const o of statement.orderBy) {
|
|
5387
|
+
const av = resolveOperand({ type: 'column', name: o.column }, a, this.ctx);
|
|
5388
|
+
const bv = resolveOperand({ type: 'column', name: o.column }, b, this.ctx);
|
|
5389
|
+
if (av === bv) continue;
|
|
5390
|
+
if (av === undefined || av === null) return o.dir === 'asc' ? -1 : 1;
|
|
5391
|
+
if (bv === undefined || bv === null) return o.dir === 'asc' ? 1 : -1;
|
|
5392
|
+
const d = typeof av === 'number' && typeof bv === 'number' ? av - bv : String(av).localeCompare(String(bv));
|
|
5393
|
+
if (d !== 0) return o.dir === 'asc' ? d : -d;
|
|
5394
|
+
}
|
|
5395
|
+
return 0;
|
|
5396
|
+
};
|
|
5397
|
+
candidates = candidates.slice().sort(cmp);
|
|
5398
|
+
}
|
|
5399
|
+
if (typeof statement.limit === 'number') candidates = candidates.slice(0, statement.limit);
|
|
5258
5400
|
let count = 0;
|
|
5259
|
-
for (const row of
|
|
5260
|
-
|
|
5261
|
-
|
|
5262
|
-
|
|
5263
|
-
|
|
5264
|
-
|
|
5265
|
-
data[col] = typeof val === 'object' && val !== null && val.type ? resolveOperand(val, row, this.ctx) : val;
|
|
5266
|
-
}
|
|
5267
|
-
// 必须 await:wasm / native client 的 updateById 是异步的,
|
|
5268
|
-
// 不等就会把「主键/唯一约束冲突」的异常变成未处理的 Promise 拒绝 ——
|
|
5269
|
-
// 表现为 UPDATE 静默成功、产生两行同主键,而调用方拿不到任何错误。
|
|
5270
|
-
// lib/database.js 的实现是同步的,await 同步值同样安全。
|
|
5271
|
-
await this.engine.updateById(statement.table, id, data);
|
|
5272
|
-
count++;
|
|
5401
|
+
for (const row of candidates) {
|
|
5402
|
+
const id = this._rowPkId(row, pkCols);
|
|
5403
|
+
if (id !== undefined) {
|
|
5404
|
+
const data = {};
|
|
5405
|
+
for (const [col, val] of statement.assignments) {
|
|
5406
|
+
data[col] = typeof val === 'object' && val !== null && val.type ? resolveOperand(val, row, this.ctx) : val;
|
|
5273
5407
|
}
|
|
5408
|
+
// 必须 await:wasm / native client 的 updateById 是异步的,
|
|
5409
|
+
// 不等就会把「主键/唯一约束冲突」的异常变成未处理的 Promise 拒绝 ——
|
|
5410
|
+
// 表现为 UPDATE 静默成功、产生两行同主键,而调用方拿不到任何错误。
|
|
5411
|
+
// lib/database.js 的实现是同步的,await 同步值同样安全。
|
|
5412
|
+
await this.engine.updateById(statement.table, id, data);
|
|
5413
|
+
count++;
|
|
5274
5414
|
}
|
|
5275
5415
|
}
|
|
5276
5416
|
await this.engine.flush();
|
|
@@ -5670,17 +5810,26 @@ class SQLExecutor {
|
|
|
5670
5810
|
// - 带表限定的条件(如 "o.amount"):检查表名/别名是否匹配
|
|
5671
5811
|
// - 不带表限定的条件(如 "amount"):检查当前表的 schema 是否包含该列
|
|
5672
5812
|
const filter = {};
|
|
5813
|
+
// 字符串列默认 collation 为 CI(大小写不敏感),而存储层 find 的等值
|
|
5814
|
+
// 过滤是严格比较(CS)。字符串等值若下推会被 CS 预筛,绕过 evaluateExpr 的
|
|
5815
|
+
// ciEq,导致 `code = 'abc'` 匹配不到 'ABC'。所以字符串/JSON 列不参与下推,
|
|
5816
|
+
// 交给 evaluateExpr 按 CI 求值;数值/布尔仍下推走索引。
|
|
5817
|
+
const isNonPushdownCol = (d) => {
|
|
5818
|
+
const t = String((d && d.type) || '').toLowerCase();
|
|
5819
|
+
return t === 'text' || t === 'varchar' || t === 'char' || t === 'string' ||
|
|
5820
|
+
t === 'blob' || t === 'clob' || t === 'json' || t === 'object' || t === 'array';
|
|
5821
|
+
};
|
|
5673
5822
|
for (const [key, val] of Object.entries(allFilters)) {
|
|
5674
5823
|
if (key.includes('.')) {
|
|
5675
5824
|
// 带表限定:检查是否匹配当前表的别名或表名
|
|
5676
5825
|
const tablePart = key.slice(0, key.lastIndexOf('.'));
|
|
5677
5826
|
if (tablePart === alias || tablePart === table) {
|
|
5678
5827
|
const colPart = key.slice(key.lastIndexOf('.') + 1);
|
|
5679
|
-
if (schema[colPart]) filter[colPart] = this._coerceValue(val, schema[colPart]);
|
|
5828
|
+
if (schema[colPart] && !isNonPushdownCol(schema[colPart])) filter[colPart] = this._coerceValue(val, schema[colPart]);
|
|
5680
5829
|
}
|
|
5681
5830
|
} else {
|
|
5682
5831
|
// 不带表限定:只下推到持有该列的表
|
|
5683
|
-
if (schema[key]) filter[key] = this._coerceValue(val, schema[key]);
|
|
5832
|
+
if (schema[key] && !isNonPushdownCol(schema[key])) filter[key] = this._coerceValue(val, schema[key]);
|
|
5684
5833
|
}
|
|
5685
5834
|
}
|
|
5686
5835
|
const useIndex = Object.keys(filter).length > 0 &&
|
|
@@ -7208,24 +7357,42 @@ class SQLExecutor {
|
|
|
7208
7357
|
* 同时按「投影列名 → 下标」解析;命中就直接 `row[idx]`,
|
|
7209
7358
|
* 不再走 resolveOperand 的通用路径。
|
|
7210
7359
|
*/
|
|
7360
|
+
// 把 ORDER BY 引用的输出列(别名或位置序号)解析回该列的**底层取值**。
|
|
7361
|
+
// 非分组路径的行对象键是底层列名(如 'v'),不是别名('val'),
|
|
7362
|
+
// 直接 r['val'] 会取到 undefined,导致 ORDER BY 静默不排序。
|
|
7363
|
+
const resolveByColumn = (c) => {
|
|
7364
|
+
if (!c) return (r) => null;
|
|
7365
|
+
if (typeof c.expr === 'string' && c.expr !== '*') {
|
|
7366
|
+
const name = c.expr;
|
|
7367
|
+
const node = { type: 'column', name };
|
|
7368
|
+
return (r) => {
|
|
7369
|
+
const v = (r == null) ? undefined : r[name];
|
|
7370
|
+
return v !== undefined ? v : resolveOperand(node, r, this.ctx);
|
|
7371
|
+
};
|
|
7372
|
+
}
|
|
7373
|
+
if (c.scalar) return (r) => resolveOperand(c.scalar, r, this.ctx);
|
|
7374
|
+
if (c.aggregate) return (r) => this._aggValue([r], c.aggregate, c.column, c.distinct, c.separator, c.orderBy);
|
|
7375
|
+
const nm = c.alias;
|
|
7376
|
+
return (r) => (r == null ? null : r[nm]);
|
|
7377
|
+
};
|
|
7211
7378
|
const ordKeys = statement.orderBy.map(o => {
|
|
7212
7379
|
const dir = o.dir === 'asc' ? 1 : -1;
|
|
7213
|
-
// 位置序号:ORDER BY 1
|
|
7380
|
+
// 位置序号:ORDER BY 1(1-based,指第 N 个输出列)
|
|
7214
7381
|
const pos = (o.column && typeof o.column === 'object' && o.column.type === 'value' && typeof o.column.value === 'number')
|
|
7215
7382
|
? o.column.value
|
|
7216
7383
|
: (typeof o.column === 'number' ? o.column : null);
|
|
7217
7384
|
if (pos !== null && pos !== undefined) {
|
|
7218
|
-
|
|
7219
|
-
return { dir, at: (name === undefined || name === null) ? () => null : (r) => (r == null ? null : r[name]) };
|
|
7385
|
+
return { dir, at: resolveByColumn(statement.columns[pos - 1]) };
|
|
7220
7386
|
}
|
|
7221
7387
|
if (typeof o.column === 'string') {
|
|
7222
7388
|
const f = o.column;
|
|
7223
|
-
//
|
|
7389
|
+
// 命中某个输出列的别名/列名 → 用该列的底层取值(修复别名排序)
|
|
7390
|
+
const idx = ordNames.findIndex(n => n === f);
|
|
7391
|
+
if (idx >= 0) return { dir, at: resolveByColumn(statement.columns[idx]) };
|
|
7224
7392
|
const node = { type: 'column', name: f };
|
|
7225
7393
|
return {
|
|
7226
7394
|
dir,
|
|
7227
7395
|
at: (r) => {
|
|
7228
|
-
// 先按字段名直取;取不到再走 resolveOperand(点号路径、表达式等)
|
|
7229
7396
|
const v = (r == null) ? undefined : r[f];
|
|
7230
7397
|
return v !== undefined ? v : resolveOperand(node, r, this.ctx);
|
|
7231
7398
|
},
|
package/package.json
CHANGED