jsql-neo 6.3.6 → 6.3.10

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.
@@ -11,11 +11,82 @@ const fs = require('fs');
11
11
  const { executeSQL, parseSQL, splitStatements, applyParams } = require('./sql');
12
12
  const Database = require('./database');
13
13
 
14
+ /**
15
+ * 词法扫描 SQL,将字符串字面量和注释中的字符替换为空格(保持长度不变)。
16
+ * 这样在替换时可以通过检查对应位置是否为空格来判断是否在字符串/注释中。
17
+ */
18
+ function _maskStringsAndComments(sql) {
19
+ const chars = sql.split('');
20
+ let i = 0;
21
+ while (i < chars.length) {
22
+ const c = chars[i];
23
+ // 单引号字符串
24
+ if (c === "'") {
25
+ i++;
26
+ while (i < chars.length && chars[i] !== "'") {
27
+ if (chars[i] === '\\' && i + 1 < chars.length) {
28
+ chars[i] = ' ';
29
+ chars[i + 1] = ' ';
30
+ i += 2;
31
+ } else {
32
+ chars[i] = ' ';
33
+ i++;
34
+ }
35
+ }
36
+ if (i < chars.length) i++; // skip closing quote
37
+ continue;
38
+ }
39
+ // 双引号字符串
40
+ if (c === '"') {
41
+ i++;
42
+ while (i < chars.length && chars[i] !== '"') {
43
+ if (chars[i] === '\\' && i + 1 < chars.length) {
44
+ chars[i] = ' ';
45
+ chars[i + 1] = ' ';
46
+ i += 2;
47
+ } else {
48
+ chars[i] = ' ';
49
+ i++;
50
+ }
51
+ }
52
+ if (i < chars.length) i++; // skip closing quote
53
+ continue;
54
+ }
55
+ // 单行注释 --
56
+ if (c === '-' && chars[i + 1] === '-') {
57
+ while (i < chars.length && chars[i] !== '\n') {
58
+ chars[i] = ' ';
59
+ i++;
60
+ }
61
+ continue;
62
+ }
63
+ // 多行注释 /* */
64
+ if (c === '/' && chars[i + 1] === '*') {
65
+ chars[i] = ' ';
66
+ chars[i + 1] = ' ';
67
+ i += 2;
68
+ while (i < chars.length && !(chars[i] === '*' && chars[i + 1] === '/')) {
69
+ chars[i] = ' ';
70
+ i++;
71
+ }
72
+ if (i < chars.length) {
73
+ chars[i] = ' ';
74
+ chars[i + 1] = ' ';
75
+ i += 2;
76
+ }
77
+ continue;
78
+ }
79
+ i++;
80
+ }
81
+ return chars.join('');
82
+ }
83
+
14
84
  const SERVER_VERSION = '8.0.0-jsql-neo';
15
85
  const CLIENT_PROTOCOL_41 = 0x00000001 << 9;
16
86
  const CLIENT_SECURE_CONNECTION = 0x00008000;
17
87
  const CLIENT_PLUGIN_AUTH = 0x00080000;
18
88
  const CLIENT_CONNECT_WITH_DB = 0x00000008;
89
+ // CLIENT_LONG_PASSWORD 已废弃但保留协议兼容性(客户端可能发送此标志)
19
90
  const CLIENT_LONG_PASSWORD = 0x00000001;
20
91
  const CLIENT_TRANSACTIONS = 0x00002000;
21
92
  const CLIENT_MULTI_STATEMENTS = 0x00010000;
