jsql-neo 6.5.26 → 6.5.28

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/postinstall.js CHANGED
@@ -14,7 +14,11 @@ const BIN_NAME = process.platform === 'win32' ? 'jsql-neo-server.exe' : 'jsql-ne
14
14
  const BP = path.join(ROOT, 'bin', BIN_NAME);
15
15
 
16
16
  // Native N-API module (native/jsql-neo-native.node)
17
- const NATIVE_PATH = path.join(ROOT, 'native', 'jsql-neo-native.node');
17
+ // 允许测试/自定义安装把 native 放到别处(JSQL_NATIVE_PATH 为绝对路径)。
18
+ // 默认仍是包内 native/ 目录;META_PATH 会跟着 dirname 一起走。
19
+ const NATIVE_PATH = process.env.JSQL_NATIVE_PATH
20
+ ? path.resolve(process.env.JSQL_NATIVE_PATH)
21
+ : path.join(ROOT, 'native', 'jsql-neo-native.node');
18
22
 
19
23
  // GitHub 加速代理前缀(可选)。加速站的用法是把原始 URL 直接拼在后面:
20
24
  // https://<host>/https://github.com/owner/repo/releases/download/vX.Y.Z/asset
@@ -357,10 +361,36 @@ function installDownloadedModule(tmp) {
357
361
  try {
358
362
  fs.renameSync(tmp, NATIVE_PATH);
359
363
  } catch (e) {
360
- // Windows 上 rename 覆盖已存在文件会 EEXIST/EPERM
364
+ // Windows 上 rename 覆盖已存在文件会 EEXIST/EPERM。
365
+ //
366
+ // 旧实现在这里 unlinkSync(旧文件) 并把失败 catch(_) 吞掉,然后第二次
367
+ // rename 必然再失败 —— 结果是「下载成功 + 校验通过」却静默留着旧二进制,
368
+ // 上层只看到一句轻描淡写的 "keeping the existing local native module"。
369
+ // .node 是 DLL,被加载过后 unlink 常常 EPERM,但把它 rename 挪开是可行的
370
+ // (实测 mv 能成功而 unlink 不能),所以改为「挪走 → 装新的」。
361
371
  if (e.code === 'EEXIST' || e.code === 'EPERM' || e.code === 'EACCES') {
362
- try { fs.unlinkSync(NATIVE_PATH); } catch (_) { /* ignore */ }
363
- fs.renameSync(tmp, NATIVE_PATH);
372
+ const parked = `${NATIVE_PATH}.${process.pid}.old`;
373
+ try {
374
+ fs.renameSync(NATIVE_PATH, parked);
375
+ } catch (moveErr) {
376
+ try { fs.unlinkSync(NATIVE_PATH); }
377
+ catch (unlinkErr) {
378
+ // 两条路都不通就把真实原因抛出去,绝不再静默降级
379
+ throw new Error(
380
+ `cannot replace ${NATIVE_PATH}: rename=${e.code}, move=${moveErr.code}, unlink=${unlinkErr.code}`
381
+ );
382
+ }
383
+ }
384
+ try {
385
+ fs.renameSync(tmp, NATIVE_PATH);
386
+ } catch (installErr) {
387
+ // 装不回去必须把旧文件还原,别让本机变成「完全没有 native」
388
+ if (fs.existsSync(parked)) {
389
+ try { fs.renameSync(parked, NATIVE_PATH); } catch (_) { /* 外层还有 backup 兜底 */ }
390
+ }
391
+ throw installErr;
392
+ }
393
+ try { if (fs.existsSync(parked)) fs.unlinkSync(parked); } catch (_) { /* 清理失败不影响安装 */ }
364
394
  } else {
365
395
  throw e;
366
396
  }
@@ -438,6 +468,7 @@ async function downloadAndVerify(opts = {}) {
438
468
  if (installed.backup) {
439
469
  try { fs.unlinkSync(installed.backup); } catch (_) { /* ignore */ }
440
470
  }
471
+ writeInstalledMeta(tag, sha256File(NATIVE_PATH));
441
472
  console.log(`[jsql-neo] native module updated: native/jsql-neo-native.node (${tag})`);
442
473
  return true;
443
474
  }
@@ -497,13 +528,108 @@ async function ensureNativeNode() {
497
528
 
498
529
  // 拉取失败:本地可用就继续用本地 —— 网络问题不该把已经能跑的安装搞坏
499
530
  if (tryLoadNative()) {
500
- console.warn('[jsql-neo] remote refresh failed; keeping the existing local native module.');
531
+ console.warn('[jsql-neo] WARNING: remote refresh FAILED — keeping the EXISTING native module.');
532
+ console.warn('[jsql-neo] It may NOT match this package version, which can silently break native features.');
533
+ console.warn('[jsql-neo] Run "node postinstall.js --status" to check for a version mismatch.');
501
534
  return;
502
535
  }
503
536
  console.warn('[jsql-neo] could not fetch a verified native module; falling back to source build / WASM.');
504
537
  buildNativeFromSource();
505
538
  }
506
539
 
540
+ /**
541
+ * native 安装状态自检。
542
+ *
543
+ * 背景:postinstall 之前在 Windows 上会「下载成功、校验通过、装不上」,
544
+ * 却只留一句 keeping the existing local native module —— 用户无从察觉
545
+ * 自己其实还在用旧二进制(issue-38 的 bigint 用例就是这么一直挂的)。
546
+ *
547
+ * 现在每次成功安装都落一份 .installed.json,`node postinstall.js --status`
548
+ * 能直接回答「当前 .node 是不是这个包版本该用的那个」,也可当 CI 门禁。
549
+ */
550
+ const META_PATH = path.join(path.dirname(NATIVE_PATH), '.installed.json');
551
+
552
+ function writeInstalledMeta(tag, sha256) {
553
+ try {
554
+ fs.writeFileSync(META_PATH, JSON.stringify({
555
+ tag,
556
+ sha256: sha256 || null,
557
+ packageVersion: pkg.version,
558
+ installedAt: new Date().toISOString(),
559
+ platform: process.platform,
560
+ arch: process.arch,
561
+ }, null, 2) + '\n');
562
+ } catch (_) { /* 元数据写不了不该让安装失败 */ }
563
+ }
564
+
565
+ function readInstalledMeta() {
566
+ try { return JSON.parse(fs.readFileSync(META_PATH, 'utf8')); }
567
+ catch (_) { return null; }
568
+ }
569
+
570
+ /** 返回 0 = 匹配,1 = 不匹配 / 未记录(可直接用于 CI 门禁)。 */
571
+ function nativeStatus() {
572
+ const expected = `v${pkg.version}`;
573
+ const meta = readInstalledMeta();
574
+ const exists = fs.existsSync(NATIVE_PATH);
575
+ const size = exists ? fs.statSync(NATIVE_PATH).size : 0;
576
+ const out = [
577
+ `[jsql-neo] package version : ${pkg.version} (expected native tag ${expected})`,
578
+ `[jsql-neo] native module : ${exists ? `present (${size} bytes)` : 'MISSING'}`,
579
+ ];
580
+ if (meta) {
581
+ out.push(`[jsql-neo] installed tag : ${meta.tag} (${meta.installedAt})`);
582
+ out.push(`[jsql-neo] installed sha256: ${meta.sha256 || 'n/a'}`);
583
+ } else {
584
+ out.push('[jsql-neo] installed tag : UNKNOWN (no .installed.json — installed by an older build)');
585
+ }
586
+ const match = !!meta && meta.tag === expected && exists;
587
+ const state = match ? 'OK' : (meta && meta.tag !== expected) ? 'MISMATCH' : 'UNVERIFIED';
588
+ out.push(`[jsql-neo] STATUS : ${state}`);
589
+ if (!match) out.push('[jsql-neo] refresh with: node postinstall.js (or npm rebuild jsql-neo)');
590
+ console.log(out.join('\n'));
591
+ return match ? 0 : 1;
592
+ }
593
+
594
+ /**
595
+ * 内容级校验。
596
+ *
597
+ * nativeStatus 只读 .installed.json 里记录的 tag —— 如果 .node 文件被替换、
598
+ * 损坏,或被人换成了别的版本但元数据没动,status 依然会报 OK。这里直接
599
+ * 重算当前文件的 SHA256 与安装时记录的比对,回答「磁盘上这个文件是不是
600
+ * 当初装进来的那个」。
601
+ *
602
+ * 返回结构化对象,便于 CLI 输出 JSON 给 CI 消费。
603
+ */
604
+ function verifyNative() {
605
+ const expected = `v${pkg.version}`;
606
+ const meta = readInstalledMeta();
607
+ const exists = fs.existsSync(NATIVE_PATH);
608
+ const r = {
609
+ packageVersion: pkg.version,
610
+ expectedTag: expected,
611
+ installedTag: meta ? meta.tag : null,
612
+ present: exists,
613
+ size: exists ? fs.statSync(NATIVE_PATH).size : 0,
614
+ expectedSha256: meta ? (meta.sha256 || null) : null,
615
+ actualSha256: null,
616
+ state: 'UNVERIFIED',
617
+ };
618
+ if (!exists) { r.state = 'MISSING'; return r; }
619
+ try {
620
+ r.actualSha256 = sha256File(NATIVE_PATH);
621
+ } catch (e) {
622
+ r.state = 'UNREADABLE';
623
+ r.error = e.message;
624
+ return r;
625
+ }
626
+ if (!r.expectedSha256) return r; // 老安装没有校验和
627
+ if (r.actualSha256 !== r.expectedSha256) { r.state = 'CORRUPT'; return r; }
628
+ if (r.installedTag !== expected) { r.state = 'MISMATCH'; return r; }
629
+ r.state = 'OK';
630
+ return r;
631
+ }
632
+
507
633
  function ensureServerBinary() {
508
634
  if (fs.existsSync(BP)) {
509
635
  fs.chmodSync(BP, 0o755);
@@ -541,7 +667,8 @@ async function main() {
541
667
 
542
668
  // 仅当直接执行时才运行(build:native / 测试可 require 本文件复用构建逻辑)
543
669
  if (require.main === module) {
544
- main();
670
+ if (process.argv.includes('--status')) process.exitCode = nativeStatus();
671
+ else main();
545
672
  }
546
673
 
547
674
  module.exports = {
@@ -551,4 +678,7 @@ module.exports = {
551
678
  releaseTag,
552
679
  resolveLatestTag,
553
680
  installDownloadedModule,
681
+ nativeStatus,
682
+ verifyNative,
683
+ readInstalledMeta,
554
684
  };
package/test/coverage.js CHANGED
@@ -166,7 +166,9 @@ function tmp(name) {
166
166
  ok('sql rollback', (await db.count('users')) === 1);
167
167
  await executeSQL(db, 'START TRANSACTION');
168
168
  await executeSQL(db, "INSERT INTO users (name, age) VALUES ('X', 1)");
169
- await executeSQL(db, 'TRUNCATE TABLE users');
169
+ // TRUNCATE 被 findDestructiveSQL 判为破坏性语句,默认 safety 会拦。
170
+ // 这里就是要验证该功能,显式声明而不是放宽全局策略;断言保持不变。
171
+ await executeSQL(db, 'TRUNCATE TABLE users', { safety: false });
170
172
  ok('sql truncate', (await db.count('users')) === 0);
171
173
  const sqlApi = require(path.join(ROOT, 'lib/sql'));
172
174
  const parsed = sqlApi.parseSQL('SELECT * FROM t WHERE id = 5');
@@ -0,0 +1,55 @@
1
+ /* ---- finalizeResultBigInts:性能优化不能丢 BigInt 语义 ---- */
2
+ /*
3
+ * 优化前 executeSQL 用通用 finalizeBigInts 对结果深递归;优化后改成针对
4
+ * rows(二维) / raw(对象数组) 的紧凑循环,其余字段仍走通用实现。
5
+ * 这里钉死「>2^53 的值在 rows 和 raw 里都必须是可序列化的字符串」,
6
+ * 以及小整数仍是 number、结果整体可 JSON.stringify。
7
+ */
8
+ const path = require('path');
9
+ const ROOT = path.join(__dirname, '..');
10
+ const { Database } = require(path.join(ROOT, 'index.js'));
11
+
12
+ let pass = 0, fail = 0;
13
+ function ok(name, cond, got) {
14
+ if (cond) { pass++; console.log('[OK] ' + name); }
15
+ else { fail++; console.log('[FAIL] ' + name + ' -> ' + JSON.stringify(got, (k, v) => typeof v === 'bigint' ? v.toString() + 'n' : v)); }
16
+ }
17
+
18
+ (async () => {
19
+ const db = new Database(':memory:', { autoSave: false });
20
+ await db.createTable('t', { id: { type: 'integer', primaryKey: true, autoIncrement: true }, n: { type: 'integer' } });
21
+ await db.insert('t', [{ n: 1 }, { n: 2 }, { n: 3 }]);
22
+
23
+ const r = await db.query('SELECT 9007199254740993 AS big, 1 AS small');
24
+ ok('rows 里 >2^53 的 BigInt 已转字符串',
25
+ r.rows[0][0] === '9007199254740993' && typeof r.rows[0][0] === 'string', r.rows);
26
+ ok('raw 里 >2^53 的 BigInt 已转字符串',
27
+ r.raw[0].big === '9007199254740993' && typeof r.raw[0].big === 'string', r.raw);
28
+ ok('小整数仍是 number(没被无差别转字符串)',
29
+ r.rows[0][1] === 1 && r.raw[0].small === 1, r);
30
+ ok('结果整体可 JSON.stringify(BigInt 会抛错)',
31
+ (() => { try { JSON.stringify(r); return true; } catch (e) { return false; } })(), r);
32
+
33
+ const huge = await db.query('SELECT 9223372036854775807 AS huge');
34
+ ok('int64 最大值同样转字符串', huge.rows[0][0] === '9223372036854775807', huge.rows);
35
+
36
+ // 多行:rows 每一行、raw 每个对象都要处理到,不能只处理第一行
37
+ const many = await db.query('SELECT id, 9007199254740993 AS big FROM t');
38
+ ok('多行 rows 全部转换',
39
+ many.rows.length === 3 && many.rows.every((row) => row[1] === '9007199254740993'), many.rows);
40
+ ok('多行 raw 全部转换',
41
+ many.raw.length === 3 && many.raw.every((o) => o.big === '9007199254740993'), many.raw);
42
+ ok('多行场景下 id 仍是 number',
43
+ many.rows.every((row) => typeof row[0] === 'number'), many.rows);
44
+
45
+ // 其余字段走通用实现:columns / 元数据不能被跳过
46
+ const plain = await db.query('SELECT id FROM t WHERE id = 1');
47
+ ok('columns 等其余字段保留',
48
+ Array.isArray(plain.columns) && plain.columns[0] === 'id' && plain.rowCount === 1, plain);
49
+
50
+ console.log(`\n${pass} passed, ${fail} failed`);
51
+ process.exit(fail === 0 ? 0 : 1);
52
+ })().catch(e => {
53
+ console.log('[FAIL] unexpected exception: ' + e.message);
54
+ process.exit(1);
55
+ });
@@ -0,0 +1,92 @@
1
+ /*
2
+ * Issue #44 — 索引等值查找忽略类型归一:字符串查询值在数值列上静默返回空。
3
+ *
4
+ * 复现(v6.5.26):
5
+ * db.find('u', { id: '5' }) -> [] (id 是 INT PRIMARY KEY,B-Tree)
6
+ * db.findByIds('u', ['5']) -> []
7
+ * Mongo insert 的 _id 唯一性检查走同一套索引查找,也会被绕过。
8
+ *
9
+ * 修复:查询值先按列声明类型归一('5' → 5),索引未命中再回落全表过滤。
10
+ */
11
+ const path = require('path');
12
+
13
+ const ROOT = path.join(__dirname, '..');
14
+ const { Database, createMongoServer } = require(path.join(ROOT, 'index.js'));
15
+
16
+ let pass = 0, fail = 0;
17
+ function ok(name, cond, extra) {
18
+ if (cond) { pass++; console.log('[OK] ', name); }
19
+ else { fail++; console.log('[FAIL]', name, extra !== undefined ? '-> ' + JSON.stringify(extra).slice(0, 300) : ''); }
20
+ }
21
+
22
+ (async () => {
23
+ try {
24
+ const db = new Database(':memory:');
25
+ await db.createTable('u', {
26
+ id: 'INT PRIMARY KEY',
27
+ name: 'TEXT',
28
+ code: 'INT UNIQUE',
29
+ score: 'FLOAT',
30
+ active: 'BOOLEAN',
31
+ });
32
+ await db.insert('u', { id: 5, name: 'a', code: 100, score: 1.5, active: true });
33
+ await db.insert('u', { id: 6, name: 'b', code: 200, score: 2.5, active: false });
34
+
35
+ // --- B-Tree:UNIQUE 非主键列 ---
36
+ const byCode = await db.find('u', { code: '100' });
37
+ ok('#44 btree 唯一列:字符串查数值命中', byCode.length === 1 && byCode[0].id === 5, byCode);
38
+
39
+ // --- B-Tree:主键列 + $in ---
40
+ const byIn = await db.find('u', { id: { $in: ['5', '6'] } });
41
+ ok('#44 btree 主键 $in:字符串列表命中', byIn.length === 2, byIn.length);
42
+
43
+ // --- 主键 btree:findById / findByIds / updateById / removeById ---
44
+ const byId = await db.findById('u', '5');
45
+ ok('#44 findById("5") 命中数字主键', !!byId && byId.name === 'a', byId);
46
+
47
+ const byIds = await db.findByIds('u', ['5', '6']);
48
+ ok('#44 findByIds(["5","6"]) 命中', byIds.length === 2, byIds.length);
49
+
50
+ ok('#44 updateById("6") 生效', (await db.updateById('u', '6', { name: 'bb' })) && (await db.findById('u', 6)).name === 'bb');
51
+
52
+ // --- 其他声明类型的归一 ---
53
+ const byFloat = await db.find('u', { score: '1.5' });
54
+ ok('#44 float 列:字符串查数值命中', byFloat.length === 1 && byFloat[0].id === 5, byFloat);
55
+ const byBool = await db.find('u', { active: 1 });
56
+ ok('#44 boolean 列:1 命中 true', byBool.length === 1 && byBool[0].id === 5, byBool);
57
+ const byText = await db.find('u', { name: 'a' });
58
+ ok('#44 text 列:正常查询不受影响', byText.length === 1, byText.length);
59
+
60
+ // --- hash 索引(旧版 _indexes)路径 ---
61
+ await db.createTable('h', { v: 'INT', s: 'TEXT' });
62
+ const ht = db._ensureTable('h');
63
+ ht.createIndex('v');
64
+ await db.insert('h', { v: 7, s: 'x' });
65
+ const byHash = await db.find('h', { v: '7' });
66
+ ok('#44 hash 索引:字符串查数值命中', byHash.length === 1 && byHash[0].v === 7, byHash);
67
+
68
+ // --- Mongo _id 唯一性:索引不再被类型差异绕过 ---
69
+ const srv = createMongoServer({ port: 0, noAuth: true, dataDir: ':memory:' });
70
+ const engine = await srv._getEngine('test');
71
+ await engine.createTable('c', { _id: { type: 'integer', unique: true }, v: { type: 'integer' } });
72
+ const r1 = await srv._dispatch('insert', {
73
+ insert: 'c',
74
+ documents: [{ _id: 1, v: 10 }, { _id: '1', v: 99 }],
75
+ $db: 'test',
76
+ });
77
+ const cols = await engine.find('c', {});
78
+ ok('#44 Mongo _id 去重不被类型差异绕过',
79
+ r1.ok === 1 && r1.n === 1 && cols.length === 1 && cols[0].v === 10,
80
+ { r: r1, rows: cols });
81
+
82
+ // --- 清理路径同样按类型归一 ---
83
+ await db.removeById('u', '5');
84
+ ok('#44 removeById("5") 删除数字主键行', (await db.find('u', { id: 5 })).length === 0);
85
+ } catch (e) {
86
+ fail++;
87
+ console.log('[FAIL] unexpected exception:', e && e.stack || e);
88
+ }
89
+
90
+ console.log(`\n${fail === 0 ? 'ALL PASS' : 'FAILURES'} — ${pass} passed, ${fail} failed\n`);
91
+ if (fail > 0) process.exit(1);
92
+ })();
@@ -0,0 +1,62 @@
1
+ /*
2
+ * Issue #45 — MongoDB $avg 把 NULL 当 0 且计入分母。
3
+ *
4
+ * [10, null, 30] 曾经得到 (10+0+30)/3 = 13.33,MongoDB 是 20;
5
+ * 全 null 的分组曾经返回 0,MongoDB 返回 null。
6
+ */
7
+ const path = require('path');
8
+ const { createMongoServer } = require(path.join(__dirname, '..', 'lib', 'mongo_server.js'));
9
+
10
+ let pass = 0, fail = 0;
11
+ function ok(name, cond, extra) {
12
+ if (cond) { pass++; console.log('[OK] ', name); }
13
+ else { fail++; console.log('[FAIL]', name, extra !== undefined ? '-> ' + JSON.stringify(extra).slice(0, 300) : ''); }
14
+ }
15
+
16
+ (async () => {
17
+ try {
18
+ const srv = createMongoServer({ port: 0, noAuth: true, dataDir: ':memory:' });
19
+ const engine = await srv._getEngine('test');
20
+ await engine.createTable('m', {
21
+ _id: { type: 'integer' },
22
+ g: { type: 'string' },
23
+ x: { type: 'integer' },
24
+ });
25
+ const table = engine._ensureTable('m');
26
+ table._rows = [
27
+ { _id: 1, g: 'a', x: 10 },
28
+ { _id: 2, g: 'a', x: null },
29
+ { _id: 3, g: 'a', x: 30 },
30
+ { _id: 4, g: 'b', x: null },
31
+ { _id: 5, g: 'b', x: undefined },
32
+ ];
33
+
34
+ const r = await srv._dispatch('aggregate', {
35
+ aggregate: 'm',
36
+ pipeline: [{ $group: { _id: '$g', avg: { $avg: '$x' } } }],
37
+ $db: 'test',
38
+ });
39
+ const batch = r.cursor ? r.cursor.firstBatch : [];
40
+ const byGroup = {};
41
+ for (const row of batch) byGroup[row._id] = row.avg;
42
+
43
+ ok('#45 $avg 忽略 null:#a = (10+30)/2 = 20', byGroup.a === 20, byGroup);
44
+ ok('#45 全 null 组返回 null', byGroup.b === null, byGroup);
45
+
46
+ // $sum 语义不受影响(null 加 0 仍是 no-op)
47
+ const r2 = await srv._dispatch('aggregate', {
48
+ aggregate: 'm',
49
+ pipeline: [{ $group: { _id: '$g', total: { $sum: '$x' } } }],
50
+ $db: 'test',
51
+ });
52
+ const bySum = {};
53
+ for (const row of (r2.cursor ? r2.cursor.firstBatch : [])) bySum[row._id] = row.total;
54
+ ok('#45 $sum 不受影响:#a = 40', bySum.a === 40, bySum);
55
+ } catch (e) {
56
+ fail++;
57
+ console.log('[FAIL] unexpected exception:', e && e.stack || e);
58
+ }
59
+
60
+ console.log(`\n${fail === 0 ? 'ALL PASS' : 'FAILURES'} — ${pass} passed, ${fail} failed\n`);
61
+ if (fail > 0) process.exit(1);
62
+ })();
@@ -0,0 +1,42 @@
1
+ /*
2
+ * Issue #46 — index.d.ts 声明了 applyParams / escapeValue / escapeId,
3
+ * index.js 却没有导出,按类型调用会 TypeError。
4
+ */
5
+ const path = require('path');
6
+
7
+ const ROOT = path.join(__dirname, '..');
8
+ const jsql = require(path.join(ROOT, 'index.js'));
9
+
10
+ let pass = 0, fail = 0;
11
+ function ok(name, cond, extra) {
12
+ if (cond) { pass++; console.log('[OK] ', name); }
13
+ else { fail++; console.log('[FAIL]', name, extra !== undefined ? '-> ' + JSON.stringify(extra).slice(0, 300) : ''); }
14
+ }
15
+
16
+ try {
17
+ ok('#46 applyParams 是函数', typeof jsql.applyParams === 'function', typeof jsql.applyParams);
18
+ ok('#46 escapeValue 是函数', typeof jsql.escapeValue === 'function', typeof jsql.escapeValue);
19
+ ok('#46 escapeId 是函数', typeof jsql.escapeId === 'function', typeof jsql.escapeId);
20
+
21
+ if (typeof jsql.applyParams === 'function') {
22
+ ok('#46 applyParams 参数替换生效', jsql.applyParams('SELECT ?', [1]) === 'SELECT 1', jsql.applyParams('SELECT ?', [1]));
23
+ }
24
+ if (typeof jsql.escapeValue === 'function') {
25
+ ok("#46 escapeValue 转义单引号", jsql.escapeValue("a'b") === "'a\\'b'", jsql.escapeValue("a'b"));
26
+ }
27
+ if (typeof jsql.escapeId === 'function') {
28
+ ok('#46 escapeId 反引号包裹', jsql.escapeId('a`b') === '`a``b`', jsql.escapeId('a`b'));
29
+ }
30
+
31
+ // .d.ts 与 .js 的导出面必须一致(防止再漂移)
32
+ const dts = require('fs').readFileSync(path.join(ROOT, 'index.d.ts'), 'utf8');
33
+ for (const name of ['applyParams', 'escapeValue', 'escapeId']) {
34
+ ok(`#46 index.d.ts 声明的 ${name} 已导出`, dts.includes(`export function ${name}`) && name in jsql);
35
+ }
36
+ } catch (e) {
37
+ fail++;
38
+ console.log('[FAIL] unexpected exception:', e && e.stack || e);
39
+ }
40
+
41
+ console.log(`\n${fail === 0 ? 'ALL PASS' : 'FAILURES'} — ${pass} passed, ${fail} failed\n`);
42
+ if (fail > 0) process.exit(1);
@@ -0,0 +1,65 @@
1
+ /*
2
+ * Issue #47 — MongoDB $sort 无论升降序都把 NULL 排在最后。
3
+ *
4
+ * MongoDB 语义:升序 null 在最前,降序 null 在最后。
5
+ * v6.5.26 的比较器在应用方向之前就 return 1 / -1,导致升序时 null 恒在末尾。
6
+ */
7
+ const path = require('path');
8
+ const { createMongoServer } = require(path.join(__dirname, '..', 'lib', 'mongo_server.js'));
9
+
10
+ let pass = 0, fail = 0;
11
+ function ok(name, cond, extra) {
12
+ if (cond) { pass++; console.log('[OK] ', name); }
13
+ else { fail++; console.log('[FAIL]', name, extra !== undefined ? '-> ' + JSON.stringify(extra).slice(0, 300) : ''); }
14
+ }
15
+
16
+ async function sorted(srv, dir) {
17
+ const r = await srv._dispatch('aggregate', {
18
+ aggregate: 's',
19
+ pipeline: [{ $sort: { score: dir } }],
20
+ $db: 'test',
21
+ });
22
+ const batch = r.cursor ? r.cursor.firstBatch : [];
23
+ return batch.map(x => (x.score === undefined ? null : x.score));
24
+ }
25
+
26
+ (async () => {
27
+ try {
28
+ const srv = createMongoServer({ port: 0, noAuth: true, dataDir: ':memory:' });
29
+ const engine = await srv._getEngine('test');
30
+ await engine.createTable('s', { _id: { type: 'integer' }, score: { type: 'integer' } });
31
+ const table = engine._ensureTable('s');
32
+ table._rows = [
33
+ { _id: 1, score: 20 },
34
+ { _id: 2, score: null },
35
+ { _id: 3, score: 10 },
36
+ { _id: 4, score: 30 },
37
+ ];
38
+
39
+ const asc = await sorted(srv, 1);
40
+ ok('#47 升序 null 排最前', JSON.stringify(asc) === JSON.stringify([null, 10, 20, 30]), asc);
41
+
42
+ const desc = await sorted(srv, -1);
43
+ ok('#47 降序 null 排最后', JSON.stringify(desc) === JSON.stringify([30, 20, 10, null]), desc);
44
+
45
+ // 多键排序:第一键相同时第二键的 null 也要跟随方向
46
+ table._rows = [
47
+ { _id: 1, g: 'a', score: null },
48
+ { _id: 2, g: 'a', score: 5 },
49
+ { _id: 3, g: 'a', score: 1 },
50
+ ];
51
+ const r = await srv._dispatch('aggregate', {
52
+ aggregate: 's',
53
+ pipeline: [{ $sort: { g: 1, score: -1 } }],
54
+ $db: 'test',
55
+ });
56
+ const multi = (r.cursor ? r.cursor.firstBatch : []).map(x => x.score);
57
+ ok('#47 多键降序 null 排最后', JSON.stringify(multi) === JSON.stringify([5, 1, null]), multi);
58
+ } catch (e) {
59
+ fail++;
60
+ console.log('[FAIL] unexpected exception:', e && e.stack || e);
61
+ }
62
+
63
+ console.log(`\n${fail === 0 ? 'ALL PASS' : 'FAILURES'} — ${pass} passed, ${fail} failed\n`);
64
+ if (fail > 0) process.exit(1);
65
+ })();
@@ -0,0 +1,39 @@
1
+ /*
2
+ * Issue #48 — mysql_compat 把 FLOAT/DOUBLE 列的 result metadata 报成
3
+ * LONGLONG(8),客户端按类型解码会误判。
4
+ *
5
+ * 修复:整数 → 8 (LONGLONG),非整数 → 5 (DOUBLE)。
6
+ */
7
+ const path = require('path');
8
+ const { createConnection } = require(path.join(__dirname, '..', 'lib', 'mysql_compat.js'));
9
+
10
+ let pass = 0, fail = 0;
11
+ function ok(name, cond, extra) {
12
+ if (cond) { pass++; console.log('[OK] ', name); }
13
+ else { fail++; console.log('[FAIL]', name, extra !== undefined ? '-> ' + JSON.stringify(extra).slice(0, 300) : ''); }
14
+ }
15
+
16
+ (async () => {
17
+ try {
18
+ const conn = createConnection();
19
+ await conn.connect();
20
+ await conn.query('CREATE TABLE m (id INTEGER PRIMARY KEY, i INTEGER, d DOUBLE, s TEXT)');
21
+ await conn.query("INSERT INTO m (id, i, d, s) VALUES (1, 42, 1.5, 'x')");
22
+
23
+ const [rows, fields] = await conn.query('SELECT i, d, s FROM m');
24
+ const typeOf = (name) => (fields.find(f => f.name === name) || {}).type;
25
+
26
+ ok('#48 INTEGER 列仍为 LONGLONG(8)', typeOf('i') === 8, { i: typeOf('i'), rows });
27
+ ok('#48 DOUBLE 列为 DOUBLE(5)', typeOf('d') === 5, { d: typeOf('d') });
28
+ ok('#48 TEXT 列为 VAR_STRING(253)', typeOf('s') === 253, { s: typeOf('s') });
29
+ ok('#48 行数据未受影响', rows.length === 1 && rows[0].i === 42 && rows[0].d === 1.5, rows);
30
+
31
+ await conn.end();
32
+ } catch (e) {
33
+ fail++;
34
+ console.log('[FAIL] unexpected exception:', e && e.stack || e);
35
+ }
36
+
37
+ console.log(`\n${fail === 0 ? 'ALL PASS' : 'FAILURES'} — ${pass} passed, ${fail} failed\n`);
38
+ if (fail > 0) process.exit(1);
39
+ })();