jsql-neo 6.0.0-beta2 → 6.0.2

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/pg_server.js CHANGED
@@ -762,7 +762,7 @@ class PgServer {
762
762
  this.auth = options.auth || null; // { user: { password, databases: [...] } }
763
763
  this.noAuth = options.noAuth === true;
764
764
  this.dataDir = options.dataDir || null;
765
- this.version = '5.1.3';
765
+ this.version = require('../package.json').version;
766
766
  this._server = null;
767
767
  this._sockets = new Set();
768
768
  this._connSeq = 0;
@@ -861,4 +861,10 @@ class PgServer {
861
861
  }
862
862
  }
863
863
 
864
- module.exports = { PgServer, PgConnection, buildRowDescription, buildDataRow, buildCommandComplete, buildErrorResponse, buildReadyForQuery, buildParameterStatus, buildBackendKeyData, buildParseComplete, buildBindComplete, buildCloseComplete, buildNoData, buildParameterDesc, TYPE_OIDS, pgTypeOid, pgFormatValue };
864
+ // README 的 createXxxServer 工厂系列里,PG 这个之前漏了实现:
865
+ // index.js 一直从本模块解构 createPgServer,实际拿到 undefined。
866
+ function createPgServer(options) {
867
+ return new PgServer(options || {});
868
+ }
869
+
870
+ module.exports = { PgServer, createPgServer, PgConnection, buildRowDescription, buildDataRow, buildCommandComplete, buildErrorResponse, buildReadyForQuery, buildParameterStatus, buildBackendKeyData, buildParseComplete, buildBindComplete, buildCloseComplete, buildNoData, buildParameterDesc, TYPE_OIDS, pgTypeOid, pgFormatValue };
package/lib/plugin.js CHANGED
@@ -159,7 +159,7 @@ function buildPluginContext(engine, plugin) {
159
159
  /** 主动触发一个钩子(含自定义钩子) */
160
160
  hook(hookName, ...args) { return engine.runHook(hookName, ...args); },
161
161
  /** 组合:加载另一个插件 */
162
- use(other) { return engine.use(other); },
162
+ use(other, opts) { return engine.use(other, opts); },
163
163
  /** 表清单(同步) */
164
164
  tables() {
165
165
  if (engine._tableNames) return Array.from(engine._tableNames);
@@ -191,7 +191,7 @@ function buildPluginContext(engine, plugin) {
191
191
 
192
192
  /**
193
193
  * 把一个插件应用到引擎:登记钩子、调用 install、挂载事件与 API。
194
- * 引擎的 `use()` 只需 `return applyPlugin(this, plugin)`。
194
+ * 引擎的 `use()` 只需 `return applyPlugin(this, plugin, opts)`。
195
195
  * @returns {object} engine(便于链式调用)
196
196
  */
197
197
  function applyPlugin(engine, plugin, opts) {
@@ -2,7 +2,8 @@
2
2
  * timestamps —— 自动维护创建/更新时间戳。
3
3
  *
4
4
  * 在插入时补全 createdAt / updatedAt(已存在则不覆盖 createdAt),
5
- * 更新时刷新 updatedAt。所有引擎通用,钩子内只原地修改数据、不回调引擎。
5
+ * 更新时刷新 updatedAt。建表时若 schema 未声明这两个字段,会原地补上,
6
+ * 以便 native / WASM 引擎按 schema 列持久化。钩子内只原地修改数据、不回调引擎。
6
7
  *
7
8
  * @example
8
9
  * db.use(require('jsql-neo/lib/plugins').timestamps());
@@ -22,9 +23,22 @@ function createTimestamps(opts = {}) {
22
23
  if (row[updatedField] === undefined || opts.overwriteUpdated) row[updatedField] = t;
23
24
  };
24
25
 
26
+ const stampDef = { type: 'string' };
27
+
28
+ const ensureStampFields = (schema) => {
29
+ if (!schema) return;
30
+ if (typeof schema === 'string') return;
31
+ if (typeof schema !== 'object') return;
32
+ if (schema[createdField] === undefined) schema[createdField] = { ...stampDef };
33
+ if (schema[updatedField] === undefined) schema[updatedField] = { ...stampDef };
34
+ };
35
+
25
36
  return definePlugin('timestamps', {
26
37
  config: { createdField, updatedField },
27
38
  hooks: {
39
+ beforeCreateTable(_name, schema) {
40
+ ensureStampFields(schema);
41
+ },
28
42
  beforeInsert(_table, rows) {
29
43
  if (Array.isArray(rows)) rows.forEach(r => stampRow(r, true));
30
44
  else stampRow(rows, true);
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Shared prepared-statement helper for JS / native / WASM engines.
3
+ * parse once, bind on each run. Compatible with executeSQL placeholder rules.
4
+ */
5
+
6
+ function createPrepared(engine, sql) {
7
+ if (typeof sql !== 'string' || sql.length === 0) {
8
+ throw new Error('prepare() requires a SQL string');
9
+ }
10
+ const bound = {
11
+ sql,
12
+ async all(...params) {
13
+ const values = params.length === 1 && Array.isArray(params[0]) ? params[0] : params;
14
+ return engine.query(sql, values);
15
+ },
16
+ async get(...params) {
17
+ const r = await bound.all(...params);
18
+ if (!r) return null;
19
+ if (Array.isArray(r.raw) && r.raw.length > 0) return r.raw[0];
20
+ if (Array.isArray(r.rows) && r.rows.length > 0) {
21
+ if (r.columns && Array.isArray(r.rows[0])) {
22
+ const obj = {};
23
+ for (let i = 0; i < r.columns.length; i++) obj[r.columns[i]] = r.rows[0][i];
24
+ return obj;
25
+ }
26
+ return r.rows[0];
27
+ }
28
+ return null;
29
+ },
30
+ async run(...params) {
31
+ return bound.all(...params);
32
+ }
33
+ };
34
+ return bound;
35
+ }
36
+
37
+ module.exports = { createPrepared };
package/lib/query.js CHANGED
@@ -25,6 +25,7 @@ class Query {
25
25
  this._cache = null; // { ttl, result, timestamp }
26
26
  this._caseExpr = null; // [{ when, then }, ...] + else
27
27
  this._optimizerHints = {}; // { useHashJoin: true }
28
+ this._forUpdate = false;
28
29
  }
29
30
 
30
31
  // ============================================================
@@ -302,8 +303,6 @@ class Query {
302
303
  });
303
304
  }
304
305
 
305
- // 字段选择
306
- rows = rows.map(r => ({ ...r }));
307
306
  if (this._selectFields) {
308
307
  rows = rows.map(r => {
309
308
  const obj = {};
@@ -312,6 +311,12 @@ class Query {
312
311
  }
313
312
  return obj;
314
313
  });
314
+ } else if (this._caseExpr) {
315
+ // CASE already cloned
316
+ } else {
317
+ const copy = new Array(rows.length);
318
+ for (let i = 0; i < rows.length; i++) copy[i] = { ...rows[i] };
319
+ rows = copy;
315
320
  }
316
321
 
317
322
  // 缓存存储
@@ -329,6 +334,14 @@ class Query {
329
334
  return rows.length > 0 ? rows[0] : null;
330
335
  }
331
336
 
337
+ exists() {
338
+ const prev = this._limitCount;
339
+ this._limitCount = 1;
340
+ const rows = this.get();
341
+ this._limitCount = prev;
342
+ return rows.length > 0;
343
+ }
344
+
332
345
  count() {
333
346
  return this._countRaw();
334
347
  }
@@ -533,10 +546,16 @@ class Query {
533
546
  }
534
547
  }
535
548
  } else if (join.type === 'right') {
549
+ const localMap = new Map();
550
+ for (const lr of localRows) {
551
+ const key = lr[join.localField];
552
+ if (key === undefined || key === null) continue;
553
+ if (!localMap.has(key)) localMap.set(key, []);
554
+ localMap.get(key).push(lr);
555
+ }
536
556
  for (const fr of foreignTable._rows) {
537
- const key = fr[join.foreignField];
538
- const matches = localRows.filter(lr => lr[join.localField] === key);
539
- if (matches.length === 0) {
557
+ const matches = localMap.get(fr[join.foreignField]);
558
+ if (!matches || matches.length === 0) {
540
559
  result.push(this._rightNullRow(localRows, fr, join));
541
560
  } else {
542
561
  for (const lr of matches) {
@@ -470,52 +470,81 @@ class RedisServer {
470
470
 
471
471
  /** 供多协议嗅探复用:把 socket 交给 Redis 处理,可携带已缓冲的首包 */
472
472
  _handleSocket(socket, existing = '') {
473
- let buf = String(existing || '');
473
+ // 刻意不 setEncoding('utf8'):RESP 的 $N 是**字节**长度,
474
+ // 转成字符串再按字符切片会把多字节 UTF-8(中文等)截断。
475
+ let buf = Buffer.isBuffer(existing) ? existing : Buffer.from(String(existing || ''), 'utf8');
474
476
  this._authedSockets.set(socket, false);
475
- socket.setEncoding('utf8');
476
- socket.on('data', (chunk) => {
477
- buf += chunk;
477
+
478
+ const drain = () => {
478
479
  for (;;) {
479
- const msg = this._parse(buf);
480
+ let msg;
481
+ try {
482
+ msg = this._parse(buf);
483
+ } catch (e) {
484
+ // 协议错误:回复错误并丢弃缓冲,避免一直卡在同一段坏数据上
485
+ this._send(socket, (e instanceof Error) ? e : new Error('ERR Protocol error'));
486
+ buf = Buffer.alloc(0);
487
+ return;
488
+ }
480
489
  if (!msg) break;
481
490
  buf = buf.slice(msg.consumed);
491
+ if (msg.ignore) continue;
482
492
  this._handle(socket, msg.cmd, msg.args);
483
493
  }
494
+ };
495
+
496
+ socket.on('data', (chunk) => {
497
+ buf = Buffer.concat([buf, Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk), 'utf8')]);
498
+ drain();
484
499
  });
485
500
  socket.on('error', () => {});
486
- for (;;) {
487
- const msg = this._parse(buf);
488
- if (!msg) break;
489
- buf = buf.slice(msg.consumed);
490
- this._handle(socket, msg.cmd, msg.args);
491
- }
501
+ drain();
492
502
  }
493
503
 
504
+ /**
505
+ * 解析一条 RESP2 请求,返回 { consumed, cmd, args } 或 null(数据不完整)。
506
+ * buf 是 Buffer —— 所有长度计算都按**字节**进行。
507
+ */
494
508
  _parse(buf) {
495
- if (buf[0] !== '*') {
496
- const i = buf.indexOf('\r\n');
509
+ if (!buf || buf.length === 0) return null;
510
+ const crlf = (from) => buf.indexOf('\r\n', from, 'utf8');
511
+
512
+ // 非 '*' 开头:inline 命令(telnet / nc 手工输入)
513
+ if (buf[0] !== 0x2a) {
514
+ const i = crlf(0);
497
515
  if (i < 0) return null;
498
- const cmd = buf.slice(0, i).trim().split(/\s+/);
499
- if (!cmd.length) return null;
500
- return { consumed: i + 2, cmd: cmd.map(c => c.toUpperCase()), args: cmd.slice(1) };
516
+ const line = buf.slice(0, i).toString('utf8').trim();
517
+ if (!line) return { consumed: i + 2, ignore: true };
518
+ const parts = line.split(/\s+/);
519
+ // 之前这里返回的是数组(cmd.map(...)),导致 switch(cmd) 永远匹配不上,
520
+ // inline 输入一律被当成 unknown command。必须是字符串。
521
+ const cmd = parts[0].toUpperCase();
522
+ return { consumed: i + 2, cmd, args: parts.slice(1) };
501
523
  }
502
- const i = buf.indexOf('\r\n');
524
+
525
+ const i = crlf(0);
503
526
  if (i < 0) return null;
504
- const n = parseInt(buf.slice(1, i), 10);
505
- if (isNaN(n)) throw new Error('ERR Protocol error');
527
+ const n = parseInt(buf.slice(1, i).toString('utf8'), 10);
528
+ if (isNaN(n)) throw new Error('ERR Protocol error: invalid multibulk length');
529
+ // *0(空命令)与 *-1(null 数组)此前会走到 parts[0].toUpperCase() 崩溃
530
+ if (n <= 0) return { consumed: i + 2, ignore: true };
531
+
506
532
  let off = i + 2;
507
533
  const parts = [];
508
534
  for (let k = 0; k < n; k++) {
509
- if (buf[off] !== '$') throw new Error('ERR Protocol error: expected bulk string');
510
- const j = buf.indexOf('\r\n', off);
535
+ if (buf.length <= off) return null;
536
+ if (buf[off] !== 0x24) throw new Error('ERR Protocol error: expected bulk string'); // '$'
537
+ const j = crlf(off);
511
538
  if (j < 0) return null;
512
- const len = parseInt(buf.slice(off + 1, j), 10);
513
- if (isNaN(len)) throw new Error('ERR Protocol error');
514
- if (buf.length < j + 2 + len + 2) return null;
515
- parts.push(buf.slice(j + 2, j + 2 + len));
516
- off = j + 2 + len + 2;
539
+ const len = parseInt(buf.slice(off + 1, j).toString('utf8'), 10);
540
+ if (isNaN(len)) throw new Error('ERR Protocol error: invalid bulk length');
541
+ const start = j + 2;
542
+ if (len === -1) { parts.push(null); off = start; continue; } // $-1 null bulk
543
+ if (buf.length < start + len + 2) return null; // 按字节判断
544
+ parts.push(buf.slice(start, start + len).toString('utf8'));
545
+ off = start + len + 2;
517
546
  }
518
- const cmd = parts[0].toUpperCase();
547
+ const cmd = String(parts[0] || '').toUpperCase();
519
548
  return { consumed: off, cmd, args: parts.slice(1) };
520
549
  }
521
550
 
@@ -550,7 +579,8 @@ class RedisServer {
550
579
  } else if (typeof val === 'number') {
551
580
  out = ':' + val + '\r\n';
552
581
  } else if (Array.isArray(val)) {
553
- out = '*' + val.length + '\r\n' + val.map(v => '$' + String(v).length + '\r\n' + v + '\r\n').join('');
582
+ // 长度用字节数,不能用 String.length —— 否则中文等多字节值会让客户端解帧错位
583
+ out = '*' + val.length + '\r\n' + val.map(v => '$' + Buffer.byteLength(String(v), 'utf8') + '\r\n' + v + '\r\n').join('');
554
584
  } else if (typeof val === 'string') {
555
585
  if (val === 'OK' || val === 'PONG') {
556
586
  out = '+' + val + '\r\n';