jsql-neo 6.0.5 → 6.2.0
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/lib/sql.js +48 -16
- package/package.json +1 -1
- package/postinstall.js +49 -14
- package/test/readme-audit.test.js +35 -0
package/lib/sql.js
CHANGED
|
@@ -112,6 +112,8 @@ function tokenize(sql) {
|
|
|
112
112
|
else str += esc;
|
|
113
113
|
j += 2;
|
|
114
114
|
} else if (sql[j] === quote) {
|
|
115
|
+
// SQL 标准转义:字符串内两个连续引号表示一个字面量引号('O''Brien' → O'Brien)
|
|
116
|
+
if (sql[j + 1] === quote) { str += quote; j += 2; continue; }
|
|
115
117
|
break;
|
|
116
118
|
} else {
|
|
117
119
|
str += sql[j];
|
|
@@ -1855,6 +1857,13 @@ class Parser {
|
|
|
1855
1857
|
this.expect('op', ')');
|
|
1856
1858
|
return { type: 'cast', expr, dataType };
|
|
1857
1859
|
}
|
|
1860
|
+
// 零参时间函数:CURRENT_DATE / CURRENT_TIME / CURRENT_TIMESTAMP(可省略括号)。
|
|
1861
|
+
// 此前不在 KEYWORDS 里,被当成列名解析 → "no such column: CURRENT_DATE"
|
|
1862
|
+
if (['CURRENT_DATE', 'CURRENT_TIME', 'CURRENT_TIMESTAMP'].includes(String(t.value).toUpperCase())) {
|
|
1863
|
+
const name = String(t.value).toUpperCase();
|
|
1864
|
+
if (this.peek().type === 'op' && this.peek().value === '(') { this.next(); this.expect('op', ')'); }
|
|
1865
|
+
return { type: 'func', name, args: [] };
|
|
1866
|
+
}
|
|
1858
1867
|
// EXTRACT(YEAR FROM expr) —— 标准 SQL 的 FROM 形式
|
|
1859
1868
|
if (String(t.value).toUpperCase() === 'EXTRACT' && this.peek().type === 'op' && this.peek().value === '(') {
|
|
1860
1869
|
this.next();
|
|
@@ -2491,6 +2500,12 @@ function parseSqlDate(v) {
|
|
|
2491
2500
|
|
|
2492
2501
|
function pad2(n) { return String(n).padStart(2, '0'); }
|
|
2493
2502
|
|
|
2503
|
+
/** 本地时区的 'YYYY-MM-DD HH:mm:ss'(MySQL 的 NOW() 语义;区别于 UTC_TIMESTAMP) */
|
|
2504
|
+
function sqlLocalDateTime(d = new Date()) {
|
|
2505
|
+
return d.getFullYear() + '-' + pad2(d.getMonth() + 1) + '-' + pad2(d.getDate()) + ' ' +
|
|
2506
|
+
pad2(d.getHours()) + ':' + pad2(d.getMinutes()) + ':' + pad2(d.getSeconds());
|
|
2507
|
+
}
|
|
2508
|
+
|
|
2494
2509
|
/** 按输入形态回写日期:纯日期返回 YYYY-MM-DD,带时间返回 YYYY-MM-DD HH:MM:SS */
|
|
2495
2510
|
function fmtSqlDate(dateOnly, d) {
|
|
2496
2511
|
const ymd = d.getFullYear() + '-' + pad2(d.getMonth() + 1) + '-' + pad2(d.getDate());
|
|
@@ -2522,9 +2537,11 @@ function applyScalarFunction(fnNode, row, ctx) {
|
|
|
2522
2537
|
case 'FOUND_ROWS': return session && session.foundRows !== undefined ? session.foundRows : 0;
|
|
2523
2538
|
case 'CONNECTION_ID': return session && session.connectionId !== undefined ? session.connectionId : 0;
|
|
2524
2539
|
case 'DATABASE': case 'SCHEMA': return session && session.currentDb ? session.currentDb : 'default';
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
case '
|
|
2540
|
+
// NOW() / CURDATE() / CURTIME() 返回会话时区(本地)时间,不能用 toISOString()(UTC)——
|
|
2541
|
+
// 否则东八区凌晨会整整差一天(2024-02-29 被存成 2024-02-28)。
|
|
2542
|
+
case 'NOW': case 'CURRENT_TIMESTAMP': return sqlLocalDateTime();
|
|
2543
|
+
case 'CURDATE': case 'CURRENT_DATE': return sqlLocalDateTime().slice(0, 10);
|
|
2544
|
+
case 'CURTIME': return sqlLocalDateTime().slice(11, 19);
|
|
2528
2545
|
case 'UTC_TIMESTAMP': return new Date().toISOString().slice(0, 19).replace('T', ' ') + ' UTC';
|
|
2529
2546
|
case 'CONCAT': return args.map(a => a === null || a === undefined ? '' : String(a)).join('');
|
|
2530
2547
|
case 'CONCAT_WS': {
|
|
@@ -2829,7 +2846,8 @@ function evaluateExpr(expr, row, ctx) {
|
|
|
2829
2846
|
const v = resolveOperand(expr.operand, row, ctx);
|
|
2830
2847
|
if (expr.subquery && expr.subquery._values !== undefined) expr.list = expr.subquery._values;
|
|
2831
2848
|
if (!expr.list) return false;
|
|
2832
|
-
|
|
2849
|
+
const r = expr.list.some(x => x === v || String(x) === String(v));
|
|
2850
|
+
return expr.not ? !r : r; // NOT IN:此前漏了取反,导致 NOT IN 与 IN 结果相同
|
|
2833
2851
|
}
|
|
2834
2852
|
case 'like': {
|
|
2835
2853
|
const v = resolveOperand(expr.operand, row, ctx);
|
|
@@ -4644,16 +4662,16 @@ class SQLExecutor {
|
|
|
4644
4662
|
const rightNulls = (rightRes.schema) ? this._nullPrefixedRow(rightRes.schema, rightPrefix) : null;
|
|
4645
4663
|
const leftNulls = (rowsAll && rowsAll.schema) ? this._nullPrefixedRow(rowsAll.schema, firstPrefix) : null;
|
|
4646
4664
|
rows.forEach(l => {
|
|
4647
|
-
|
|
4665
|
+
// 一对多:一个左行可能匹配多个右行,必须全部收进来(此前只取第一个)
|
|
4666
|
+
const hits = [];
|
|
4648
4667
|
for (let ri = 0; ri < rightRows.length; ri++) {
|
|
4649
|
-
if (evaluateExpr(j.on, { ...l, ...rightRows[ri] }))
|
|
4650
|
-
m = ri;
|
|
4651
|
-
break;
|
|
4652
|
-
}
|
|
4668
|
+
if (evaluateExpr(j.on, { ...l, ...rightRows[ri] })) hits.push(ri);
|
|
4653
4669
|
}
|
|
4654
|
-
if (
|
|
4655
|
-
|
|
4656
|
-
|
|
4670
|
+
if (hits.length > 0) {
|
|
4671
|
+
for (const ri of hits) {
|
|
4672
|
+
matched.push({ ...l, ...rightRows[ri] });
|
|
4673
|
+
unmatchedRight.delete(ri);
|
|
4674
|
+
}
|
|
4657
4675
|
} else if (j.type === 'left' || j.type === 'full') {
|
|
4658
4676
|
matched.push(rightNulls ? { ...rightNulls, ...l } : { ...l });
|
|
4659
4677
|
}
|
|
@@ -4729,6 +4747,10 @@ class SQLExecutor {
|
|
|
4729
4747
|
const seen = new Set();
|
|
4730
4748
|
rows = rows.filter(r => {
|
|
4731
4749
|
const key = JSON.stringify(statement.columns.map(c => {
|
|
4750
|
+
// SELECT DISTINCT *:按整行去重(此前把 '*' 当列名解析成 null,导致所有行同 key 只剩 1 行)
|
|
4751
|
+
if (c.expr === '*') {
|
|
4752
|
+
return Object.keys(r).filter(k => !String(k).startsWith('_')).sort().map(k => r[k]);
|
|
4753
|
+
}
|
|
4732
4754
|
if (c.scalar) return resolveOperand(c.scalar, r, this.ctx);
|
|
4733
4755
|
if (c.caseExpr) return evaluateCaseVal(c.caseExpr, r);
|
|
4734
4756
|
if (c.aggregate) return this._aggValue(r._group || [r], c.aggregate || 'COUNT', c.column, c.distinct, c.separator, c.orderBy);
|
|
@@ -4776,10 +4798,20 @@ class SQLExecutor {
|
|
|
4776
4798
|
}
|
|
4777
4799
|
|
|
4778
4800
|
if (statement.orderBy) {
|
|
4779
|
-
// ORDER BY
|
|
4780
|
-
const
|
|
4781
|
-
|
|
4782
|
-
|
|
4801
|
+
// ORDER BY 项可能是列名字符串、表达式节点,或列的位置序号(ORDER BY 1,1-based)
|
|
4802
|
+
const ordNames = statement.columns.map(c => scalarColumnName(c));
|
|
4803
|
+
const ordVal = (row, o) => {
|
|
4804
|
+
const pos = (o.column && typeof o.column === 'object' && o.column.type === 'value' && typeof o.column.value === 'number')
|
|
4805
|
+
? o.column.value
|
|
4806
|
+
: (typeof o.column === 'number' ? o.column : null);
|
|
4807
|
+
if (pos !== null && pos !== undefined) {
|
|
4808
|
+
const name = ordNames[pos - 1];
|
|
4809
|
+
if (name !== undefined && name !== null) return resolveOperand({ type: 'column', name }, row, this.ctx);
|
|
4810
|
+
}
|
|
4811
|
+
return typeof o.column === 'string'
|
|
4812
|
+
? resolveOperand({ type: 'column', name: o.column }, row, this.ctx)
|
|
4813
|
+
: resolveOperand(o.column, row, this.ctx);
|
|
4814
|
+
};
|
|
4783
4815
|
const cmp = (a, b) => {
|
|
4784
4816
|
for (const o of statement.orderBy) {
|
|
4785
4817
|
const av = ordVal(a, o);
|
package/package.json
CHANGED
package/postinstall.js
CHANGED
|
@@ -14,10 +14,43 @@ const BP = path.join(ROOT, 'bin', BIN_NAME);
|
|
|
14
14
|
// Native N-API module (native/jsql-neo-native.node)
|
|
15
15
|
const NATIVE_PATH = path.join(ROOT, 'native', 'jsql-neo-native.node');
|
|
16
16
|
|
|
17
|
-
// GitHub
|
|
18
|
-
|
|
17
|
+
// GitHub 加速代理前缀;加速站的用法是把原始 URL 直接拼在后面:
|
|
18
|
+
// https://<host>/https://github.com/owner/repo/releases/download/vX.Y.Z/asset?token=<TOKEN>
|
|
19
|
+
// 可用环境变量覆盖 / 叠加(逗号分隔多个,按序尝试);空字符串 '' 表示直连兜底。
|
|
20
|
+
// 加速站候选(按优先级):自建站优先,公共站回退,最后直连兜底。
|
|
21
|
+
const DEFAULT_PROXIES = [
|
|
22
|
+
'https://gh-proxyjsql-neoworkerapi.vexify.de5.net/',
|
|
23
|
+
'https://gh-proxy.com/',
|
|
24
|
+
];
|
|
25
|
+
const GHPROXY = process.env.JSQL_GHPROXY !== undefined
|
|
26
|
+
? process.env.JSQL_GHPROXY
|
|
27
|
+
: DEFAULT_PROXIES[0];
|
|
28
|
+
const GHPROXY_TOKEN = process.env.JSQL_GHPROXY_TOKEN || '';
|
|
19
29
|
const TAG = `v${pkg.version}`;
|
|
20
30
|
|
|
31
|
+
/**
|
|
32
|
+
* 生成候选下载地址列表(按优先级):
|
|
33
|
+
* 环境变量 JSQL_GHPROXY 指定的代理 → 内置加速站 → 直连
|
|
34
|
+
*/
|
|
35
|
+
function proxyCandidates(raw) {
|
|
36
|
+
const bases = String(process.env.JSQL_GHPROXY || '')
|
|
37
|
+
.split(',')
|
|
38
|
+
.map((s) => s.trim())
|
|
39
|
+
.filter(Boolean)
|
|
40
|
+
.concat(DEFAULT_PROXIES)
|
|
41
|
+
.concat('');
|
|
42
|
+
const list = [];
|
|
43
|
+
for (const base of bases) {
|
|
44
|
+
let url = raw;
|
|
45
|
+
if (base) url = (base.endsWith('/') ? base : base + '/') + raw;
|
|
46
|
+
if (base && GHPROXY_TOKEN) {
|
|
47
|
+
url += (url.indexOf('?') === -1 ? '?' : '&') + 'token=' + encodeURIComponent(GHPROXY_TOKEN);
|
|
48
|
+
}
|
|
49
|
+
if (list.indexOf(url) === -1) list.push(url);
|
|
50
|
+
}
|
|
51
|
+
return list;
|
|
52
|
+
}
|
|
53
|
+
|
|
21
54
|
// 从 repository 字段提取 owner/repo,例如 vexify-org/JSQL-neo
|
|
22
55
|
const REPO = (pkg.repository && pkg.repository.url)
|
|
23
56
|
? pkg.repository.url.replace(/\.git$/, '').replace(/^.*github\.com\//i, '')
|
|
@@ -95,20 +128,22 @@ function tryLoadNative() {
|
|
|
95
128
|
|
|
96
129
|
async function downloadAndVerify() {
|
|
97
130
|
const asset = `jsql-neo-native.${nativeTargetSuffix()}.node`;
|
|
98
|
-
const
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
131
|
+
const raw = `https://github.com/${REPO}/releases/download/${TAG}/${asset}`;
|
|
132
|
+
// 依次尝试:环境变量指定的代理 → 内置加速站 → 直连
|
|
133
|
+
for (const url of proxyCandidates(raw)) {
|
|
134
|
+
console.log(`[jsql-neo] downloading native module: ${url}`);
|
|
135
|
+
try {
|
|
136
|
+
await download(url, NATIVE_PATH);
|
|
137
|
+
if (tryLoadNative()) {
|
|
138
|
+
console.log('[jsql-neo] native module downloaded: native/jsql-neo-native.node');
|
|
139
|
+
return true;
|
|
140
|
+
}
|
|
141
|
+
console.warn('[jsql-neo] downloaded module cannot be loaded; trying next source.');
|
|
142
|
+
} catch (e) {
|
|
143
|
+
console.warn('[jsql-neo] download failed:', e.message);
|
|
105
144
|
}
|
|
106
|
-
console.log('[jsql-neo] native module downloaded: native/jsql-neo-native.node');
|
|
107
|
-
return true;
|
|
108
|
-
} catch (e) {
|
|
109
|
-
console.warn('[jsql-neo] download failed, falling back to source build:', e.message);
|
|
110
|
-
return false;
|
|
111
145
|
}
|
|
146
|
+
return false;
|
|
112
147
|
}
|
|
113
148
|
|
|
114
149
|
async function ensureNativeNode() {
|
|
@@ -202,6 +202,13 @@ async function main() {
|
|
|
202
202
|
ok("nm ~* '^a' 不敏感", JSON.stringify(await rowsOf("SELECT id FROM tbl WHERE nm ~* '^a'")) === '[[1]]');
|
|
203
203
|
ok("nm !~ '^A' 取反", JSON.stringify(await rowsOf("SELECT id FROM tbl WHERE nm !~ '^A'")) === '[[2],[3]]');
|
|
204
204
|
ok('REGEXP 仍可用', JSON.stringify(await rowsOf("SELECT id FROM tbl WHERE nm REGEXP '^A'")) === '[[1]]');
|
|
205
|
+
// —— 6.2.0 回归项(这些曾不报错但结果错)——
|
|
206
|
+
ok('NOT IN 取反', JSON.stringify(await rowsOf('SELECT id FROM tbl WHERE id NOT IN (1,2)')) === '[[3]]');
|
|
207
|
+
const dStar = await rowsOf('SELECT DISTINCT * FROM tbl');
|
|
208
|
+
ok('DISTINCT * 按整行去重(此前只剩 1 行)', dStar.length === 3, dStar);
|
|
209
|
+
ok('ORDER BY 1 按第 1 列排序', JSON.stringify(await rowsOf('SELECT n FROM tbl ORDER BY 1')) === '[[10],[20],[30]]');
|
|
210
|
+
ok("'' 内双写引号转义",
|
|
211
|
+
JSON.stringify(await rowsOf("SELECT 'O''Brien' AS x FROM tbl WHERE id = 1")) === '[["O\'Brien"]]');
|
|
205
212
|
ok("LIKE ... ESCAPE '!' 匹配字面量 %",
|
|
206
213
|
JSON.stringify(await rowsOf("SELECT id FROM tbl WHERE s LIKE '100!%' ESCAPE '!'")) === '[[1]]');
|
|
207
214
|
ok('LIKE 反斜杠转义匹配字面量 %',
|
|
@@ -392,6 +399,34 @@ async function main() {
|
|
|
392
399
|
}
|
|
393
400
|
}
|
|
394
401
|
|
|
402
|
+
console.log('\n--- 6.2.0 回归:JOIN 一对多 / NOW() 用本地时区 ---');
|
|
403
|
+
{
|
|
404
|
+
let JSQL, db;
|
|
405
|
+
try {
|
|
406
|
+
({ JSQL } = require('../lib/native_client.js'));
|
|
407
|
+
db = new JSQL();
|
|
408
|
+
await db.start();
|
|
409
|
+
} catch (e) {
|
|
410
|
+
console.log('[SKIP] native 不可用:' + e.message.slice(0, 60));
|
|
411
|
+
}
|
|
412
|
+
if (db) {
|
|
413
|
+
const run = (sql) => executeSQL(db, sql, OPTS);
|
|
414
|
+
await run('CREATE TABLE ru (id INT PRIMARY KEY, name TEXT)');
|
|
415
|
+
await run('CREATE TABLE ro (id INT PRIMARY KEY, uid INT, amt INT)');
|
|
416
|
+
await run("INSERT INTO ru (id,name) VALUES (1,'x')");
|
|
417
|
+
await run('INSERT INTO ro (id,uid,amt) VALUES (1,1,10),(2,1,20),(3,1,30)');
|
|
418
|
+
const j = await run('SELECT ru.name, ro.amt FROM ru JOIN ro ON ru.id = ro.uid');
|
|
419
|
+
ok('JOIN 一对多返回 3 行', j.rows.length === 3, j.rows);
|
|
420
|
+
|
|
421
|
+
const n = await run('SELECT NOW() AS d');
|
|
422
|
+
const nowLocal = new Date();
|
|
423
|
+
const p = (x) => String(x).padStart(2, '0');
|
|
424
|
+
const expectYmd = nowLocal.getFullYear() + '-' + p(nowLocal.getMonth() + 1) + '-' + p(nowLocal.getDate());
|
|
425
|
+
ok('NOW() 返回本地时区日期', String(n.rows[0][0]).slice(0, 10) === expectYmd, n.rows);
|
|
426
|
+
await db.stop();
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
395
430
|
console.log('\n--- Redis:RESP2 帧解析(曾 inline 命令恒为 unknown command)---');
|
|
396
431
|
{
|
|
397
432
|
const { RedisServer } = require('../lib/redis_server.js');
|