@@ -239,13 +310,14 @@ function resultSetPacket(result, tableSchema, baseSeq, rawRow) {
239
310
  const sampleRow = (result.rows && result.rows[0]) || null;
240
311
  for (let ci = 0; ci < (result.columns || []).length; ci++) {
241
312
  const name = result.columns[ci];
313
+ if (name === null || name === undefined) name = 'col_' + ci;
242
314
  let sample;
243
315
  if (sampleRow !== null) {
244
316
  if (Array.isArray(sampleRow)) sample = sampleRow[ci];
245
317
  else sample = sampleRow[name];
246
318
  }
247
319
  const col = {
248
- name,
320
+ name: String(name),
249
321
  table: result.table || '',
250
322
  type: inferType(name, sample),
251
323
  length: 1024,
@@ -255,6 +327,7 @@ function resultSetPacket(result, tableSchema, baseSeq, rawRow) {
255
327
  push(eofPacket());
256
328
 
257
329
  for (const row of result.rows || []) {
330
+ if (!row) continue;
258
331
  const parts = [];
259
332
  for (let i = 0; i < (result.columns || []).length; i++) {
260
333
  const v = row[i];
@@ -307,7 +380,7 @@ function binaryResultSetPacket(result, tableSchema, baseSeq) {
307
380
  }
308
381
  const t = inferType(cols[ci], sample);
309
382
  types.push(t);
310
- push(columnDefinition({ name: cols[ci], table: result.table || '', type: t, length: 1024 }));
383
+ push(columnDefinition({ name: String(cols[ci]), table: result.table || '', type: t, length: 1024 }));
311
384
  }
312
385
  push(eofPacket());
313
386
 
@@ -351,11 +424,12 @@ function binaryResultSetPacket(result, tableSchema, baseSeq) {
351
424
 
352
425
  for (const row of result.rows || []) {
353
426
  const parts = [Buffer.from([0x00])];
354
- const nb = Buffer.alloc(Math.ceil(cols.length / 8));
427
+ const nb = Buffer.alloc(Math.ceil((cols.length + 2) / 8));
355
428
  const encoded = [];
356
429
  for (let i = 0; i < cols.length; i++) {
357
430
  const v = Array.isArray(row) ? row[i] : row[cols[i]];
358
431
  if (v === null || v === undefined) {
432
+ // null bitmap 按列顺序编码,bit=1 表示 NULL
359
433
  nb[Math.floor(i / 8)] |= (1 << (i % 8));
360
434
  encoded.push(null);
361
435
  } else {
@@ -646,6 +720,21 @@ class MysqlConnection {
646
720
  continue;
647
721
  }
648
722
  if (c === "'" || c === '"' || c === '`') { inStr = c; continue; }
723
+ // P2-4: countPlaceholders 不跳过注释——注释中的 ? 不应计入占位符
724
+ if (c === '-' && sql[i + 1] === '-') {
725
+ while (i < sql.length && sql[i] !== '\n') i++;
726
+ continue;
727
+ }
728
+ if (c === '#') {
729
+ while (i < sql.length && sql[i] !== '\n') i++;
730
+ continue;
731
+ }
732
+ if (c === '/' && sql[i + 1] === '*') {
733
+ i += 2;
734
+ while (i + 1 < sql.length && !(sql[i] === '*' && sql[i + 1] === '/')) i++;
735
+ i += 2;
736
+ continue;
737
+ }
649
738
  if (c === '?' && sql[i + 1] === '?') { i++; continue; }
650
739
  if (c === '?') n++;
651
740
  }
@@ -1000,9 +1089,16 @@ class MysqlServer {
1000
1089
  if (!this._dbExists(db)) await this.createDatabase(db);
1001
1090
  const engine = await this._getDatabase(db);
1002
1091
  let s = sql;
1092
+ // 词法扫描:找出所有字符串字面量和注释的位置,替换时跳过它们
1093
+ const masked = _maskStringsAndComments(s);
1003
1094
  for (const t of tables) {
1004
1095
  if (t && t.indexOf('.') !== -1) {
1005
- s = s.replace(new RegExp('\\b' + t.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '\\b', 'g'), t.split('.')[1]);
1096
+ const re = new RegExp('\\b' + t.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '\\b', 'g');
1097
+ s = s.replace(re, (match, offset) => {
1098
+ // 如果匹配位置在字符串字面量或注释中,不替换
1099
+ if (masked[offset] !== ' ') return match;
1100
+ return t.split('.')[1];
1101
+ });
1006
1102
  }
1007
1103
  }
1008
1104
  return { engine, sql: s };
@@ -1126,8 +1222,12 @@ class MysqlServer {
1126
1222
  }
1127
1223
 
1128
1224
  _canAccessDb(user, dbName) {
1225
+ // 未配置认证:允许访问
1226
+ if (!this.auth) return true;
1129
1227
  const list = this._userDbList(user);
1228
+ // _userDbList 返回 null 表示该用户有全局访问权限
1130
1229
  if (list === null) return true;
1230
+ // 空数组表示用户有受限数据库集,dbName 必须明确在列表中
1131
1231
  return list.includes(dbName);
1132
1232
  }
1133
1233
 
@@ -70,10 +70,10 @@ function matchOperator(value, op, expected) {
70
70
  switch (op) {
71
71
  case '$eq': return looseEqual(value, expected);
72
72
  case '$ne': return !looseEqual(value, expected);
73
- case '$lt': return value !== null && value !== undefined && value < expected;
74
- case '$lte': return value !== null && value !== undefined && value <= expected;
75
- case '$gt': return value !== null && value !== undefined && value > expected;
76
- case '$gte': return value !== null && value !== undefined && value >= expected;
73
+ case '$lt': return value !== undefined && (value === null ? expected !== null && expected !== undefined && 0 < expected : value < expected);
74
+ case '$lte': return value !== undefined && (value === null ? expected !== null && expected !== undefined && 0 <= expected : value <= expected);
75
+ case '$gt': return value !== undefined && (value === null ? expected !== null && expected !== undefined && 0 > expected : value > expected);
76
+ case '$gte': return value !== undefined && (value === null ? expected !== null && expected !== undefined && 0 >= expected : value >= expected);
77
77
  case '$in': return Array.isArray(expected) && expected.some(e => looseEqual(value, e));
78
78
  case '$nin': return Array.isArray(expected) && !expected.some(e => looseEqual(value, e));
79
79
  case '$exists': return expected ? value !== undefined : value === undefined;
@@ -242,8 +242,11 @@ class Cursor {
242
242
 
243
243
  exec(cb) {
244
244
  const p = this._run();
245
- if (cb) p.then(docs => cb(null, docs), err => cb(err)).catch(() => {});
246
- return p;
245
+ if (cb) {
246
+ p.then(docs => { if (this._limit === 0) cb(null, []); else cb(null, docs); }, err => cb(err));
247
+ return p;
248
+ }
249
+ return this._limit === 0 ? Promise.resolve([]) : p;
247
250
  }
248
251
 
249
252
  toArray(cb) {
package/lib/pg_server.js CHANGED
@@ -139,7 +139,7 @@ function buildRowDescription(columns, types) {
139
139
  p.nul();
140
140
  p.i32(0); // table oid
141
141
  p.i16(0); // column attnum
142
- p.i32(TYPE_OIDS[types[i]] || 1043);
142
+ p.i32(pgTypeOid(types[i]));
143
143
  p.i16(types[i] === 'integer' ? 4 : -1); // typlen
144
144
  p.i32(-1); // typmod
145
145
  p.i16(0); // format code (text)
@@ -726,15 +726,11 @@ class PgConnection {
726
726
  }
727
727
 
728
728
  _pgErrorCode(e) {
729
- if (!e) if (/Table .*(doesn't exist|does not exist)/i.test(m)) return '42P01';
730
- return 'XX000';
729
+ if (!e) return 'XX000';
731
730
  const codeKey = e.code;
732
731
  const m = String(e.message || '');
733
- // Debug write to /tmp/pg_error_debug.txt
734
- const fs = require('fs');
735
- fs.writeFileSync('/tmp/pg_error_debug.txt', 'codeKey=' + codeKey + '\\nmessage=' + m + '\\n');
736
732
  // Map JSQL error keys to PG SQLSTATE codes
737
- if (codeKey === 'ER_NO_SUCH_TABLE' || /Table .* doesn\\'t exist/i.test(m)) return '42P01';
733
+ if (codeKey === 'ER_NO_SUCH_TABLE' || /Table .* doesn't exist/i.test(m)) return '42P01';
738
734
  if (codeKey === 'ER_TABLE_EXISTS' || /already exists/i.test(m)) return '42P07';
739
735
  if (codeKey === 'ER_DUP_ENTRY' || m.includes('ER_DUP_ENTRY') || /Duplicate entry/i.test(m)) return '23505';
740
736
  if (codeKey === 'ER_BAD_FIELD_ERROR' || /Unknown column/i.test(m)) return '42703';
@@ -786,10 +782,7 @@ class PgServer {
786
782
 
787
783
  _canAccessDb(user, dbName) {
788
784
  if (this.noAuth) return true;
789
- if (!this.auth) {
790
- // 单用户模式下仅允许该用户访问其默认库
791
- return true;
792
- }
785
+ if (!this.auth) return false;
793
786
  const entry = this.auth[user];
794
787
  if (!entry) return false;
795
788
  if (entry && typeof entry === 'object' && Array.isArray(entry.databases)) {
package/lib/plugin.js CHANGED
@@ -206,6 +206,20 @@ function applyPlugin(engine, plugin, opts) {
206
206
  if (!engine._plugins) engine._plugins = [];
207
207
  if (!engine._eventListeners) engine._eventListeners = [];
208
208
 
209
+ // P3-2: 重复 use() 同一插件钩子双触发——检查是否已注册
210
+ if (engine._plugins.some(p => p.name && p.name === plugin.name)) {
211
+ return engine; // 已注册,跳过
212
+ }
213
+
214
+ // P3-1: plugin depends/priority 未实现——按依赖关系排序
215
+ if (plugin.depends && plugin.depends.length > 0) {
216
+ for (const dep of plugin.depends) {
217
+ if (!engine._plugins.some(p => p.name === dep)) {
218
+ throw new Error(`Plugin '${plugin.name}' depends on '${dep}' which is not loaded`);
219
+ }
220
+ }
221
+ }
222
+
209
223
  registerHooks(engine, plugin);
210
224
 
211
225
  const ctx = buildPluginContext(engine, plugin);
@@ -213,6 +227,8 @@ function applyPlugin(engine, plugin, opts) {
213
227
  if (typeof plugin.onEvent === 'function') engine._eventListeners.push(plugin.onEvent);
214
228
 
215
229
  engine._plugins.push(plugin);
230
+ // P3-1: 按 priority 排序插件(数值大的先执行)
231
+ engine._plugins.sort((a, b) => (b.priority || 0) - (a.priority || 0));
216
232
  if (plugin.name && plugin.api) getPluginApis(engine).set(plugin.name, plugin.api);
217
233
  return engine;
218
234
  }
@@ -222,7 +238,7 @@ function applyPlugin(engine, plugin, opts) {
222
238
  * - 任一钩子返回 false → 中止(返回 false),引擎据此跳过本次操作;
223
239
  * - native 引擎置 `_forbidReentry`,钩子内再次触发引擎操作会抛 ER_PLUGIN_REENTRY。
224
240
  */
225
- function runHooks(engine, hookName, args) {
241
+ async function runHooks(engine, hookName, args) {
226
242
  const hooks = engine._hooks && engine._hooks[hookName];
227
243
  if (!hooks || hooks.length === 0) return true;
228
244
 
@@ -236,7 +252,9 @@ function runHooks(engine, hookName, args) {
236
252
  }
237
253
  try {
238
254
  for (const fn of hooks) {
239
- if (fn(...args) === false) return false;
255
+ // P3-3: runHooks 异步钩子静默失效——支持异步钩子
256
+ const result = await fn(...args);
257
+ if (result === false) return false;
240
258
  }
241
259
  return true;
242
260
  } finally {
package/lib/prepared.js CHANGED
@@ -7,10 +7,20 @@ function createPrepared(engine, sql) {
7
7
  if (typeof sql !== 'string' || sql.length === 0) {
8
8
  throw new Error('prepare() requires a SQL string');
9
9
  }
10
+ // P3-4: parse once 名不副实——缓存解析结果,避免每次调用都重新解析
11
+ let parsed = null;
12
+ let parseError = null;
13
+ try {
14
+ parsed = engine.parse ? engine.parse(sql) : null;
15
+ } catch (e) {
16
+ parseError = e;
17
+ }
10
18
  const bound = {
11
19
  sql,
12
20
  async all(...params) {
13
21
  const values = params.length === 1 && Array.isArray(params[0]) ? params[0] : params;
22
+ if (parseError) throw parseError;
23
+ if (parsed) return engine.execute(parsed, values);
14
24
  return engine.query(sql, values);
15
25
  },
16
26
  async get(...params) {
package/lib/query.js CHANGED
@@ -239,10 +239,17 @@ class Query {
239
239
  rows = this._applyJoinOptimized(rows, join);
240
240
  }
241
241
 
242
- // UNION
242
+ // UNION(去重,标准 SQL 语义)
243
243
  if (this._union) {
244
244
  const unionRows = this._union.get();
245
- rows = [...rows, ...unionRows];
245
+ const seen = new Set(rows.map(r => JSON.stringify(r)));
246
+ for (const r of unionRows) {
247
+ const key = JSON.stringify(r);
248
+ if (!seen.has(key)) {
249
+ seen.add(key);
250
+ rows.push(r);
251
+ }
252
+ }
246
253
  }
247
254
 
248
255
  // GROUP BY
@@ -21,14 +21,14 @@ const fs = require('fs');
21
21
  const path = require('path');
22
22
 
23
23
  const TYPE_SIGNATURES = {
24
- PING: 0, ECHO: 1, SET: 2, SETNX: 2, GET: 1, DEL: -1, EXISTS: -1, KEYS: 1,
24
+ PING: 0, ECHO: 1, SET: -2, SETNX: 2, GET: 1, DEL: -1, EXISTS: -1, KEYS: 1,
25
25
  TYPE: 1, EXPIRE: 2, TTL: 1, PERSIST: 1,
26
26
  INCR: 1, DECR: 1, INCRBY: 2, DECRBY: 2, APPEND: 2, STRLEN: 1,
27
27
  MSET: -2, MGET: -1,
28
28
  HSET: -3, HGET: 2, HGETALL: 1, HDEL: -2, HEXISTS: 2, HLEN: 1, HKEYS: 1, HVALS: 1,
29
29
  LPUSH: -2, RPUSH: -2, LPOP: 1, RPOP: 1, LLEN: 1, LRANGE: 3, LINDEX: 2, LREM: 3,
30
30
  SADD: -2, SREM: -2, SMEMBERS: 1, SISMEMBER: 2, SCARD: 1,
31
- ZADD: -3, ZRANGE: -3, ZREVRANGE: -3, ZSCORE: 2, ZCARD: 1, ZREM: -2, ZINCRBY: 3,
31
+ ZADD: -3, ZRANGE: 3, ZREVRANGE: 3, ZSCORE: 2, ZCARD: 1, ZREM: -2, ZINCRBY: 3,
32
32
  DBSIZE: 0, FLUSHALL: 0, FLUSHDB: 0, SELECT: 1, INFO: 0, AUTH: 1, QUIT: 0,
33
33
  };
34
34
 
@@ -54,7 +54,12 @@ class RedisServer {
54
54
  if (!p || !fs.existsSync(p)) return;
55
55
  try {
56
56
  const raw = JSON.parse(fs.readFileSync(p, 'utf8'));
57
- for (const [k, v] of Object.entries(raw)) this.db.set(k, v);
57
+ for (const [k, v] of Object.entries(raw)) {
58
+ // Restore type info: { _t: 'set', _v: [...] } → Set, { _t: 'date', _v: ... } → Date
59
+ if (v && v._t === 'set' && Array.isArray(v._v)) this.db.set(k, new Set(v._v));
60
+ else if (v && v._t === 'date') this.db.set(k, new Date(v._v));
61
+ else this.db.set(k, v);
62
+ }
58
63
  } catch (_) {}
59
64
  }
60
65
 
@@ -72,18 +77,24 @@ class RedisServer {
72
77
  const out = {};
73
78
  const now = Date.now();
74
79
  for (const [k, v] of this.db) {
75
- if (v.ttl && v.ttl <= now) continue;
76
- out[k] = v;
80
+ if (v && v.ttl && v.ttl <= now) continue;
81
+ // Preserve type info: Set → { _t: 'set', _v: [...] }, Date → ISO string
82
+ if (v instanceof Set) out[k] = { _t: 'set', _v: [...v] };
83
+ else if (v instanceof Date) out[k] = { _t: 'date', _v: v.toISOString() };
84
+ else out[k] = v;
77
85
  }
78
86
  const tmp = p + '.tmp';
79
87
  fs.writeFileSync(tmp, JSON.stringify(out));
80
- fs.renameSync(tmp, p);
88
+ try { fs.renameSync(tmp, p); } catch (e) { fs.unlinkSync(tmp); }
81
89
  }
82
90
 
83
91
  stop() {
84
- if (this.snapshotTimer) clearTimeout(this.snapshotTimer);
92
+ if (this.snapshotTimer) { clearTimeout(this.snapshotTimer); this.snapshotTimer = null; }
85
93
  this._snapshot();
86
- if (this.server) this.server.close();
94
+ if (this.server) {
95
+ this.server.close();
96
+ this.server = null;
97
+ }
87
98
  }
88
99
 
89
100
  /* ---------- value helpers ---------- */
@@ -112,8 +123,33 @@ class RedisServer {
112
123
  this.selected = parseInt(args[0], 10);
113
124
  return 'OK';
114
125
  case 'SET': {
115
- const prev = this._get(args[0]);
116
- this.db.set(args[0], { type: 'string', val: args[1], ttl: null });
126
+ const key = args[0];
127
+ const val = args[1];
128
+ // Parse optional arguments: EX seconds | PX milliseconds | NX | XX
129
+ let ttl = null;
130
+ let nx = false, xx = false;
131
+ for (let i = 2; i < args.length; i++) {
132
+ const opt = args[i].toUpperCase();
133
+ if (opt === 'EX') {
134
+ const secs = parseInt(args[++i], 10);
135
+ if (isNaN(secs) || secs < 0) throw new Error('ERR invalid expire time in set');
136
+ ttl = Date.now() + secs * 1000;
137
+ } else if (opt === 'PX') {
138
+ const ms = parseInt(args[++i], 10);
139
+ if (isNaN(ms) || ms < 0) throw new Error('ERR invalid expire time in set');
140
+ ttl = Date.now() + ms;
141
+ } else if (opt === 'NX') {
142
+ nx = true;
143
+ } else if (opt === 'XX') {
144
+ xx = true;
145
+ } else {
146
+ throw new Error('ERR syntax error');
147
+ }
148
+ }
149
+ const prev = this._get(key);
150
+ if (nx && prev) return null;
151
+ if (xx && !prev) return null;
152
+ this.db.set(key, { type: 'string', val, ttl });
117
153
  this._scheduleSnapshot();
118
154
  return 'OK';
119
155
  }
@@ -184,7 +220,9 @@ class RedisServer {
184
220
  case 'EXPIRE': {
185
221
  const v = this._get(args[0]);
186
222
  if (!v) return 0;
187
- v.ttl = Date.now() + parseInt(args[1], 10) * 1000;
223
+ const secs = parseInt(args[1], 10);
224
+ if (isNaN(secs)) throw new Error('ERR value is not an integer or out of range');
225
+ v.ttl = Date.now() + secs * 1000;
188
226
  this._scheduleSnapshot();
189
227
  return 1;
190
228
  }
@@ -301,10 +339,26 @@ class RedisServer {
301
339
  const target = args[2];
302
340
  const out = [];
303
341
  let removed = 0;
304
- const remaining = Math.abs(count);
305
- for (const el of v.val) {
306
- if (el === target && (count === 0 || removed < remaining)) { removed++; continue; }
307
- out.push(el);
342
+ if (count > 0) {
343
+ // 从头删除
344
+ const maxRemove = count;
345
+ for (const el of v.val) {
346
+ if (el === target && removed < maxRemove) { removed++; continue; }
347
+ out.push(el);
348
+ }
349
+ } else if (count < 0) {
350
+ // 从尾删除
351
+ const maxRemove = Math.abs(count);
352
+ for (let i = v.val.length - 1; i >= 0; i--) {
353
+ if (v.val[i] === target && removed < maxRemove) { removed++; continue; }
354
+ out.unshift(v.val[i]);
355
+ }
356
+ } else {
357
+ // count === 0: 删除所有
358
+ for (const el of v.val) {
359
+ if (el === target) { removed++; continue; }
360
+ out.push(el);
361
+ }
308
362
  }
309
363
  v.val = out;
310
364
  this._scheduleSnapshot();
@@ -438,7 +492,14 @@ class RedisServer {
438
492
  return v && v.type === 'string' ? v.val : null;
439
493
  });
440
494
  }
441
- case 'DBSIZE': return this.db.size;
495
+ case 'DBSIZE': {
496
+ let count = 0;
497
+ const now = Date.now();
498
+ for (const v of this.db.values()) {
499
+ if (!v.ttl || v.ttl > now) count++;
500
+ }
501
+ return count;
502
+ }
442
503
  case 'FLUSHALL': case 'FLUSHDB':
443
504
  this.db.clear();
444
505
  this._scheduleSnapshot();
@@ -563,6 +624,17 @@ class RedisServer {
563
624
  return this._send(socket, new Error('NOAUTH Authentication required.'));
564
625
  }
565
626
  if (cmd === 'QUIT') { this._send(socket, 'OK'); socket.end(); return; }
627
+ if (cmd === 'GET') {
628
+ const r = this.execute(cmd, args);
629
+ if (r === null) {
630
+ this._send(socket, null);
631
+ } else {
632
+ // GET 返回值必须是 bulk string,即使内容是 "OK" 或 "PONG"
633
+ const buf = Buffer.from(String(r), 'utf8');
634
+ socket.write('$' + buf.length + '\r\n' + r + '\r\n');
635
+ }
636
+ return;
637
+ }
566
638
  const r = this.execute(cmd, args);
567
639
  this._send(socket, r);
568
640
  } catch (e) {