jsql-neo 6.5.8 → 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 +4 -1
- package/index.js +1 -1
- package/lib/btree.js +9 -9
- package/lib/cache.js +1 -1
- package/lib/database.js +1 -1
- package/lib/jsql_format.js +1 -1
- package/lib/mongo_server.js +6 -2
- package/lib/native_client.js +1 -1
- package/lib/nedb_compat.js +1 -1
- package/lib/query.js +1 -1
- package/lib/redis_server.js +1 -1
- package/lib/sql.js +222 -39
- package/lib/tui.js +3 -1
- 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
|

|
|
@@ -188,3 +188,6 @@ Apache-2.0 License — contributions welcome!
|
|
|
188
188
|
- **GitHub:** [vexify-org/JSQL-neo](https://github.com/vexify-org/JSQL-neo)
|
|
189
189
|
- **Issues:** [Open an issue](https://github.com/vexify-org/JSQL-neo/issues)
|
|
190
190
|
- **PRs:** Fork → branch → PR — thank you!
|
|
191
|
+
- **Contributing:** [CONTRIBUTING.md](CONTRIBUTING.md)
|
|
192
|
+
- **Code of Conduct:** [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md)
|
|
193
|
+
- **Security:** [SECURITY.md](SECURITY.md) — please report vulnerabilities privately
|
package/index.js
CHANGED
package/lib/btree.js
CHANGED
|
@@ -173,7 +173,7 @@ class BTree {
|
|
|
173
173
|
const s = k === undefined || k === null ? '' : String(k);
|
|
174
174
|
// 不能假设键按字典序排序(数字键按数值序),必须检查所有键
|
|
175
175
|
if (s.startsWith(p)) {
|
|
176
|
-
|
|
176
|
+
for (const __e of node.values[i]) result.push(__e);
|
|
177
177
|
}
|
|
178
178
|
}
|
|
179
179
|
node = node.next;
|
|
@@ -193,7 +193,7 @@ class BTree {
|
|
|
193
193
|
const k = node.keys[i];
|
|
194
194
|
if (k > max) return result;
|
|
195
195
|
if (k >= min) {
|
|
196
|
-
|
|
196
|
+
for (const __e of node.values[i]) result.push(__e);
|
|
197
197
|
}
|
|
198
198
|
}
|
|
199
199
|
node = node.next;
|
|
@@ -209,7 +209,7 @@ class BTree {
|
|
|
209
209
|
let node = this._findLeaf(min);
|
|
210
210
|
while (node) {
|
|
211
211
|
for (let i = 0; i < node.keys.length; i++) {
|
|
212
|
-
if (node.keys[i] > min)
|
|
212
|
+
if (node.keys[i] > min) for (const __e of node.values[i]) result.push(__e);
|
|
213
213
|
}
|
|
214
214
|
node = node.next;
|
|
215
215
|
}
|
|
@@ -221,7 +221,7 @@ class BTree {
|
|
|
221
221
|
let node = this._findLeaf(min);
|
|
222
222
|
while (node) {
|
|
223
223
|
for (let i = 0; i < node.keys.length; i++) {
|
|
224
|
-
if (node.keys[i] >= min)
|
|
224
|
+
if (node.keys[i] >= min) for (const __e of node.values[i]) result.push(__e);
|
|
225
225
|
}
|
|
226
226
|
node = node.next;
|
|
227
227
|
}
|
|
@@ -237,7 +237,7 @@ class BTree {
|
|
|
237
237
|
while (!node.leaf) node = node.children[0];
|
|
238
238
|
while (node) {
|
|
239
239
|
for (let i = 0; i < node.keys.length; i++) {
|
|
240
|
-
if (node.keys[i] < max)
|
|
240
|
+
if (node.keys[i] < max) for (const __e of node.values[i]) result.push(__e);
|
|
241
241
|
else return result;
|
|
242
242
|
}
|
|
243
243
|
node = node.next;
|
|
@@ -251,7 +251,7 @@ class BTree {
|
|
|
251
251
|
while (!node.leaf) node = node.children[0];
|
|
252
252
|
while (node) {
|
|
253
253
|
for (let i = 0; i < node.keys.length; i++) {
|
|
254
|
-
if (node.keys[i] <= max)
|
|
254
|
+
if (node.keys[i] <= max) for (const __e of node.values[i]) result.push(__e);
|
|
255
255
|
else return result;
|
|
256
256
|
}
|
|
257
257
|
node = node.next;
|
|
@@ -408,9 +408,9 @@ class BTree {
|
|
|
408
408
|
left.keys.push(midKey);
|
|
409
409
|
left.values.push(midVal);
|
|
410
410
|
}
|
|
411
|
-
left.keys.push(
|
|
412
|
-
left.values.push(
|
|
413
|
-
if (!left.leaf) left.children.push(
|
|
411
|
+
for (const __e of right.keys) left.keys.push(__e);
|
|
412
|
+
for (const __e of right.values) left.values.push(__e);
|
|
413
|
+
if (!left.leaf) for (const __e of right.children) left.children.push(__e);
|
|
414
414
|
if (left.leaf) left.next = right.next;
|
|
415
415
|
|
|
416
416
|
parent.keys.splice(index, 1);
|
package/lib/cache.js
CHANGED
|
@@ -152,7 +152,7 @@ class Cache {
|
|
|
152
152
|
throw new Error('WRONGTYPE Operation against a key holding the wrong kind of value');
|
|
153
153
|
}
|
|
154
154
|
if (!list) { list = []; this._getData().set(k, list); }
|
|
155
|
-
|
|
155
|
+
for (const __e of values.map(String)) list.push(__e);
|
|
156
156
|
this._dirty = true;
|
|
157
157
|
return list.length;
|
|
158
158
|
}
|
package/lib/database.js
CHANGED
|
@@ -566,7 +566,7 @@ class Database {
|
|
|
566
566
|
case 'insert': {
|
|
567
567
|
const t = op.table && this._tables[op.table];
|
|
568
568
|
if (!t || !Array.isArray(op.rows) || op.rows.length === 0) { failed++; break; }
|
|
569
|
-
t._rows.push(
|
|
569
|
+
for (const __e of op.rows) t._rows.push(__e);
|
|
570
570
|
touched.add(t);
|
|
571
571
|
replayed++;
|
|
572
572
|
break;
|
package/lib/jsql_format.js
CHANGED
package/lib/mongo_server.js
CHANGED
|
@@ -647,10 +647,14 @@ class MongoServer {
|
|
|
647
647
|
doc[k] = vals.reduce((a, b) => a + b, 0) / vals.length;
|
|
648
648
|
} else if (v.$min !== undefined) {
|
|
649
649
|
const vals = grow.map(r => r[field]).filter(v => v != null);
|
|
650
|
-
|
|
650
|
+
let best = null;
|
|
651
|
+
for (const x of vals) if (best === null || x < best) best = x;
|
|
652
|
+
doc[k] = best;
|
|
651
653
|
} else if (v.$max !== undefined) {
|
|
652
654
|
const vals = grow.map(r => r[field]).filter(v => v != null);
|
|
653
|
-
|
|
655
|
+
let best = null;
|
|
656
|
+
for (const x of vals) if (best === null || x > best) best = x;
|
|
657
|
+
doc[k] = best;
|
|
654
658
|
} else if (v.$first !== undefined) {
|
|
655
659
|
doc[k] = grow[0] ? grow[0][field] : null;
|
|
656
660
|
} else if (v.$last !== undefined) {
|
package/lib/native_client.js
CHANGED
|
@@ -851,7 +851,7 @@ class JSQL {
|
|
|
851
851
|
}
|
|
852
852
|
if (!(await this._runHooks('beforeUpdate', [table, pairs]))) return;
|
|
853
853
|
if (!this._opBuffer.update[table]) this._opBuffer.update[table] = [];
|
|
854
|
-
this._opBuffer.update[table].push(
|
|
854
|
+
for (const __e of pairs) this._opBuffer.update[table].push(__e);
|
|
855
855
|
this._scheduleOpFlush();
|
|
856
856
|
}
|
|
857
857
|
|
package/lib/nedb_compat.js
CHANGED
|
@@ -170,7 +170,7 @@ function applyModifier(doc, update) {
|
|
|
170
170
|
for (const [k, v] of Object.entries(payload)) {
|
|
171
171
|
const cur = getPath(doc, k);
|
|
172
172
|
const arr = Array.isArray(cur) ? cur : [];
|
|
173
|
-
if (v && typeof v === 'object' && v.$each)
|
|
173
|
+
if (v && typeof v === 'object' && v.$each) for (const __e of v.$each) arr.push(__e);
|
|
174
174
|
else arr.push(v);
|
|
175
175
|
setPath(doc, k, arr);
|
|
176
176
|
}
|
package/lib/query.js
CHANGED
package/lib/redis_server.js
CHANGED
|
@@ -335,7 +335,7 @@ class RedisServer {
|
|
|
335
335
|
if (cmd === 'LPUSH') {
|
|
336
336
|
for (const val of args.slice(1)) list.unshift(val);
|
|
337
337
|
} else {
|
|
338
|
-
|
|
338
|
+
for (const __e of args.slice(1)) list.push(__e);
|
|
339
339
|
}
|
|
340
340
|
this.db.set(args[0], { type: 'list', val: list, ttl: v ? v.ttl : null });
|
|
341
341
|
this._scheduleSnapshot();
|
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;
|
|
@@ -4114,13 +4210,25 @@ function applyScalarFunction(fnNode, row, ctx) {
|
|
|
4114
4210
|
}
|
|
4115
4211
|
function depth(obj) {
|
|
4116
4212
|
if (typeof obj !== 'object' || obj === null) return 0;
|
|
4117
|
-
|
|
4118
|
-
|
|
4119
|
-
|
|
4213
|
+
let max = 0;
|
|
4214
|
+
const stack = [{ o: obj, d: 1 }];
|
|
4215
|
+
while (stack.length) {
|
|
4216
|
+
const { o, d } = stack.pop();
|
|
4217
|
+
if (typeof o !== 'object' || o === null) continue;
|
|
4218
|
+
if (d > max) max = d;
|
|
4219
|
+
if (Array.isArray(o)) {
|
|
4220
|
+
for (const x of o) {
|
|
4221
|
+
if (x && typeof x === 'object') stack.push({ o: x, d: d + 1 });
|
|
4222
|
+
}
|
|
4223
|
+
} else {
|
|
4224
|
+
const ks = Object.keys(o);
|
|
4225
|
+
for (const k of ks) {
|
|
4226
|
+
const x = o[k];
|
|
4227
|
+
if (x && typeof x === 'object') stack.push({ o: x, d: d + 1 });
|
|
4228
|
+
}
|
|
4229
|
+
}
|
|
4120
4230
|
}
|
|
4121
|
-
|
|
4122
|
-
if (keys.length === 0) return 1;
|
|
4123
|
-
return 1 + Math.max(...keys.map(k => depth(obj[k])));
|
|
4231
|
+
return max;
|
|
4124
4232
|
}
|
|
4125
4233
|
return depth(data);
|
|
4126
4234
|
}
|
|
@@ -4301,6 +4409,19 @@ function normalizeCmpValue(a, other) {
|
|
|
4301
4409
|
return otherIsNumeric ? (a ? 1 : 0) : a;
|
|
4302
4410
|
}
|
|
4303
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
|
+
|
|
4304
4425
|
function evaluateExpr(expr, row, ctx) {
|
|
4305
4426
|
if (expr === null || expr === undefined) return false;
|
|
4306
4427
|
switch (expr.type) {
|
|
@@ -4317,12 +4438,14 @@ function evaluateExpr(expr, row, ctx) {
|
|
|
4317
4438
|
case 'compare': {
|
|
4318
4439
|
const l = resolveOperand(expr.left, row, ctx);
|
|
4319
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'));
|
|
4320
4443
|
// MySQL NULL 安全相等:NULL <=> NULL 为真;NULL <=> 值 为假
|
|
4321
4444
|
if (expr.op === '<=>') {
|
|
4322
4445
|
const ln = l === null || l === undefined;
|
|
4323
4446
|
const rn = r === null || r === undefined;
|
|
4324
4447
|
if (ln || rn) return ln && rn;
|
|
4325
|
-
return l
|
|
4448
|
+
return ciEq(l, r, cs);
|
|
4326
4449
|
}
|
|
4327
4450
|
// SQL 标准:与 NULL 的比较结果是 UNKNOWN,这里用 null 表示。
|
|
4328
4451
|
// 不能返回 false —— 否则 NOT (a > 1) 会把 a 为 NULL 的行错误纳入结果。
|
|
@@ -4337,11 +4460,19 @@ function evaluateExpr(expr, row, ctx) {
|
|
|
4337
4460
|
const mixedNum = (isNum(nl) && isNumStr(nr)) || (isNumStr(nl) && isNum(nr));
|
|
4338
4461
|
if (expr.op === '=') {
|
|
4339
4462
|
if (mixedNum) return Number(nl) === Number(nr);
|
|
4340
|
-
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;
|
|
4341
4475
|
}
|
|
4342
|
-
const fn = OPERATORS[expr.op];
|
|
4343
|
-
if (mixedNum) return fn(Number(nl), Number(nr));
|
|
4344
|
-
return typeof nl === 'number' && typeof nr === 'number' ? fn(nl, nr) : fn(String(nl), String(nr));
|
|
4345
4476
|
}
|
|
4346
4477
|
case 'isNull': {
|
|
4347
4478
|
const v = resolveOperand(expr.operand, row, ctx);
|
|
@@ -4353,6 +4484,8 @@ function evaluateExpr(expr, row, ctx) {
|
|
|
4353
4484
|
if (v === null || v === undefined) return null; // NULL IN (...) → NULL(三值逻辑)
|
|
4354
4485
|
if (expr.subquery && expr.subquery._values !== undefined) expr.list = expr.subquery._values;
|
|
4355
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')));
|
|
4356
4489
|
|
|
4357
4490
|
// 先找匹配:只要有一个非 NULL 元素等于 v,整个 IN 就是 TRUE ——
|
|
4358
4491
|
// 列表里别的位置有没有 NULL 都不影响。
|
|
@@ -4360,9 +4493,12 @@ function evaluateExpr(expr, row, ctx) {
|
|
|
4360
4493
|
// 此前是先判「列表里有 NULL 就返回 NULL」,于是 `v IN (10, NULL)`
|
|
4361
4494
|
// 连 v=10 的行都匹配不上(实测返回 0 行,应为 2 行)。
|
|
4362
4495
|
// 三值逻辑里 NULL 只在**没匹配上**的时候才让结果变 UNKNOWN。
|
|
4363
|
-
const matched = expr.list.some(x =>
|
|
4364
|
-
|
|
4365
|
-
|
|
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
|
+
});
|
|
4366
4502
|
if (matched) return expr.not ? false : true;
|
|
4367
4503
|
|
|
4368
4504
|
// 没匹配上:列表里有 NULL → UNKNOWN,否则是确定的 FALSE
|
|
@@ -5243,22 +5379,38 @@ class SQLExecutor {
|
|
|
5243
5379
|
throw createError('ER_PARSE_ERROR', `Table '${statement.table}' has no primary key, UPDATE requires a primary key`);
|
|
5244
5380
|
}
|
|
5245
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);
|
|
5246
5400
|
let count = 0;
|
|
5247
|
-
for (const row of
|
|
5248
|
-
|
|
5249
|
-
|
|
5250
|
-
|
|
5251
|
-
|
|
5252
|
-
|
|
5253
|
-
data[col] = typeof val === 'object' && val !== null && val.type ? resolveOperand(val, row, this.ctx) : val;
|
|
5254
|
-
}
|
|
5255
|
-
// 必须 await:wasm / native client 的 updateById 是异步的,
|
|
5256
|
-
// 不等就会把「主键/唯一约束冲突」的异常变成未处理的 Promise 拒绝 ——
|
|
5257
|
-
// 表现为 UPDATE 静默成功、产生两行同主键,而调用方拿不到任何错误。
|
|
5258
|
-
// lib/database.js 的实现是同步的,await 同步值同样安全。
|
|
5259
|
-
await this.engine.updateById(statement.table, id, data);
|
|
5260
|
-
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;
|
|
5261
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++;
|
|
5262
5414
|
}
|
|
5263
5415
|
}
|
|
5264
5416
|
await this.engine.flush();
|
|
@@ -5658,17 +5810,26 @@ class SQLExecutor {
|
|
|
5658
5810
|
// - 带表限定的条件(如 "o.amount"):检查表名/别名是否匹配
|
|
5659
5811
|
// - 不带表限定的条件(如 "amount"):检查当前表的 schema 是否包含该列
|
|
5660
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
|
+
};
|
|
5661
5822
|
for (const [key, val] of Object.entries(allFilters)) {
|
|
5662
5823
|
if (key.includes('.')) {
|
|
5663
5824
|
// 带表限定:检查是否匹配当前表的别名或表名
|
|
5664
5825
|
const tablePart = key.slice(0, key.lastIndexOf('.'));
|
|
5665
5826
|
if (tablePart === alias || tablePart === table) {
|
|
5666
5827
|
const colPart = key.slice(key.lastIndexOf('.') + 1);
|
|
5667
|
-
if (schema[colPart]) filter[colPart] = this._coerceValue(val, schema[colPart]);
|
|
5828
|
+
if (schema[colPart] && !isNonPushdownCol(schema[colPart])) filter[colPart] = this._coerceValue(val, schema[colPart]);
|
|
5668
5829
|
}
|
|
5669
5830
|
} else {
|
|
5670
5831
|
// 不带表限定:只下推到持有该列的表
|
|
5671
|
-
if (schema[key]) filter[key] = this._coerceValue(val, schema[key]);
|
|
5832
|
+
if (schema[key] && !isNonPushdownCol(schema[key])) filter[key] = this._coerceValue(val, schema[key]);
|
|
5672
5833
|
}
|
|
5673
5834
|
}
|
|
5674
5835
|
const useIndex = Object.keys(filter).length > 0 &&
|
|
@@ -5915,7 +6076,7 @@ class SQLExecutor {
|
|
|
5915
6076
|
_rollupRow(groupReps) {
|
|
5916
6077
|
const all = [];
|
|
5917
6078
|
for (const rep of groupReps) {
|
|
5918
|
-
if (rep._group)
|
|
6079
|
+
if (rep._group) for (const __e of rep._group) all.push(__e);
|
|
5919
6080
|
else all.push(rep);
|
|
5920
6081
|
}
|
|
5921
6082
|
const out = {};
|
|
@@ -5979,8 +6140,12 @@ class SQLExecutor {
|
|
|
5979
6140
|
const numeric = (v) => typeof v === 'number' ||
|
|
5980
6141
|
(typeof v === 'string' && String(v).trim() !== '' && Number.isFinite(Number(v)));
|
|
5981
6142
|
if (vs.every(numeric)) {
|
|
5982
|
-
|
|
5983
|
-
|
|
6143
|
+
let best = Number(vs[0]);
|
|
6144
|
+
for (let i = 1; i < vs.length; i++) {
|
|
6145
|
+
const n = Number(vs[i]);
|
|
6146
|
+
if (fn === 'MIN' ? n < best : n > best) best = n;
|
|
6147
|
+
}
|
|
6148
|
+
return best;
|
|
5984
6149
|
}
|
|
5985
6150
|
let best = vs[0];
|
|
5986
6151
|
for (const v of vs.slice(1)) {
|
|
@@ -7192,24 +7357,42 @@ class SQLExecutor {
|
|
|
7192
7357
|
* 同时按「投影列名 → 下标」解析;命中就直接 `row[idx]`,
|
|
7193
7358
|
* 不再走 resolveOperand 的通用路径。
|
|
7194
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
|
+
};
|
|
7195
7378
|
const ordKeys = statement.orderBy.map(o => {
|
|
7196
7379
|
const dir = o.dir === 'asc' ? 1 : -1;
|
|
7197
|
-
// 位置序号:ORDER BY 1
|
|
7380
|
+
// 位置序号:ORDER BY 1(1-based,指第 N 个输出列)
|
|
7198
7381
|
const pos = (o.column && typeof o.column === 'object' && o.column.type === 'value' && typeof o.column.value === 'number')
|
|
7199
7382
|
? o.column.value
|
|
7200
7383
|
: (typeof o.column === 'number' ? o.column : null);
|
|
7201
7384
|
if (pos !== null && pos !== undefined) {
|
|
7202
|
-
|
|
7203
|
-
return { dir, at: (name === undefined || name === null) ? () => null : (r) => (r == null ? null : r[name]) };
|
|
7385
|
+
return { dir, at: resolveByColumn(statement.columns[pos - 1]) };
|
|
7204
7386
|
}
|
|
7205
7387
|
if (typeof o.column === 'string') {
|
|
7206
7388
|
const f = o.column;
|
|
7207
|
-
//
|
|
7389
|
+
// 命中某个输出列的别名/列名 → 用该列的底层取值(修复别名排序)
|
|
7390
|
+
const idx = ordNames.findIndex(n => n === f);
|
|
7391
|
+
if (idx >= 0) return { dir, at: resolveByColumn(statement.columns[idx]) };
|
|
7208
7392
|
const node = { type: 'column', name: f };
|
|
7209
7393
|
return {
|
|
7210
7394
|
dir,
|
|
7211
7395
|
at: (r) => {
|
|
7212
|
-
// 先按字段名直取;取不到再走 resolveOperand(点号路径、表达式等)
|
|
7213
7396
|
const v = (r == null) ? undefined : r[f];
|
|
7214
7397
|
return v !== undefined ? v : resolveOperand(node, r, this.ctx);
|
|
7215
7398
|
},
|
package/lib/tui.js
CHANGED
|
@@ -72,7 +72,9 @@ function renderTable(headers, rows, maxWidth) {
|
|
|
72
72
|
for (const w of widths) total += w + 3;
|
|
73
73
|
if (total > maxCol && widths.length > 1) {
|
|
74
74
|
let overshoot = total - maxCol;
|
|
75
|
-
let
|
|
75
|
+
let maxW = widths[0];
|
|
76
|
+
for (const w of widths) if (w > maxW) maxW = w;
|
|
77
|
+
let i = widths.indexOf(maxW);
|
|
76
78
|
while (overshoot > 0) {
|
|
77
79
|
if (i === -1 || widths[i] <= 3) break;
|
|
78
80
|
const cut = Math.min(widths[i] - 3, overshoot);
|
package/package.json
CHANGED