jsql-neo 6.1.0 → 6.3.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jsql-neo",
3
- "version": "6.1.0",
3
+ "version": "6.3.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,51 @@ 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
19
+ // 鉴权走 `Authorization: Bearer <token>` 请求头(Worker 从该头读取,不读 ?token=)。
20
+ // 前缀可用 JSQL_GHPROXY 覆盖(逗号分隔多个,按序回退;'' 表示直连兜底);
21
+ // token 可用 JSQL_GHPROXY_TOKEN 覆盖。
22
+ // 加速站候选(按优先级):自建站优先,公共站回退,最后直连兜底。
23
+ const DEFAULT_PROXIES = [
24
+ 'https://gh-proxyjsql-neoworkerapi.vexify.de5.net/',
25
+ 'https://gh-proxy.com/',
26
+ ];
27
+ // 内置 token:让所有安装者都自动走自建加速站,无需配置环境变量。
28
+ // 加速站自身已限制只能代理 /releases/download/ 等文件下载路径,故该值公开无风险。
29
+ const DEFAULT_GHPROXY_TOKEN =
30
+ 'NkUbD165V_1iPvEeGRcAfNEv4cqv5WRLn1NfjgBzIMpo9gq5CBH10Rl5ZPmt7RGCh3kyaZU4Nc6pLhHHbvzc9w';
31
+ const GHPROXY = process.env.JSQL_GHPROXY !== undefined
32
+ ? process.env.JSQL_GHPROXY
33
+ : DEFAULT_PROXIES[0];
34
+ const GHPROXY_TOKEN = process.env.JSQL_GHPROXY_TOKEN || DEFAULT_GHPROXY_TOKEN;
19
35
  const TAG = `v${pkg.version}`;
20
36
 
37
+ /** 加速站鉴权头 */
38
+ function proxyHeaders() {
39
+ return GHPROXY_TOKEN ? { Authorization: 'Bearer ' + GHPROXY_TOKEN } : undefined;
40
+ }
41
+
42
+ /**
43
+ * 生成候选下载地址列表(按优先级):
44
+ * JSQL_GHPROXY 指定的代理 → 内置加速站 → 直连
45
+ */
46
+ function proxyCandidates(raw) {
47
+ const bases = String(GHPROXY || '')
48
+ .split(',')
49
+ .map((s) => s.trim())
50
+ .filter(Boolean)
51
+ .concat(DEFAULT_PROXIES)
52
+ .concat('');
53
+ const list = [];
54
+ for (const base of bases) {
55
+ const url = base ? (base.endsWith('/') ? base : base + '/') + raw : raw;
56
+ if (list.some((x) => x.url === url)) continue;
57
+ list.push({ url, viaProxy: !!base });
58
+ }
59
+ return list;
60
+ }
61
+
21
62
  // 从 repository 字段提取 owner/repo,例如 vexify-org/JSQL-neo
22
63
  const REPO = (pkg.repository && pkg.repository.url)
