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 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
- case 'NOW': case 'CURRENT_TIMESTAMP': return new Date().toISOString().slice(0, 19).replace('T', ' ');
2526
- case 'CURDATE': case 'CURRENT_DATE': return new Date().toISOString().slice(0, 10);
2527
- case 'CURTIME': return new Date().toISOString().slice(11, 19);
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
- return expr.list.some(x => x === v || String(x) === String(v));
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
- let m = null;
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 (m !== null) {
4655
- matched.push({ ...l, ...rightRows[m] });
4656
- unmatchedRight.delete(m);
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 ordVal = (row, o) => (typeof o.column === 'string'
4781
- ? resolveOperand({ type: 'column', name: o.column }, row, this.ctx)
4782
- : resolveOperand(o.column, row, this.ctx));
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jsql-neo",
3
- "version": "6.0.5",
3
+ "version": "6.2.0",
4
4
  "description": "JSQL-NEO — Rust-powered embedded database with WASM, REST API, B-Tree indexes, WAL, crash recovery",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",
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 加速代理前缀;把原始 github 链接直接拼在后面即可
18
- const GHPROXY = 'https://gh-proxy.com/';
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 url = `${GHPROXY}https://github.com/${REPO}/releases/download/${TAG}/${asset}`;
99
- console.log(`[jsql-neo] downloading native module: ${url}`);
100
- try {
101
- await download(url, NATIVE_PATH);
102
- if (!tryLoadNative()) {
103
- console.warn('[jsql-neo] downloaded module also invalid, removed.');
104
- return false;
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');