jsql-neo 6.5.20 → 6.5.22

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 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.20** — stable release · [github.com/vexify-org/JSQL-neo](https://github.com/vexify-org/JSQL-neo) · [jsql.vexify.top](https://jsql.vexify.top)
6
+ > **v6.5.22** — 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
  ![Engines](https://img.shields.io/badge/engines-Native%20%7C%20WASM%20%7C%20Pure%20JS-7ee787)
9
9
  ![MySQL](https://img.shields.io/badge/protocol-MySQL%20compatible-1f6feb)
package/bin/jsql CHANGED
@@ -1,7 +1,11 @@
1
1
  #!/usr/bin/env node
2
2
  const path = require('path');
3
3
  const fs = require('fs');
4
- const yaggs = require('@vexify-org/yaggs');
4
+ // @vexify-org/yaggs 是可选依赖:缺失时(离线安装/私有源不可达/lockfile 漂移)
5
+ // 用内置 shim 兜底,保证 --help/--version 及核心命令仍能启动(#24)。
6
+ let yaggs;
7
+ try { yaggs = require('@vexify-org/yaggs'); }
8
+ catch (e) { yaggs = null; }
5
9
  const { ModuleManager } = require('../lib/mod');
6
10
  const Database = require('../lib/database');
7
11
  const migrate = require('../lib/migrate');
@@ -29,7 +33,10 @@ const manager = new ModuleManager();
29
33
 
30
34
  const RAW_ARGV = process.argv.slice(2);
31
35
 
32
- const cli = yaggs({ pkg: require('../package.json') })
36
+ const _pkg = require('../package.json');
37
+ const cli = (yaggs || require('./yaggs-fallback').buildYaggsFallback(_pkg))({
38
+ pkg: _pkg,
39
+ })
33
40
  .usage('jsql <command> [options]')
34
41
  .option('help', { alias: ['h'], type: 'boolean', description: 'Show help' })
35
42
  .command('mod', 'Module management', (sub) => {
@@ -0,0 +1,108 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Minimal @vexify-org/yaggs-compatible CLI shim.
5
+ *
6
+ * Used when the optional dependency @vexify-org/yaggs is not installed
7
+ * (offline install, private registry unreachable, lockfile drift, …).
8
+ * Without this shim, bin/jsql crashed on every invocation — even --help /
9
+ * --version — because the top-level `require('@vexify-org/yaggs')` threw.
10
+ *
11
+ * The shim mirrors the fluent API surface bin/jsql relies on:
12
+ * .usage(str).option(name, cfg).command(name, desc, builder, handler)
13
+ * .middleware(fn).run(argv)
14
+ *
15
+ * It captures every command handler registered via the fluent chain and
16
+ * dispatches to it with a minimal `--key value` / `--flag` argument parse.
17
+ * This keeps --help/--version and the core commands (serve, export, import,
18
+ * mod, plugin, …) working even when the optional dependency is absent.
19
+ */
20
+
21
+ function buildYaggsFallback(pkg) {
22
+ // 与 @vexify-org/yaggs 的调用形态一致:buildYaggsFallback(pkg)({ pkg }) 返回 CLI 对象。
23
+ return function makeCli(opts) {
24
+ const realPkg = (opts && opts.pkg) || pkg;
25
+ const commands = new Map();
26
+ let usageStr = '';
27
+ const self = {
28
+ usage(u) { usageStr = u; return self; },
29
+ option() { return self; },
30
+ positional() { return self; },
31
+ middleware() { return self; },
32
+ command(name, description, builder, handler) {
33
+ // 兼容两种签名:command(name, descObj) 或 command(name, desc, builder, handler)
34
+ let h = handler;
35
+ let desc = description;
36
+ if (description && typeof description === 'object') {
37
+ desc = description.description;
38
+ if (description.handler) h = description.handler;
39
+ }
40
+ commands.set(name, { desc: desc || '', handler: h, builder: builder || (() => {}) });
41
+ return self;
42
+ },
43
+ async run(argv) {
44
+ const args = (argv || []).slice();
45
+ const first = args[0];
46
+
47
+ if (!first || first === '--help' || first === '-h' || first === 'help') {
48
+ console.log(usageStr || `jsql ${realPkg.version} — embedded SQL CLI`);
49
+ console.log('Usage: jsql <command> [options]');
50
+ if (commands.size) {
51
+ console.log('');
52
+ console.log('Commands:');
53
+ for (const [name, c] of commands) {
54
+ console.log(` ${name.padEnd(12)} ${c.desc}`);
55
+ }
56
+ }
57
+ console.log('');
58
+ console.log('Note: optional dependency @vexify-org/yaggs is not installed; full argument parsing is unavailable.');
59
+ console.log(' Install it with `npm install @vexify-org/yaggs` to restore rich argument parsing.');
60
+ return;
61
+ }
62
+
63
+ if (first === '--version' || first === '-v' || first === 'version') {
64
+ console.log(realPkg.version);
65
+ return;
66
+ }
67
+
68
+ const cmd = commands.get(first);
69
+ if (!cmd) {
70
+ console.error(`Unknown command: ${first}`);
71
+ console.error('Run `jsql --help` for the list of commands.');
72
+ process.exitCode = 1;
73
+ return;
74
+ }
75
+
76
+ // 极简参数解析:--key value / --key=value / --flag,以及位置参数。
77
+ const parsed = { _: [] };
78
+ for (let i = 1; i < args.length; i++) {
79
+ const a = args[i];
80
+ if (a.startsWith('--')) {
81
+ const eq = a.indexOf('=');
82
+ if (eq !== -1) {
83
+ parsed[a.slice(2, eq)] = a.slice(eq + 1);
84
+ } else {
85
+ const nxt = args[i + 1];
86
+ if (nxt !== undefined && !nxt.startsWith('-')) { parsed[a.slice(2)] = nxt; i++; }
87
+ else parsed[a.slice(2)] = true;
88
+ }
89
+ } else if (a.startsWith('-') && a.length > 1) {
90
+ const key = a.slice(1);
91
+ const nxt = args[i + 1];
92
+ if (nxt !== undefined && !nxt.startsWith('-')) { parsed[key] = nxt; i++; }
93
+ else parsed[key] = true;
94
+ } else {
95
+ parsed._.push(a);
96
+ }
97
+ }
98
+
99
+ if (cmd.handler) return cmd.handler(parsed);
100
+ console.error(`Command '${first}' has no handler.`);
101
+ process.exitCode = 1;
102
+ },
103
+ };
104
+ return self;
105
+ };
106
+ }
107
+
108
+ module.exports = { buildYaggsFallback };
package/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * JSQL-NEO v6.5.20 — Rust-Powered Embedded Database (WASM + HTTP)
2
+ * JSQL-NEO v6.5.22 — Rust-Powered Embedded Database (WASM + HTTP)
3
3
  *
4
4
  * @example
5
5
  * const jsql = require('jsql-neo');
package/lib/database.js CHANGED
@@ -1948,7 +1948,23 @@ class Database {
1948
1948
  const clash = table._rows.find(r => r !== row && r[field] === nv);
1949
1949
  if (clash) throw createError('ER_DUP_ENTRY', `Duplicate entry '${nv}' for key '${field}'`);
1950
1950
  }
1951
- Object.assign(row, validated);
1951
+ // 外键:改主键时按 ON UPDATE 传播到引用它的子表;改自身外键列时校验引用存在。
1952
+ // 此前这里直接 Object.assign 绕过 Table.updateById,ON UPDATE CASCADE / RESTRICT
1953
+ // 与 FK 校验全部失效(外键形同虚设)。
1954
+ if (pk && Object.prototype.hasOwnProperty.call(validated, pk) && validated[pk] !== row[pk]) {
1955
+ table._propagatePKUpdate(row[pk], validated[pk]);
1956
+ }
1957
+ if (table._foreignKeys && table._foreignKeys.length) {
1958
+ table._validateForeignKeys({ ...row, ...validated });
1959
+ }
1960
+ // 改主键/唯一值时索引必须同步:否则 _pkIndex 还指向旧值,
1961
+ // 后续 FK 校验 findOne({id:新值}) 查不到刚更新的父行。
1962
+ {
1963
+ const rowIndex = table._rows.indexOf(row);
1964
+ if (rowIndex !== -1) table._removeFromIndexes(rowIndex, row);
1965
+ Object.assign(row, validated);
1966
+ if (rowIndex !== -1) table._addToIndexes(rowIndex, row);
1967
+ }
1952
1968
  if (pk) this._walWrite({ op: 'update', table: tableName, pk, id, row: { ...validated } });
1953
1969
  this._emit('update', { table: tableName, id, data: validated });
1954
1970
  await this._runHooks('afterUpdate', [tableName, id, validated, { ok: true }]);
@@ -281,7 +281,8 @@ function columnDefinition(column) {
281
281
 
282
282
  function columnTypeFromSchema(def) {
283
283
  const type = def && def.type ? String(def.type).toLowerCase() : 'string';
284
- if (type === 'integer' || type === 'int' || type === 'bigint') return MYSQL_TYPE_LONG;
284
+ if (type === 'integer' || type === 'int') return MYSQL_TYPE_LONG;
285
+ if (type === 'bigint') return MYSQL_TYPE_LONGLONG;
285
286
  if (type === 'number' || type === 'float' || type === 'double' || type === 'real') return MYSQL_TYPE_DOUBLE;
286
287
  if (type === 'boolean' || type === 'bool') return MYSQL_TYPE_TINY;
287
288
  if (type === 'date') return MYSQL_TYPE_DATE;
@@ -394,15 +395,26 @@ function binaryResultSetPacket(result, tableSchema, baseSeq) {
394
395
  if (v === null || v === undefined) return null;
395
396
  if (type === MYSQL_TYPE_LONG || type === MYSQL_TYPE_LONGLONG || type === MYSQL_TYPE_DOUBLE) {
396
397
  const num = Number(v);
398
+ if (!Number.isFinite(num)) return encodeLenencString(String(v));
397
399
  if (type === MYSQL_TYPE_DOUBLE) {
398
- if (!Number.isFinite(num)) return encodeLenencString(String(v));
399
400
  const b = Buffer.alloc(8); b.writeDoubleLE(num, 0); return b;
400
401
  }
401
- if (Number.isInteger(num) && num <= 2147483647 && num >= -2147483648) {
402
- const b = Buffer.alloc(4); b.writeInt32LE(num, 0); return b;
403
- }
404
402
  if (!Number.isFinite(num)) return encodeLenencString(String(v));
405
- const b = Buffer.alloc(8); b.writeBigInt64LE(BigInt(Math.trunc(num)), 0); return b;
403
+ if (type === MYSQL_TYPE_LONG) {
404
+ // 4 字节 LONG:总是写 4 字节(#32)。超出 int32 范围时钳位,
405
+ // 不能写 8 字节,否则二进制结果集的行布局被破坏(下一列错位)。
406
+ let n = Math.trunc(num);
407
+ if (n > 2147483647) n = 2147483647;
408
+ if (n < -2147483648) n = -2147483648;
409
+ const b = Buffer.alloc(4); b.writeInt32LE(n, 0); return b;
410
+ }
411
+ // MYSQL_TYPE_LONGLONG:始终写 8 字节,超出 int64 范围时钳位(保持行布局)。
412
+ let bi = BigInt(Math.trunc(num));
413
+ const INT64_MAX = 9223372036854775807n;
414
+ const INT64_MIN = -9223372036854775808n;
415
+ if (bi > INT64_MAX) bi = INT64_MAX;
416
+ if (bi < INT64_MIN) bi = INT64_MIN;
417
+ const b = Buffer.alloc(8); b.writeBigInt64LE(bi, 0); return b;
406
418
  }
407
419
  if (type === MYSQL_TYPE_TINY) {
408
420
  const b = Buffer.alloc(1); b.writeInt8(v ? 1 : 0, 0); return b;
@@ -1102,8 +1114,10 @@ class MysqlServer {
1102
1114
  if (t && t.indexOf('.') !== -1) {
1103
1115
  const re = new RegExp('\\b' + t.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '\\b', 'g');
1104
1116
  s = s.replace(re, (match, offset) => {
1105
- // 如果匹配位置在字符串字面量或注释中,不替换
1106
- if (masked[offset] !== ' ') return match;
1117
+ // 如果匹配位置在字符串字面量或注释中,不替换。
1118
+ // _maskStringsAndComments 把字符串/注释内的字符置为空格,
1119
+ // 因此 masked[offset] === ' ' 表示该位置在字面量/注释里(#31)。
1120
+ if (masked[offset] === ' ') return match;
1107
1121
  return t.split('.')[1];
1108
1122
  });
1109
1123
  }
@@ -57,6 +57,19 @@ function _trackInstance(inst) {
57
57
  }
58
58
 
59
59
  const INT64_TAG = 1;
60
+
61
+ /**
62
+ * 把可能是字符串/数字的 limit / offset / id 规范成非负整数,供原生 usize/u64 使用。
63
+ * 负值(或 NaN/Infinity)会在绑定层直接抛错,而不是让 Rust 侧 debug panic 或
64
+ * release 静默空结果(#35)。返回 Math.trunc(n)(对非负浮点向下取整,与 usize 语义一致)。
65
+ */
66
+ function toNonNeg(name, v, def) {
67
+ if (v === undefined || v === null) return def;
68
+ const n = Number(v);
69
+ if (!Number.isFinite(n)) throw new Error(`jsql-neo: ${name} must be a finite number, got ${v}`);
70
+ if (n < 0) throw new Error(`jsql-neo: ${name} must be non-negative, got ${v}`);
71
+ return Math.trunc(n);
72
+ }
60
73
  const FLOAT_TAG = 2;
61
74
  const STR_TAG = 3;
62
75
  const BOOL_TAG = 4;
@@ -539,7 +552,7 @@ class JSQL {
539
552
  if (!schema || !pk) return id;
540
553
  const def = schema[pk];
541
554
  let t = null;
542
- if (typeof def === 'string') t = def.toLowerCase();
555
+ if (typeof def === 'string') t = def.trim().split(/\s+/)[0].toLowerCase();
543
556
  else if (def && typeof def === 'object' && def.type) t = String(def.type).toLowerCase();
544
557
  if (['integer', 'int', 'bigint'].includes(t)) return Number(id);
545
558
  return id;
@@ -552,8 +565,11 @@ class JSQL {
552
565
  _isAutoIntPk(table) {
553
566
  if (!this._pkAutoIncrement(table)) return false;
554
567
  const def = this._schemas[table][this._pkOf(table)];
555
- const t = typeof def === 'string' ? def.toLowerCase()
556
- : (def && def.type ? String(def.type).toLowerCase() : '');
568
+ // 简写字符串 schema(如 'integer primary key auto_increment')取首个 token 作为类型;
569
+ // 对象 schema 直接读 def.type。二者都必须精确匹配整数类型,否则负 id 校验不生效(issue #35)。
570
+ let t = '';
571
+ if (typeof def === 'string') t = def.trim().split(/\s+/)[0].toLowerCase();
572
+ else if (def && def.type) t = String(def.type).toLowerCase();
557
573
  return ['integer', 'int', 'bigint'].includes(t);
558
574
  }
559
575
 
@@ -561,8 +577,11 @@ class JSQL {
561
577
  const pk = this._pkOf(table);
562
578
  if (pk) {
563
579
  if (this._isAutoIntPk(table)) {
564
- const n = Number(id);
580
+ const n = toNonNeg('id', id, NaN);
565
581
  if (Number.isFinite(n)) {
582
+ // 负 id 会被原生层 `i64 -> u64` 静默回绕成 usize::MAX,导致
583
+ // findById 误报 not found(行明明存在却取不到)。在此显式拒绝。
584
+ if (n < 0) throw new Error('id must be a non-negative number');
566
585
  const r = safeParse(this._n('jsqlFindById', table, n));
567
586
  if (r && r.error) return r;
568
587
  return r || null;
@@ -574,15 +593,18 @@ class JSQL {
574
593
  if (r && r.error) return r;
575
594
  return Array.isArray(r) && r.length > 0 ? r[0] : null;
576
595
  }
577
- return safeParse(this._n('jsqlFindById', table, Number(id)));
596
+ return safeParse(this._n('jsqlFindById', table, toNonNeg('id', id, NaN)));
578
597
  }
579
598
 
580
599
  _seqIdByPk(table, id) {
581
600
  const pk = this._pkOf(table);
582
601
  if (pk) {
583
602
  if (this._isAutoIntPk(table)) {
584
- const n = Number(id);
603
+ const n = toNonNeg('id', id, NaN);
585
604
  if (Number.isFinite(n)) {
605
+ // 负 id 在原生层回绕为 usize::MAX 后静默 not found,update/remove
606
+ // 会无提示地什么都不做 —— 这里显式拒绝(见 issue #35)。
607
+ if (n < 0) throw new Error('id must be a non-negative number');
586
608
  const r = safeParse(this._n('jsqlFindById', table, n));
587
609
  if (r && !r.error && r.id !== undefined) return r.id;
588
610
  }
@@ -824,7 +846,10 @@ class JSQL {
824
846
  let r;
825
847
  if (this._isAutoIntPk(table) && ids.every(id => Number.isFinite(Number(id)))) {
826
848
  // 自增主键:内部 id 即主键值,一次批量调用即可,避免 N 次往返。
827
- r = safeParse(this._n('jsqlFindByIds', table, JSON.stringify(ids.map(Number))));
849
+ // #35:负 id 在原生层 `i64 -> u64` 回绕成 usize::MAX,静默 not found;
850
+ // NaN/Infinity 同样不允许。绑定层先校验,绝不把非法值传给原生模块。
851
+ const nums = ids.map(id => toNonNeg('id', id, 0));
852
+ r = safeParse(this._n('jsqlFindByIds', table, JSON.stringify(nums)));
828
853
  } else if (this._pkOf(table)) {
829
854
  r = ids.map(id => this._findRawById(table, id)).filter(x => x !== null && !(x && x.error));
830
855
  } else {
@@ -846,7 +871,12 @@ class JSQL {
846
871
  if (!(await this._runHooks('beforeFind', [table, { filter, opts }]))) return [];
847
872
  const filterStr = filter ? JSON.stringify(filter) : '';
848
873
  const { limit = 100, offset = 0 } = opts;
849
- const r = safeParse(this._n('jsqlFind', table, filterStr, limit, offset));
874
+ // #35:原生层把 limit/offset 按 `as usize` 无符号转换,负数会静默回绕
875
+ // (release 下空结果 / debug 下 usize::MAX+1 溢出 panic)。绑定层先校验,
876
+ // 与 WASM 端的 usize 签名保持一致,绝不把负值传给原生模块。
877
+ const lim = toNonNeg('limit', limit, 100);
878
+ const off = toNonNeg('offset', offset, 0);
879
+ const r = safeParse(this._n('jsqlFind', table, filterStr, lim, off));
850
880
  if (r && r.error) throw new Error(r.error);
851
881
  await this._runHooks('afterFind', [table, { filter, opts }, r]);
852
882
  const schema = this._schemas[table];
package/lib/pg_server.js CHANGED
@@ -711,7 +711,11 @@ class PgConnection {
711
711
  const tag = `SELECT ${(res.rows || []).length}`;
712
712
  this._send(buildCommandComplete(tag));
713
713
  } else if (res.type === 'insert' || res.type === 'update' || res.type === 'delete') {
714
- const tag = `${String(res.type).toUpperCase()} ${res.affectedRows != null ? res.affectedRows : 0}`;
714
+ // PG CommandComplete tag:INSERT 是 "INSERT <oid> <count>",UPDATE/DELETE 是
715
+ // "UPDATE <count>" / "DELETE <count>"(#33)。缺了 OID 段会让客户端把 count
716
+ // 解析到 OID 槽位、rowCount 变 NaN。
717
+ const count = res.affectedRows != null ? res.affectedRows : 0;
718
+ const tag = res.type === 'insert' ? `INSERT 0 ${count}` : `${String(res.type).toUpperCase()} ${count}`;
715
719
  this._send(buildCommandComplete(tag));
716
720
  } else if (res.type === 'begin' || res.type === 'commit' || res.type === 'rollback') {
717
721
  this._inTransaction = res.type === 'begin';
package/lib/query.js CHANGED
@@ -227,9 +227,11 @@ class Query {
227
227
  const startTime = Date.now();
228
228
  if (this._explain) return this._getExplain();
229
229
 
230
- // 缓存命中
230
+ // 缓存命中: 返回每行的浅拷贝,与未命中路径一致,避免调用方修改污染缓存
231
231
  if (this._cache && this._cache.result && (Date.now() - this._cache.timestamp < this._cache.ttl)) {
232
- return this._cache.result;
232
+ // #37:命中返回副本,而不是内部数组引用——调用方对返回值做变更
233
+ // (push/splice/改字段)会污染缓存;未命中路径返回的是新数组,行为应一致。
234
+ return this._cache.result.map(row => (row && typeof row === 'object' ? { ...row } : row));
233
235
  }
234
236
 
235
237
  // P1-4: 软删除模式下 QueryBuilder 也不得泄露已删除行
@@ -327,9 +329,10 @@ class Query {
327
329
  rows = copy;
328
330
  }
329
331
 
330
- // 缓存存储
332
+ // 缓存存储:#37 存副本,未命中路径返回的 rows 直接给调用方随意变更,
333
+ // 不能把同一个数组塞进缓存(否则调用方 push/改字段会污染后续命中)。
331
334
  if (this._cache) {
332
- this._cache.result = rows;
335
+ this._cache.result = rows.map(row => (row && typeof row === 'object' ? { ...row } : row));
333
336
  this._cache.timestamp = Date.now();
334
337
  }
335
338