23
64
  ? pkg.repository.url.replace(/\.git$/, '').replace(/^.*github\.com\//i, '')
@@ -32,13 +73,13 @@ function nativeTargetSuffix(platform = process.platform, arch = process.arch) {
32
73
  return `${p}-${a}-msvc`;
33
74
  }
34
75
 
35
- function download(url, dest) {
76
+ function download(url, dest, headers) {
36
77
  return new Promise((resolve, reject) => {
37
78
  const mod = url.startsWith('https:') ? https : http;
38
- const req = mod.get(url, (res) => {
79
+ const req = mod.get(url, { headers: headers || {} }, (res) => {
39
80
  if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
40
81
  res.resume();
41
- download(res.headers.location, dest).then(resolve, reject);
82
+ download(res.headers.location, dest, headers).then(resolve, reject);
42
83
  return;
43
84
  }
44
85
  if (res.statusCode !== 200) {
@@ -95,20 +136,23 @@ function tryLoadNative() {
95
136
 
96
137
  async function downloadAndVerify() {
97
138
  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;
139
+ const raw = `https://github.com/${REPO}/releases/download/${TAG}/${asset}`;
140
+ // 依次尝试:环境变量指定的代理 → 内置加速站 → 直连
141
+ for (const cand of proxyCandidates(raw)) {
142
+ console.log(`[jsql-neo] downloading native module: ${cand.url}`);
143
+ try {
144
+ // 加速站鉴权走 Authorization 头,直连时不带
145
+ await download(cand.url, NATIVE_PATH, cand.viaProxy ? proxyHeaders() : undefined);
146
+ if (tryLoadNative()) {
147
+ console.log('[jsql-neo] native module downloaded: native/jsql-neo-native.node');
148
+ return true;
149
+ }
150
+ console.warn('[jsql-neo] downloaded module cannot be loaded; trying next source.');
151
+ } catch (e) {
152
+ console.warn('[jsql-neo] download failed:', e.message);
105
153
  }
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
154
  }
155
+ return false;
112
156
  }
113
157
 
114
158
  async function ensureNativeNode() {
@@ -142,16 +142,16 @@ async function main() {
142
142
  console.log('\n--- 方言特性:DIV / XOR / && / || / <=> / :: / -> ->> / LIKE ESCAPE ---');
143
143
  {
144
144
  const data = [
145
- { id: 1, n: 10, flag: 1, s: '100%', meta: '{"name":"Alice","addr":{"city":"SH"},"tags":["x","y"]}' },
146
- { id: 2, n: 20, flag: 0, s: '1000', meta: '{"name":"Bob","addr":{"city":"BJ"},"tags":[]}' },
147
- { id: 3, n: 30, flag: 1, s: '100', meta: null },
145
+ { id: 1, n: 10, flag: 1, s: '100%', nm: 'Alice', meta: '{"name":"Alice","addr":{"city":"SH"},"tags":["x","y"]}' },
146
+ { id: 2, n: 20, flag: 0, s: '1000', nm: 'Bob', meta: '{"name":"Bob","addr":{"city":"BJ"},"tags":[]}' },
147
+ { id: 3, n: 30, flag: 1, s: '100', nm: 'Cara', meta: null },
148
148
  ];
149
149
  const engine = {
150
150
  hasTable: () => true, truncate: async () => {}, flush: async () => {},
151
151
  find: async () => data.map(r => ({ ...r })),
152
152
  getTableSchema: async () => ({
153
153
  id: { type: 'integer', primaryKey: true }, n: { type: 'integer' }, flag: { type: 'integer' },
154
- s: { type: 'string' }, meta: { type: 'json' },
154
+ s: { type: 'string' }, nm: { type: 'string' }, meta: { type: 'json' },
155
155
  }),
156
156
  };
157
157
  const rowsOf = async (sql) => {
@@ -176,12 +176,98 @@ async function main() {
176
176
  JSON.stringify(await rowsOf("SELECT meta->'addr'->>'city' AS x FROM tbl WHERE id = 1")) === '[["SH"]]');
177
177
  ok("->'tags'->>0 数组下标",
178
178
  JSON.stringify(await rowsOf("SELECT meta->'tags'->>0 AS x FROM tbl WHERE id = 1")) === '[["x"]]');
179
+ // PG JSON 包含 / 路径操作符
180
+ ok("meta @> '{...}' 包含",
181
+ JSON.stringify(await rowsOf(`SELECT id FROM tbl WHERE meta @> '{"addr":{"city":"SH"}}'`)) === '[[1]]');
182
+ ok("'{...}' <@ meta 被包含",
183
+ JSON.stringify(await rowsOf(`SELECT id FROM tbl WHERE '{"name":"Bob"}' <@ meta`)) === '[[2]]');
184
+ ok("meta#>'{addr,city}' 路径",
185
+ JSON.stringify(await rowsOf("SELECT meta#>'{addr,city}' AS x FROM tbl WHERE id = 1")) === '[["SH"]]');
186
+ ok("meta#>>'{addr,city}' 路径文本",
187
+ JSON.stringify(await rowsOf("SELECT meta#>>'{addr,city}' AS x FROM tbl WHERE id = 1")) === '[["SH"]]');
188
+ ok("meta#>'{tags,0}' 路径含下标",
189
+ JSON.stringify(await rowsOf("SELECT meta#>'{tags,0}' AS x FROM tbl WHERE id = 1")) === '[["x"]]');
190
+ // JSON 键存在 ? / ?| / ?&
191
+ ok("meta ? 'name'", JSON.stringify(await rowsOf("SELECT id FROM tbl WHERE meta ? 'name'")) === '[[1],[2]]');
192
+ ok("meta ? 'nope'(不存在)",
193
+ JSON.stringify(await rowsOf("SELECT id FROM tbl WHERE meta ? 'nope'")) === '[]');
194
+ ok("meta ?| '{name,nope}'(任一)",
195
+ JSON.stringify(await rowsOf("SELECT id FROM tbl WHERE meta ?| '{name,nope}'")) === '[[1],[2]]');
196
+ ok("meta ?& '{name,addr}'(全部)",
197
+ JSON.stringify(await rowsOf("SELECT id FROM tbl WHERE meta ?& '{name,addr}'")) === '[[1],[2]]');
198
+ ok("meta ?& '{name,nope}'(缺一个)",
199
+ JSON.stringify(await rowsOf("SELECT id FROM tbl WHERE meta ?& '{name,nope}'")) === '[]');
200
+ // PG 正则:~ 敏感 / ~* 不敏感 / !~ 取反
201
+ ok("nm ~ '^A' 大小写敏感", JSON.stringify(await rowsOf("SELECT id FROM tbl WHERE nm ~ '^A'")) === '[[1]]');
202
+ ok("nm ~* '^a' 不敏感", JSON.stringify(await rowsOf("SELECT id FROM tbl WHERE nm ~* '^a'")) === '[[1]]');
203
+ ok("nm !~ '^A' 取反", JSON.stringify(await rowsOf("SELECT id FROM tbl WHERE nm !~ '^A'")) === '[[2],[3]]');
204
+ ok('REGEXP 仍可用', JSON.stringify(await rowsOf("SELECT id FROM tbl WHERE nm REGEXP '^A'")) === '[[1]]');
205
+ // —— 日期/时间函数(README 承诺但此前大量缺失、静默返回 null)——
206
+ ok('YEAR()', JSON.stringify(await rowsOf("SELECT YEAR('2024-03-15')")) === '[[2024]]');
207
+ ok('MONTH()', JSON.stringify(await rowsOf("SELECT MONTH('2024-03-15')")) === '[[3]]');
208
+ ok('DAY()', JSON.stringify(await rowsOf("SELECT DAY('2024-03-15')")) === '[[15]]');
209
+ ok('HOUR()', JSON.stringify(await rowsOf("SELECT HOUR('2024-03-15 10:20:30')")) === '[[10]]');
210
+ ok('MINUTE()', JSON.stringify(await rowsOf("SELECT MINUTE('2024-03-15 10:20:30')")) === '[[20]]');
211
+ ok('SECOND()', JSON.stringify(await rowsOf("SELECT SECOND('2024-03-15 10:20:30')")) === '[[30]]');
212
+ ok('QUARTER()', JSON.stringify(await rowsOf("SELECT QUARTER('2024-08-15')")) === '[[3]]');
213
+ ok('DAYOFWEEK()', JSON.stringify(await rowsOf("SELECT DAYOFWEEK('2024-03-15')")) === '[[6]]');
214
+ ok('DATE()', JSON.stringify(await rowsOf("SELECT DATE('2024-03-15 10:20:30')")) === '[["2024-03-15"]]');
215
+ ok('TIME()', JSON.stringify(await rowsOf("SELECT TIME('2024-03-15 10:20:30')")) === '[["10:20:30"]]');
216
+ ok('DATEDIFF', JSON.stringify(await rowsOf("SELECT DATEDIFF('2026-08-12','2026-08-01')")) === '[[11]]');
217
+ ok('TIMESTAMPDIFF 裸 unit',
218
+ JSON.stringify(await rowsOf("SELECT TIMESTAMPDIFF(MONTH,'2024-01-01','2024-03-01')")) === '[[2]]');
219
+ ok('DATE_FORMAT',
220
+ JSON.stringify(await rowsOf("SELECT DATE_FORMAT('2026-08-12 10:30:00','%Y-%m-%d %H:%i')")) === '[["2026-08-12 10:30"]]');
221
+ ok('STR_TO_DATE',
222
+ JSON.stringify(await rowsOf("SELECT STR_TO_DATE('2024-03-15','%Y-%m-%d')")) === '[["2024-03-15"]]');
223
+ ok('TO_DATE(PG 格式)',
224
+ JSON.stringify(await rowsOf("SELECT TO_DATE('2024-03-15','YYYY-MM-DD')")) === '[["2024-03-15"]]');
225
+ ok('TO_CHAR(PG 格式)',
226
+ JSON.stringify(await rowsOf("SELECT TO_CHAR('2024-03-15','YYYY-MM-DD')")) === '[["2024-03-15"]]');
227
+ ok('DATE_PART', JSON.stringify(await rowsOf("SELECT DATE_PART('year','2024-03-15')")) === '[[2024]]');
228
+ ok('AGE', JSON.stringify(await rowsOf("SELECT AGE('2026-08-12','2020-01-01')")) === '[["6 years 7 mons 11 days"]]');
229
+ ok('LAST_DAY 闰年', JSON.stringify(await rowsOf("SELECT LAST_DAY('2024-02-05')")) === '[["2024-02-29"]]');
230
+ ok('DAYNAME', JSON.stringify(await rowsOf("SELECT DAYNAME('2024-03-15')")) === '[["Friday"]]');
231
+ ok('MONTHNAME', JSON.stringify(await rowsOf("SELECT MONTHNAME('2024-03-15')")) === '[["March"]]');
232
+ ok('PERIOD_DIFF', JSON.stringify(await rowsOf('SELECT PERIOD_DIFF(202403,202401)')) === '[[2]]');
233
+ ok('DATE_TRUNC',
234
+ JSON.stringify(await rowsOf("SELECT DATE_TRUNC('month','2024-03-15 10:20:30')")) === '[["2024-03-01 00:00:00"]]');
235
+ ok('WEEK()', JSON.stringify(await rowsOf("SELECT WEEK('2024-01-14')")) === '[[2]]');
236
+
237
+ // —— 数值比较 / 聚合 / LIKE:曾"不报错但结果错"——
238
+ ok('数值列 vs 数值字符串 <', JSON.stringify(await rowsOf("SELECT id FROM tbl WHERE n < '9'")) === '[]');
239
+ ok('数值列 vs 数值字符串 =', JSON.stringify(await rowsOf("SELECT id FROM tbl WHERE n = '10'")) === '[[1]]');
240
+ ok('HAVING 无 GROUP BY', JSON.stringify(await rowsOf('SELECT COUNT(*) AS c FROM tbl HAVING COUNT(*) > 2')) === '[[3]]');
241
+ ok('HAVING 不成立应为空', JSON.stringify(await rowsOf('SELECT COUNT(*) AS c FROM tbl HAVING COUNT(*) > 100')) === '[]');
242
+ ok('MIN 字符串列', JSON.stringify(await rowsOf('SELECT MIN(nm) FROM tbl')) === '[["Alice"]]');
243
+ ok('MAX 字符串列', JSON.stringify(await rowsOf('SELECT MAX(nm) FROM tbl')) === '[["Cara"]]');
244
+ ok('LIKE 默认不区分大小写', JSON.stringify(await rowsOf("SELECT id FROM tbl WHERE nm LIKE 'alice'")) === '[[1]]');
245
+ ok('LIKE BINARY 区分大小写', JSON.stringify(await rowsOf("SELECT id FROM tbl WHERE nm LIKE BINARY 'alice'")) === '[]');
246
+
247
+ // —— 6.2.0 回归项(这些曾不报错但结果错)——
248
+ ok('NOT IN 取反', JSON.stringify(await rowsOf('SELECT id FROM tbl WHERE id NOT IN (1,2)')) === '[[3]]');
249
+ const dStar = await rowsOf('SELECT DISTINCT * FROM tbl');
250
+ ok('DISTINCT * 按整行去重(此前只剩 1 行)', dStar.length === 3, dStar);
251
+ ok('ORDER BY 1 按第 1 列排序', JSON.stringify(await rowsOf('SELECT n FROM tbl ORDER BY 1')) === '[[10],[20],[30]]');
252
+ ok("'' 内双写引号转义",
253
+ JSON.stringify(await rowsOf("SELECT 'O''Brien' AS x FROM tbl WHERE id = 1")) === '[["O\'Brien"]]');
179
254
  ok("LIKE ... ESCAPE '!' 匹配字面量 %",
180
255
  JSON.stringify(await rowsOf("SELECT id FROM tbl WHERE s LIKE '100!%' ESCAPE '!'")) === '[[1]]');
181
256
  ok('LIKE 反斜杠转义匹配字面量 %',
182
257
  JSON.stringify(await rowsOf("SELECT id FROM tbl WHERE s LIKE '100\\%'")) === '[[1]]');
183
258
  ok('LIKE 普通 % 仍为通配',
184
259
  JSON.stringify(await rowsOf("SELECT id FROM tbl WHERE s LIKE '100%'")) === '[[1],[2],[3]]');
260
+
261
+ // 参数绑定:位置 ? / 命名 :name / 编号 $1(README「Params」一节)
262
+ const rowsOfP = async (sql, params) => {
263
+ const r = await executeSQL(engine, sql, params, OPTS);
264
+ return (Array.isArray(r) ? r[0] : r).rows;
265
+ };
266
+ ok('位置参数 ?', JSON.stringify(await rowsOfP('SELECT id FROM tbl WHERE id = ?', [1])) === '[[1]]');
267
+ ok('命名参数 :id', JSON.stringify(await rowsOfP('SELECT id FROM tbl WHERE id = :id', { id: 2 })) === '[[2]]');
268
+ ok('命名参数多占位', JSON.stringify(await rowsOfP('SELECT id FROM tbl WHERE id = :a OR id = :b', { a: 1, b: 3 })) === '[[1],[3]]');
269
+ ok('编号参数 $1', JSON.stringify(await rowsOfP('SELECT id FROM tbl WHERE id = $1', [3])) === '[[3]]');
270
+ ok('编号参数 $2/$1', JSON.stringify(await rowsOfP('SELECT id FROM tbl WHERE id = $2 OR id = $1', [1, 2])) === '[[1],[2]]');
185
271
  }
186
272
 
187
273
  console.log('\n--- DDL / 解析器节点覆盖 ---');
@@ -325,6 +411,102 @@ async function main() {
325
411
  }
326
412
  }
327
413
 
414
+ console.log('\n--- 唯一约束:非主键 UNIQUE 必须报 ER_DUP_ENTRY ---');
415
+ {
416
+ let JSQL, db;
417
+ try {
418
+ ({ JSQL } = require('../lib/native_client.js'));
419
+ db = new JSQL();
420
+ await db.start();
421
+ } catch (e) {
422
+ console.log('[SKIP] native 不可用:' + e.message.slice(0, 60));
423
+ }
424
+ if (db) {
425
+ const run = (sql) => executeSQL(db, sql, OPTS);
426
+ const dupErrMsg = async (sql) => {
427
+ try { await run(sql); return null; } catch (e) { return e.message; }
428
+ };
429
+ await run('CREATE TABLE u (id SERIAL PRIMARY KEY, email TEXT UNIQUE, nick TEXT UNIQUE)');
430
+ await run("INSERT INTO u (email, nick) VALUES ('a@x.com', 'a')");
431
+ ok('唯一列重复 → ER_DUP_ENTRY',
432
+ /ER_DUP_ENTRY/.test(await dupErrMsg("INSERT INTO u (email, nick) VALUES ('a@x.com', 'z')") || ''));
433
+ ok('批内唯一重复 → ER_DUP_ENTRY',
434
+ /ER_DUP_ENTRY/.test(await dupErrMsg("INSERT INTO u (email, nick) VALUES ('b@x.com', 'n1'), ('c@x.com', 'n1')") || ''));
435
+ const r1 = await run('SELECT id FROM u');
436
+ ok('冲突后仍只有 1 行', r1.rows.length === 1, r1.rows);
437
+ await run("INSERT INTO u (email, nick) VALUES (NULL, 'x'), (NULL, 'y')");
438
+ const r2 = await run('SELECT id FROM u');
439
+ ok('UNIQUE 允许多个 NULL', r2.rows.length === 3, r2.rows);
440
+ await db.stop();
441
+ }
442
+ }
443
+
444
+ console.log('\n--- 6.2.0 回归:JOIN 一对多 / NOW() 用本地时区 ---');
445
+ {
446
+ let JSQL, db;
447
+ try {
448
+ ({ JSQL } = require('../lib/native_client.js'));
449
+ db = new JSQL();
450
+ await db.start();
451
+ } catch (e) {
452
+ console.log('[SKIP] native 不可用:' + e.message.slice(0, 60));
453
+ }
454
+ if (db) {
455
+ const run = (sql) => executeSQL(db, sql, OPTS);
456
+ await run('CREATE TABLE ru (id INT PRIMARY KEY, name TEXT)');
457
+ await run('CREATE TABLE ro (id INT PRIMARY KEY, uid INT, amt INT)');
458
+ await run("INSERT INTO ru (id,name) VALUES (1,'x')");
459
+ await run('INSERT INTO ro (id,uid,amt) VALUES (1,1,10),(2,1,20),(3,1,30)');
460
+ const j = await run('SELECT ru.name, ro.amt FROM ru JOIN ro ON ru.id = ro.uid');
461
+ ok('JOIN 一对多返回 3 行', j.rows.length === 3, j.rows);
462
+
463
+ const n = await run('SELECT NOW() AS d');
464
+ const nowLocal = new Date();
465
+ const p = (x) => String(x).padStart(2, '0');
466
+ const expectYmd = nowLocal.getFullYear() + '-' + p(nowLocal.getMonth() + 1) + '-' + p(nowLocal.getDate());
467
+ ok('NOW() 返回本地时区日期', String(n.rows[0][0]).slice(0, 10) === expectYmd, n.rows);
468
+ await db.stop();
469
+ }
470
+ }
471
+
472
+ console.log('\n--- NULL 三值逻辑 / 相关子查询 / DATE 列时区 ---');
473
+ {
474
+ let JSQL, db;
475
+ try {
476
+ ({ JSQL } = require('../lib/native_client.js'));
477
+ db = new JSQL();
478
+ await db.start();
479
+ } catch (e) {
480
+ console.log('[SKIP] native 不可用:' + e.message.slice(0, 60));
481
+ }
482
+ if (db) {
483
+ const run = (sql) => executeSQL(db, sql, OPTS);
484
+ // NOT 作用于含 NULL 的比较:UNKNOWN 取反仍是 UNKNOWN,该行不应入选
485
+ await run('CREATE TABLE nl (id INT PRIMARY KEY, a INT)');
486
+ await run('INSERT INTO nl (id,a) VALUES (1,1),(2,2),(3,NULL)');
487
+ ok('NOT (a > 1) 排除 NULL 行', JSON.stringify((await run('SELECT id FROM nl WHERE NOT (a > 1)')).rows) === '[[1]]');
488
+
489
+ // 相关子查询引用外层别名
490
+ await run('CREATE TABLE ta (id INT PRIMARY KEY, v INT)');
491
+ await run('CREATE TABLE tb (id INT PRIMARY KEY, av INT)');
492
+ await run('INSERT INTO ta (id,v) VALUES (1,10),(2,20)');
493
+ await run('INSERT INTO tb (id,av) VALUES (1,10)');
494
+ ok('EXISTS 相关子查询',
495
+ JSON.stringify((await run('SELECT id FROM ta WHERE EXISTS (SELECT 1 FROM tb WHERE tb.av = ta.v)')).rows) === '[[1]]');
496
+ ok('NOT EXISTS 相关子查询',
497
+ JSON.stringify((await run('SELECT id FROM ta WHERE NOT EXISTS (SELECT 1 FROM tb WHERE tb.av = ta.v)')).rows) === '[[2]]');
498
+
499
+ // DATE 列序列化:不能用 toISOString(UTC),否则东八区差一天
500
+ const { validateDateType } = require('../lib/date-types.js');
501
+ ok('DATE 字符串不回退一天', validateDateType('2023-01-01', 'date', 'f') === '2023-01-01');
502
+ ok('DATE 闰年边界(2024-02-29)', validateDateType('2024-02-29', 'date', 'f') === '2024-02-29');
503
+ ok('DATE 对象按本地时区', validateDateType(new Date(2023, 0, 1), 'date', 'f') === '2023-01-01');
504
+ ok('DATETIME 对象按本地时区',
505
+ validateDateType(new Date(2023, 0, 1, 8, 30), 'datetime', 'f') === '2023-01-01 08:30:00');
506
+ await db.stop();
507
+ }
508
+ }
509
+
328
510
  console.log('\n--- Redis:RESP2 帧解析(曾 inline 命令恒为 unknown command)---');
329
511
  {
330
512
  const { RedisServer } = require('../lib/redis_server.js');