jsql-neo 6.3.10 → 6.4.0-beta.1

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.3.10** — official release build · [github.com/vexify-org/JSQL-neo](https://github.com/vexify-org/JSQL-neo) · [jsql.vexify.top](https://jsql.vexify.top)
6
+ > **v6.4.0-beta.1** — official release build · [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)
@@ -112,6 +112,27 @@ const db = new Database(':memory:');
112
112
  // same API as Node.js
113
113
  ```
114
114
 
115
+ ### 🦀 NativeJSQL
116
+
117
+ Pure Rust native bindings — the highest-performance way to run JSQL-NEO:
118
+
119
+ - **Zero abstraction overhead** — direct calls into the Rust core
120
+ - **Batch operations** — up to **1.54M rows/sec** peak throughput
121
+ - **Automatic memory management** with smart caching
122
+
123
+ **Install:**
124
+
125
+ ```bash
126
+ npm install jsql-neo
127
+ ```
128
+
129
+ **Usage:**
130
+
131
+ ```javascript
132
+ import { Database } from 'jsql-neo';
133
+ const db = new Database('mydb.jsql');
134
+ ```
135
+
115
136
  ### Storage modes
116
137
 
117
138
  | Mode | Usage | Use Case |
package/bin/jsql CHANGED
@@ -247,6 +247,96 @@ const cli = yaggs({ pkg: require('../package.json') })
247
247
  console.log(`jsql-neo Redis server on ${argv.host || '127.0.0.1'}:${argv.port != null ? argv.port : 6379}${argv['data-dir'] ? ' (data: ' + argv['data-dir'] + ')' : ''}`);
248
248
  setInterval(() => {}, 1 << 30);
249
249
  })
250
+ .command('plugin', 'Plugin registry management (list, search, install, uninstall, info)', (sub) => {
251
+ sub.option('json', { type: 'boolean', description: 'Output as JSON' });
252
+ sub.option('category', { type: 'string', description: 'Filter by category' });
253
+ sub.option('limit', { type: 'number', description: 'Max results per page' });
254
+ }, async (argv) => {
255
+ const pluginMgr = require('../src/cli/plugin');
256
+ const action = argv._[0];
257
+ const arg = argv._[1];
258
+ const useJson = !!argv.json;
259
+
260
+ const fail = (e) => { console.error(`Error: ${e.message}`); process.exitCode = 1; };
261
+ const out = (obj) => {
262
+ if (obj === undefined) return;
263
+ if (useJson) console.log(JSON.stringify(obj, null, 2));
264
+ else console.log(obj);
265
+ };
266
+
267
+ try {
268
+ switch (action) {
269
+ case 'list':
270
+ case 'ls': {
271
+ const opts = {};
272
+ if (argv.category) opts.category = argv.category;
273
+ if (argv.limit) opts.limit = argv.limit;
274
+ const plugins = await pluginMgr.listPlugins(opts);
275
+ out(useJson ? plugins : pluginMgr.formatPluginList(plugins));
276
+ break;
277
+ }
278
+ case 'search': {
279
+ if (!arg) throw new Error('Usage: jsql plugin search <query>');
280
+ const opts = {};
281
+ if (argv.category) opts.category = argv.category;
282
+ const data = await pluginMgr.searchPlugins(arg, opts);
283
+ // search 返回 { results: [...], query, total } 或 [...]
284
+ const plugins = Array.isArray(data) ? data : (data.results || []);
285
+ if (useJson) {
286
+ out(data);
287
+ } else {
288
+ if (!plugins.length) {
289
+ console.log(`No plugins found for '${arg}'.`);
290
+ } else {
291
+ console.log(`Found ${plugins.length} plugin(s) for '${arg}':`);
292
+ console.log(pluginMgr.formatPluginList(plugins.map(p => p.plugin || p)));
293
+ }
294
+ }
295
+ break;
296
+ }
297
+ case 'install': {
298
+ if (!arg) throw new Error('Usage: jsql plugin install <name>');
299
+ const manifest = await pluginMgr.installPlugin(arg);
300
+ out(useJson ? manifest : `Installed plugin '${manifest.name}' (${manifest.version})`);
301
+ break;
302
+ }
303
+ case 'uninstall':
304
+ case 'remove': {
305
+ if (!arg) throw new Error('Usage: jsql plugin uninstall <name>');
306
+ const r = pluginMgr.uninstallPlugin(arg);
307
+ out(useJson ? r : `Uninstalled plugin '${r.name}'.`);
308
+ break;
309
+ }
310
+ case 'info': {
311
+ if (!arg) throw new Error('Usage: jsql plugin info <name>');
312
+ const p = await pluginMgr.getPluginInfo(arg);
313
+ out(useJson ? p : pluginMgr.formatPluginInfo(p));
314
+ break;
315
+ }
316
+ case undefined:
317
+ // 无子命令 → 显示已安装插件
318
+ const installed = pluginMgr.listInstalled();
319
+ if (useJson) {
320
+ out(installed);
321
+ } else {
322
+ if (!installed.length) {
323
+ console.log('No plugins installed.');
324
+ console.log(`Plugin directory: ${pluginMgr.PLUGIN_DIR}`);
325
+ } else {
326
+ console.log(`Installed plugins (${installed.length}):`);
327
+ installed.forEach(p => {
328
+ console.log(` ${p.name.padEnd(24)} ${p.version || '?'}`);
329
+ });
330
+ }
331
+ }
332
+ break;
333
+ default:
334
+ throw new Error(`Unknown plugin action: ${action}`);
335
+ }
336
+ } catch (e) {
337
+ fail(e);
338
+ }
339
+ })
250
340
  .command('version', 'Show version', null, () => {
251
341
  console.log(require('../package.json').version);
252
342
  })
package/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * JSQL-NEO v6.3.10 — Rust-Powered Embedded Database (WASM + HTTP)
2
+ * JSQL-NEO v6.4.0-beta.1 — Rust-Powered Embedded Database (WASM + HTTP)
3
3
  *
4
4
  * @example
5
5
  * const jsql = require('jsql-neo');
package/lib/cache.js CHANGED
@@ -50,7 +50,7 @@ class Cache {
50
50
  if (typeof val === 'string') return 'string';
51
51
  if (val instanceof Set) return 'set';
52
52
  if (Array.isArray(val)) return val._type === 'hash' ? 'hash' : 'list';
53
- if (typeof val === 'object' && val._type === 'hash') return 'hash';
53
+ if (val !== null && typeof val === 'object' && val._type === 'hash') return 'hash';
54
54
  return 'string';
55
55
  }
56
56
  expire(key, seconds) {
@@ -85,7 +85,10 @@ class Cache {
85
85
  hset(key, field, value) {
86
86
  const k = String(key);
87
87
  let hash = this._getData().get(k);
88
- if (!hash || hash._type !== 'hash') { hash = { _type: 'hash' }; this._getData().set(k, hash); }
88
+ if (hash !== undefined && (typeof hash !== 'object' || hash._type !== 'hash')) {
89
+ throw new Error('WRONGTYPE Operation against a key holding the wrong kind of value');
90
+ }
91
+ if (!hash) { hash = { _type: 'hash' }; this._getData().set(k, hash); }
89
92
  const existed = hash[field] !== undefined;
90
93
  hash[field] = String(value);
91
94
  this._dirty = true;
@@ -134,7 +137,10 @@ class Cache {
134
137
  lpush(key, ...values) {
135
138
  const k = String(key);
136
139
  let list = this._getData().get(k);
137
- if (!list || !Array.isArray(list) || list._type === 'hash') { list = []; this._getData().set(k, list); }
140
+ if (list !== undefined && (!Array.isArray(list) || list._type === 'hash')) {
141
+ throw new Error('WRONGTYPE Operation against a key holding the wrong kind of value');
142
+ }
143
+ if (!list) { list = []; this._getData().set(k, list); }
138
144
  list.unshift(...values.map(String).reverse());
139
145
  this._dirty = true;
140
146
  return list.length;
@@ -142,7 +148,10 @@ class Cache {
142
148
  rpush(key, ...values) {
143
149
  const k = String(key);
144
150
  let list = this._getData().get(k);
145
- if (!list || !Array.isArray(list) || list._type === 'hash') { list = []; this._getData().set(k, list); }
151
+ if (list !== undefined && (!Array.isArray(list) || list._type === 'hash')) {
152
+ throw new Error('WRONGTYPE Operation against a key holding the wrong kind of value');
153
+ }
154
+ if (!list) { list = []; this._getData().set(k, list); }
146
155
  list.push(...values.map(String));
147
156
  this._dirty = true;
148
157
  return list.length;
@@ -185,7 +194,10 @@ class Cache {
185
194
  sadd(key, ...members) {
186
195
  const k = String(key);
187
196
  let set = this._getData().get(k);
188
- if (!set || !(set instanceof Set)) { set = new Set(); this._getData().set(k, set); }
197
+ if (set !== undefined && !(set instanceof Set)) {
198
+ throw new Error('WRONGTYPE Operation against a key holding the wrong kind of value');
199
+ }
200
+ if (!set) { set = new Set(); this._getData().set(k, set); }
189
201
  let count = 0;
190
202
  for (const m of members) { if (!set.has(String(m))) { set.add(String(m)); count++; } }
191
203
  if (count > 0) this._dirty = true;
@@ -223,10 +235,11 @@ class Cache {
223
235
  }
224
236
 
225
237
  flushall() {
238
+ const currentDb = this._db;
226
239
  for (let i = 0; i < this._dbs.length; i++) { if (this._dbs[i]) this._dbs[i].clear(); if (this._dbsExpires[i]) this._dbsExpires[i].clear(); }
227
- this._data = this._dbs[0] = new Map();
228
- this._expires = this._dbsExpires[0] = new Map();
229
- this._db = 0;
240
+ this._data = this._dbs[currentDb] = new Map();
241
+ this._expires = this._dbsExpires[currentDb] = new Map();
242
+ this._db = currentDb;
230
243
  this._dirty = true;
231
244
  return 'OK';
232
245
  }
package/lib/database.js CHANGED
@@ -1710,7 +1710,8 @@ class Database {
1710
1710
  if (!table) throw createError('ER_NO_SUCH_TABLE', tableName);
1711
1711
  if (!this._runHooks('beforeFind', [tableName, { filter, opts }])) return [];
1712
1712
  const { limit, offset = 0 } = opts;
1713
- let rows = table._applyFilterOptimized(table._rows, filter || {});
1713
+ // P1-4: 软删除模式下不得泄露已删除行——只看可见行
1714
+ let rows = table._applyFilterOptimized(table._getVisibleRows(), filter || {});
1714
1715
  if (limit > 0) rows = rows.slice(offset, offset + limit);
1715
1716
  else if (offset > 0) rows = rows.slice(offset);
1716
1717
  // P1-11: 返回浅拷贝,防止调用方修改影响内部数据
@@ -1744,6 +1745,8 @@ class Database {
1744
1745
  if (!table) throw createError('ER_NO_SUCH_TABLE', tableName);
1745
1746
  if (!this._runHooks('beforeFind', [tableName, { id }])) return null;
1746
1747
  const row = this._resolveId(table, id);
1748
+ // P1-4: 软删除行对普通读取不可见
1749
+ if (row && row._deleted) return null;
1747
1750
  // P1-11: 返回浅拷贝,防止调用方修改影响内部数据
1748
1751
  const result = row ? { ...row } : null;
1749
1752
  this._touchTable(tableName);
@@ -1755,8 +1758,8 @@ class Database {
1755
1758
  const table = this._ensureTable(tableName);
1756
1759
  if (!table) throw createError('ER_NO_SUCH_TABLE', tableName);
1757
1760
  if (!this._runHooks('beforeFind', [tableName, { ids }])) return null;
1758
- // P1-11: 返回浅拷贝
1759
- const rows = ids.map(id => this._resolveId(table, id)).filter(r => r !== null).map(r => ({ ...r }));
1761
+ // P1-4: 过滤掉软删除行;P1-11: 返回浅拷贝
1762
+ const rows = ids.map(id => this._resolveId(table, id)).filter(r => r !== null && !r._deleted).map(r => ({ ...r }));
1760
1763
  this._touchTable(tableName);
1761
1764
  this._runHooks('afterFind', [tableName, { ids }, rows]);
1762
1765
  return rows;
@@ -1770,7 +1773,8 @@ class Database {
1770
1773
  const table = this._ensureTable(tableName);
1771
1774
  if (!table) throw createError('ER_NO_SUCH_TABLE', tableName);
1772
1775
  this._runHooks('beforeCount', [tableName]);
1773
- const n = table._rows.length;
1776
+ // P1-4: 软删除模式下只统计可见行
1777
+ const n = table._softDelete ? table._getVisibleRows().length : table._rows.length;
1774
1778
  this._touchTable(tableName);
1775
1779
  this._runHooks('afterCount', [tableName, n]);
1776
1780
  return n;
@@ -1783,7 +1787,8 @@ class Database {
1783
1787
  // P1-9: updateById 绕过类型校验——先校验再更新
1784
1788
  const validated = table._validateDataTypes({ ...data }, true);
1785
1789
  const row = this._resolveId(table, id);
1786
- if (!row) return { ok: false, error: 'not found' };
1790
+ // P1-4: 软删除行不可更新
1791
+ if (!row || row._deleted) return { ok: false, error: 'not found' };
1787
1792
  // 改主键/唯一列时要先确认新值不与其它行冲突。
1788
1793
  // 此前直接 Object.assign,导致
1789
1794
  // UPDATE t SET id = 1 WHERE id = 2 (t 已有 id=1)
@@ -1893,7 +1898,8 @@ class Database {
1893
1898
  if (!table) throw createError('ER_NO_SUCH_TABLE', tableName);
1894
1899
  if (!this._runHooks('beforeDelete', [tableName, [filter]])) return { ok: true, count: 0 };
1895
1900
  const before = table._rows.length;
1896
- const matched = table._applyFilter(table._rows, filter || {});
1901
+ // P1-4: 只匹配可见行(软删除行已“不存在”,不应被硬删操作波及)
1902
+ const matched = table._applyFilter(table._getVisibleRows(), filter || {});
1897
1903
  const matchSet = new Set(matched);
1898
1904
  table._rows = table._rows.filter(r => !matchSet.has(r));
1899
1905
  const removed = before - table._rows.length;
package/lib/date-types.js CHANGED
@@ -199,9 +199,12 @@ function validateTime(value, fieldName) {
199
199
  return value;
200
200
  }
201
201
  }
202
- // 负时间: -HH:MM:SS
202
+ // 负时间: -HH:MM:SS(同样校验范围)
203
203
  const neg = value.match(/^-(\d{2}):(\d{2}):(\d{2})$/);
204
- if (neg) return value;
204
+ if (neg) {
205
+ const h = parseInt(neg[1]), min = parseInt(neg[2]), sec = parseInt(neg[3]);
206
+ if (h >= 0 && h < 24 && min >= 0 && min < 60 && sec >= 0 && sec < 60) return value;
207
+ }
205
208
  }
206
209
  throw createError('ER_INVALID_DATE', fieldName, String(value));
207
210
  }
@@ -12,12 +12,19 @@ const SZ = {
12
12
 
13
13
  const BT = { FREE: 0, DATA: 1, IDX: 2 };
14
14
 
15
- const TYPE = { NUL:0, BOL:1, I32:2, F64:3, STR:4, BUF:5, ARR:6, OBJ:7 };
15
+ const TYPE = { NUL:0, BOL:1, I32:2, F64:3, STR:4, BUF:5, ARR:6, OBJ:7, I64:8 };
16
16
 
17
17
  function tag(v) {
18
18
  if (v===null||v===undefined) return TYPE.NUL;
19
19
  if (typeof v==='boolean') return TYPE.BOL;
20
- if (typeof v==='number') return Number.isInteger(v)?TYPE.I32:TYPE.F64;
20
+ if (typeof v==='number') {
21
+ if (Number.isInteger(v)) {
22
+ // P2-29-fix: 超出 int32 范围的整数用 I64 存储
23
+ if (v >= -0x80000000 && v <= 0x7FFFFFFF) return TYPE.I32;
24
+ return TYPE.I64;
25
+ }
26
+ return TYPE.F64;
27
+ }
21
28
  if (typeof v==='string') return TYPE.STR;
22
29
  if (v instanceof Buffer) return TYPE.BUF;
23
30
  return Array.isArray(v)?TYPE.ARR:TYPE.OBJ;
@@ -29,6 +36,7 @@ function enc(v) {
29
36
  case TYPE.NUL: return Buffer.from([t]);
30
37
  case TYPE.BOL: return Buffer.from([t, v?1:0]);
31
38
  case TYPE.I32: { const b=Buffer.alloc(5); b[0]=t; b.writeInt32LE(v,1); return b; }
39
+ case TYPE.I64: { const b=Buffer.alloc(9); b[0]=t; b.writeBigInt64LE(BigInt(v),1); return b; }
32
40
  case TYPE.F64: { const b=Buffer.alloc(9); b[0]=t; b.writeDoubleLE(v,1); return b; }
33
41
  case TYPE.STR: { const s=Buffer.from(v,'utf8'); const b=Buffer.alloc(5+s.length); b[0]=t; b.writeUInt32LE(s.length,1); s.copy(b,5); return b; }
34
42
  case TYPE.BUF: { const b=Buffer.alloc(5+v.length); b[0]=t; b.writeUInt32LE(v.length,1); v.copy(b,5); return b; }
@@ -43,6 +51,7 @@ function dec(buf, off) {
43
51
  case TYPE.NUL: return {v:null,off};
44
52
  case TYPE.BOL: return {v:buf[off]!==0,off:off+1};
45
53
  case TYPE.I32: return {v:buf.readInt32LE(off),off:off+4};
54
+ case TYPE.I64: return {v:Number(buf.readBigInt64LE(off)),off:off+8};
46
55
  case TYPE.F64: return {v:buf.readDoubleLE(off),off:off+8};
47
56
  case TYPE.STR: { const l=buf.readUInt32LE(off);off+=4; return {v:buf.toString('utf8',off,off+l),off:off+l}; }
48
57
  case TYPE.BUF: { const l=buf.readUInt32LE(off);off+=4; return {v:Buffer.from(buf.subarray(off,off+l)),off:off+l}; }
@@ -62,7 +71,11 @@ function writeEnc(buf, off, v) {
62
71
  if (v === null || v === undefined) { buf[off] = 0; return off + 1; }
63
72
  if (typeof v === 'boolean') { buf[off] = 1; buf[off+1] = v ? 1 : 0; return off + 2; }
64
73
  if (typeof v === 'number') {
65
- if (Number.isInteger(v)) { buf[off] = 2; buf.writeInt32LE(v, off+1); return off + 5; }
74
+ if (Number.isInteger(v)) {
75
+ // P2-29-fix: 超出 int32 范围的整数用 I64 存储
76
+ if (v >= -0x80000000 && v <= 0x7FFFFFFF) { buf[off] = 2; buf.writeInt32LE(v, off+1); return off + 5; }
77
+ buf[off] = 8; buf.writeBigInt64LE(BigInt(v), off+1); return off + 9;
78
+ }
66
79
  buf[off] = 3; buf.writeDoubleLE(v, off+1); return off + 9;
67
80
  }
68
81
  if (typeof v === 'string') {
@@ -414,8 +427,7 @@ class JSQLFormat {
414
427
  }
415
428
  if (buf[36] !== BT.DATA || buf[37] !== t.tableId) continue;
416
429
  if (!verifyHash(buf)) {
417
- console.warn(`[JSQLFormat] Table '${name}': data block ${bid} failed integrity check, skipping`);
418
- continue;
430
+ throw new Error(`[JSQLFormat] Table '${name}': data block ${bid} failed integrity check`);
419
431
  }
420
432
 
421
433
  const compFlag = buf[48];
@@ -154,6 +154,7 @@ class MongoServer {
154
154
  this._engine = null;
155
155
  this._server = null;
156
156
  this._sockets = new Set();
157
+ this._dbCollections = new Map(); // db name → Set of collection names
157
158
  }
158
159
 
159
160
  async _getEngine() {
@@ -166,11 +167,15 @@ class MongoServer {
166
167
  return this._engine;
167
168
  }
168
169
 
169
- async _ensureCollection(name) {
170
+ async _ensureCollection(name, db = null) {
170
171
  const engine = await this._getEngine();
171
172
  if (!engine.getTableSchema(name)) {
172
173
  engine.createTable(name, {});
173
174
  }
175
+ if (db) {
176
+ if (!this._dbCollections.has(db)) this._dbCollections.set(db, new Set());
177
+ this._dbCollections.get(db).add(name);
178
+ }
174
179
  return engine;
175
180
  }
176
181
 
@@ -339,7 +344,7 @@ class MongoServer {
339
344
  return { ok: 1, cursor: { id: { $long: 0 }, ns: `${this._dbOf(doc)}.$cmd.listCollections`, firstBatch } };
340
345
  }
341
346
  case 'create': {
342
- const engine = await this._ensureCollection(String(doc.create));
347
+ const engine = await this._ensureCollection(String(doc.create), this._dbOf(doc));
343
348
  return { ok: 1, ns: engine ? `${this._dbOf(doc)}.${doc.create}` : '' };
344
349
  }
345
350
  case 'drop': {
@@ -353,7 +358,7 @@ class MongoServer {
353
358
  }
354
359
  case 'insert': {
355
360
  const coll = String(doc.insert);
356
- const engine = await this._ensureCollection(coll);
361
+ const engine = await this._ensureCollection(coll, this._dbOf(doc));
357
362
  const docs = doc.documents || [];
358
363
  const ids = [];
359
364
  for (const d of docs) {
@@ -369,7 +374,7 @@ class MongoServer {
369
374
  }
370
375
  case 'findAndModify': {
371
376
  const coll = String(doc.findAndModify);
372
- const engine = await this._ensureCollection(coll);
377
+ const engine = await this._ensureCollection(coll, this._dbOf(doc));
373
378
  const q = doc.query || {};
374
379
  const rows = this._matching(engine, coll, q);
375
380
  const found = rows[0] || null;
@@ -393,7 +398,7 @@ class MongoServer {
393
398
  }
394
399
  case 'distinct': {
395
400
  const coll = String(doc.distinct);
396
- const engine = await this._ensureCollection(coll);
401
+ const engine = await this._ensureCollection(coll, this._dbOf(doc));
397
402
  const key = String(doc.key || '');
398
403
  const rows = this._matching(engine, coll, doc.query);
399
404
  const seen = new Set();
@@ -404,11 +409,21 @@ class MongoServer {
404
409
  }
405
410
  return { ok: 1, values };
406
411
  }
407
- case 'dropDatabase':
408
- return { ok: 1, dropped: String(doc.dropDatabase || this._dbOf(doc)) };
412
+ case 'dropDatabase': {
413
+ const dbName = String(doc.$db || 'test');
414
+ const engine = await this._getEngine();
415
+ const colls = this._dbCollections.get(dbName);
416
+ if (colls) {
417
+ for (const c of colls) {
418
+ try { engine.dropTable(c); } catch (_) {}
419
+ }
420
+ this._dbCollections.delete(dbName);
421
+ }
422
+ return { ok: 1, dropped: dbName };
423
+ }
409
424
  case 'find': {
410
425
  const coll = String(doc.find);
411
- const engine = await this._ensureCollection(coll);
426
+ const engine = await this._ensureCollection(coll, this._dbOf(doc));
412
427
  let rows = this._matching(engine, coll, doc.filter);
413
428
  const skip = typeof doc.skip === 'number' ? doc.skip : 0;
414
429
  if (skip > 0) rows = rows.slice(skip);
@@ -420,7 +435,7 @@ class MongoServer {
420
435
  return { ok: 1, cursor: { id: { $long: 0 }, ns: String(doc.collection || ''), nextBatch: [] } };
421
436
  case 'update': {
422
437
  const coll = String(doc.update);
423
- const engine = await this._ensureCollection(coll);
438
+ const engine = await this._ensureCollection(coll, this._dbOf(doc));
424
439
  let n = 0;
425
440
  const updates = doc.updates || [];
426
441
  for (const u of updates) {
@@ -453,24 +468,26 @@ class MongoServer {
453
468
  }
454
469
  case 'delete': {
455
470
  const coll = String(doc.delete);
456
- const engine = await this._ensureCollection(coll);
471
+ const engine = await this._ensureCollection(coll, this._dbOf(doc));
457
472
  let n = 0;
458
473
  for (const d of (doc.deletes || [])) {
459
474
  const rows = this._matching(engine, coll, d.q || {});
460
- if (rows.length > 0) this._removeRows(engine, coll, rows);
461
- n += rows.length;
475
+ // MongoDB delete: limit=1 means delete only one, limit=0 means delete all
476
+ const toDelete = d.limit === 1 ? rows.slice(0, 1) : rows;
477
+ if (toDelete.length > 0) this._removeRows(engine, coll, toDelete);
478
+ n += toDelete.length;
462
479
  }
463
480
  return { ok: 1, n };
464
481
  }
465
482
  case 'count': {
466
483
  const coll = String(doc.count);
467
- const engine = await this._ensureCollection(coll);
484
+ const engine = await this._ensureCollection(coll, this._dbOf(doc));
468
485
  const rows = this._matching(engine, coll, doc.query);
469
486
  return { ok: 1, n: Number(doc.limit) > 0 ? Math.min(rows.length, doc.limit) : rows.length };
470
487
  }
471
488
  case 'aggregate': {
472
489
  const coll = String(doc.aggregate);
473
- const engine = await this._ensureCollection(coll);
490
+ const engine = await this._ensureCollection(coll, this._dbOf(doc));
474
491
  const pipeline = doc.pipeline || [];
475
492
  let rows = engine.find(coll, {});
476
493
  for (const stage of pipeline) {
@@ -19,7 +19,7 @@ function toResultPacket(r, table, rowsAsArray) {
19
19
  // P3-9: fields 类型全 253——根据实际类型设置
20
20
  const fields = columns.map(name => {
21
21
  let type = 253; // VAR_STRING
22
- const val = values[0] ? values[0][name] : null;
22
+ const val = rawRows[0] ? rawRows[0][columns.indexOf(name)] : null;
23
23
  if (typeof val === 'number') type = 8; // LONGLONG
24
24
  else if (typeof val === 'boolean') type = 1; // TINY
25
25
  else if (val instanceof Date) type = 12; // DATETIME
@@ -73,6 +73,7 @@ class Connection extends EventEmitter {
73
73
  this.filename = options.filename || null;
74
74
  this.engine = null;
75
75
  this._ownEngine = false;
76
+ this._connectEmitted = false;
76
77
  this.state = 'disconnected';
77
78
  this.stream = {
78
79
  destroyed: false,
@@ -131,6 +131,12 @@ function parseLenenc(buf, offset) {
131
131
  if (first === 0xfb) return { value: null, size: 1 };
132
132
  if (first === 0xfc) return { value: buf.readUInt16LE(offset + 1), size: 3 };
133
133
  if (first === 0xfd) return { value: buf.readUIntLE(offset + 1, 3), size: 4 };
134
+ // 0xfe: 8-byte int64
135
+ if (first === 0xfe) {
136
+ const low = buf.readUInt32LE(offset + 1);
137
+ const high = buf.readUInt32LE(offset + 5);
138
+ return { value: low + high * 4294967296, size: 9 };
139
+ }
134
140
  return { value: buf.readUInt32LE(offset + 1), size: 5 };
135
141
  }
136
142
 
@@ -53,7 +53,7 @@ function arraysEqual(a, b) {
53
53
  function looseEqual(a, b) {
54
54
  if (a === b) return true;
55
55
  if (a === null || b === null || a === undefined || b === undefined) return false;
56
- if (typeof a === 'number' || typeof b === 'number') {
56
+ if (typeof a === 'number' && typeof b === 'number') {
57
57
  const na = Number(a), nb = Number(b);
58
58
  if (!Number.isNaN(na) && !Number.isNaN(nb)) return na === nb;
59
59
  }
@@ -63,17 +63,17 @@ function looseEqual(a, b) {
63
63
  if (ka.length !== kb.length) return false;
64
64
  return ka.every(k => looseEqual(a[k], b[k]));
65
65
  }
66
- return String(a) === String(b);
66
+ return false;
67
67
  }
68
68
 
69
69
  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 !== 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);
73
+ case '$lt': return value !== undefined && (value === null ? 0 < expected : value < expected);
74
+ case '$lte': return value !== undefined && (value === null ? 0 <= expected : value <= expected);
75
+ case '$gt': return value !== undefined && (value === null ? 0 > expected : value > expected);
76
+ case '$gte': return value !== undefined && (value === null ? 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;
package/lib/query.js CHANGED
@@ -232,7 +232,8 @@ class Query {
232
232
  return this._cache.result;
233
233
  }
234
234
 
235
- let rows = this._table._applyFilterOptimized(this._table._rows, this._conditions);
235
+ // P1-4: 软删除模式下 QueryBuilder 也不得泄露已删除行
236
+ let rows = this._table._applyFilterOptimized(this._table._getVisibleRows(), this._conditions);
236
237
 
237
238
  // JOIN
238
239
  for (const join of this._joins) {
@@ -26,7 +26,7 @@ const TYPE_SIGNATURES = {
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
- LPUSH: -2, RPUSH: -2, LPOP: 1, RPOP: 1, LLEN: 1, LRANGE: 3, LINDEX: 2, LREM: 3,
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
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,
@@ -167,17 +167,22 @@ class RedisServer {
167
167
  return v.val;
168
168
  }
169
169
  case 'APPEND': {
170
+ const argCount = args.length;
171
+ if (argCount > 2) throw new Error(`ERR wrong number of arguments for 'append' command`);
170
172
  const v = this._get(args[0]);
171
- const prev = v && v.type === 'string' ? v.val : '';
173
+ // N2-fix: 对非 string 类型的 key(list/hash/set/zset)必须报 WRONGTYPE,
174
+ // 此前静默把整值覆盖成 string,丢数据且与真 Redis 行为不符。
175
+ if (v && v.type !== 'string') throw new Error('WRONGTYPE Operation against a key holding the wrong kind of value');
176
+ const prev = v ? v.val : '';
172
177
  this.db.set(args[0], { type: 'string', val: prev + args[1], ttl: v ? v.ttl : null });
173
178
  this._scheduleSnapshot();
174
- return this.db.get(args[0]).val.length;
179
+ return Buffer.byteLength(this.db.get(args[0]).val, 'utf8');
175
180
  }
176
181
  case 'STRLEN': {
177
182
  const v = this._get(args[0]);
178
183
  if (!v) return 0;
179
184
  if (v.type !== 'string') throw new Error('WRONGTYPE Operation against a key holding the wrong kind of value');
180
- return v.val.length;
185
+ return Buffer.byteLength(v.val, 'utf8');
181
186
  }
182
187
  case 'INCR': case 'DECR': case 'INCRBY': case 'DECRBY': {
183
188
  const by = cmd === 'INCRBY' || cmd === 'DECRBY' ? parseInt(args[1], 10) : 1;
@@ -241,7 +246,8 @@ class RedisServer {
241
246
  case 'HSET': {
242
247
  if (args.length % 2 !== 1) throw new Error(`ERR wrong number of arguments for 'hset' command`);
243
248
  const v = this._get(args[0]);
244
- const h = (v && v.type === 'hash') ? { ...v.val } : {};
249
+ if (v && v.type !== 'hash') throw new Error('WRONGTYPE Operation against a key holding the wrong kind of value');
250
+ const h = v ? { ...v.val } : {};
245
251
  let n = 0;
246
252
  for (let i = 1; i < args.length; i += 2) {
247
253
  if (!(args[i] in h)) n++;
@@ -290,7 +296,8 @@ class RedisServer {
290
296
  }
291
297
  case 'LPUSH': case 'RPUSH': {
292
298
  const v = this._get(args[0]);
293
- const list = (v && v.type === 'list') ? [...v.val] : [];
299
+ if (v && v.type !== 'list') throw new Error('WRONGTYPE Operation against a key holding the wrong kind of value');
300
+ const list = v ? [...v.val] : [];
294
301
  if (cmd === 'LPUSH') {
295
302
  for (const val of args.slice(1)) list.unshift(val);
296
303
  } else {
@@ -301,16 +308,32 @@ class RedisServer {
301
308
  return list.length;
302
309
  }
303
310
  case 'LPOP': case 'RPOP': {
311
+ // N3-fix: 支持可选 count 参数(Redis 6.2+)。旧实现忽略 count,LPOP key 2 只弹 1 个。
312
+ // 无 count:弹 1 个,key 不存在时返回 nil(bulk null)。
313
+ // 有 count:返回数组(最多 count 个元素),key 不存在时返回空数组;count<0 报错。
314
+ const hasCount = args.length >= 2;
315
+ let count = 1;
316
+ if (hasCount) {
317
+ count = parseInt(args[1], 10);
318
+ if (isNaN(count) || count < 0) throw new Error('ERR value is out of range, must be positive');
319
+ }
304
320
  const v = this._get(args[0]);
305
- if (!v) return null;
321
+ if (!v) return hasCount ? [] : null;
306
322
  if (v.type !== 'list') throw new Error('WRONGTYPE Operation against a key holding the wrong kind of value');
307
- const el = cmd === 'LPOP' ? v.val.shift() : v.val.pop();
323
+ const popped = [];
324
+ for (let i = 0; i < count && v.val.length > 0; i++) {
325
+ popped.push(cmd === 'LPOP' ? v.val.shift() : v.val.pop());
326
+ }
327
+ if (v.val.length === 0) this.db.delete(args[0]);
308
328
  this._scheduleSnapshot();
309
- return el === undefined ? null : el;
329
+ if (hasCount) return popped;
330
+ return popped.length ? popped[0] : null;
310
331
  }
311
332
  case 'LLEN': {
312
333
  const v = this._get(args[0]);
313
- return v && v.type === 'list' ? v.val.length : 0;
334
+ if (!v) return 0;
335
+ if (v.type !== 'list') throw new Error('WRONGTYPE Operation against a key holding the wrong kind of value');
336
+ return v.val.length;
314
337
  }
315
338
  case 'LRANGE': {
316
339
  const v = this._get(args[0]);
@@ -366,7 +389,8 @@ class RedisServer {
366
389
  }
367
390
  case 'SADD': case 'SREM': {
368
391
  const v = this._get(args[0]);
369
- const set = (v && v.type === 'set') ? new Set(v.val) : new Set();
392
+ if (v && v.type !== 'set') throw new Error('WRONGTYPE Operation against a key holding the wrong kind of value');
393
+ const set = v ? new Set(v.val) : new Set();
370
394
  let n = 0;
371
395
  for (const m of args.slice(1)) {
372
396
  if (cmd === 'SADD') { if (!set.has(m)) { set.add(m); n++; } }
@@ -495,8 +519,9 @@ class RedisServer {
495
519
  case 'DBSIZE': {
496
520
  let count = 0;
497
521
  const now = Date.now();
498
- for (const v of this.db.values()) {
499
- if (!v.ttl || v.ttl > now) count++;
522
+ for (const [k, v] of this.db) {
523
+ if (v.ttl && v.ttl <= now) { this.db.delete(k); continue; }
524
+ count++;
500
525
  }
501
526
  return count;
502
527
  }
package/lib/sql.js CHANGED
@@ -1,3 +1,6 @@
1
+ // N1-fix: createError 此前未导入,UPDATE 无主键表时抛 ReferenceError(而非预期的 SQL 错误)
2
+ const { createError } = require('./errors');
3
+
1
4
  const DANGEROUS_SQL = [
2
5
  { re: /^INTO$/, next: /^(OUTFILE|DUMPFILE)$/, name: 'INTO OUTFILE/DUMPFILE' },
3
6
  { re: /^LOAD$/, next: /^(FILE|DATA)$/, name: 'LOAD_FILE/LOAD DATA' },
@@ -616,7 +619,9 @@ class Parser {
616
619
  const nextTok = this.peek(1);
617
620
  if (nextTok && (nextTok.type === 'ident' || nextTok.type === 'keyword') &&
618
621
  ['TEXT', 'INT', 'INTEGER', 'VARCHAR', 'CHAR', 'DATE', 'DATETIME', 'TIMESTAMP', 'BOOLEAN', 'BOOL', 'FLOAT', 'DOUBLE', 'DECIMAL', 'BLOB', 'JSON'].includes(String(nextTok.value).toUpperCase())) {
619
- const col = this.parseTableName();
622
+ // P1-24: KEY/INDEX 是保留字,不能直接用 parseTableName(),改用 nameOfToken + next()
623
+ const col = nameOfToken(t);
624
+ this.next(); // 消费 KEY/INDEX token
620
625
  const def = this.parseColumnDef();
621
626
  schema[col] = def;
622
627
  if (def.primaryKey) hasPk = true;
@@ -983,6 +988,24 @@ class Parser {
983
988
  }
984
989
 
985
990
  parseValue() {
991
+ const atom = this.parseValueAtom();
992
+ return this.parseBinaryFromAtom(atom);
993
+ }
994
+
995
+ /** P2-3 helper: parse binary expression starting from an already-parsed left atom. */
996
+ parseBinaryFromAtom(left) {
997
+ const ops = ['+', '-', '*', '/', '%', '<<', '>>', '&', '|', '^'];
998
+ for (;;) {
999
+ const opTok = this.peek();
1000
+ if (opTok.type !== 'op' || !ops.includes(opTok.value)) break;
1001
+ this.next();
1002
+ const right = this.parseValueAtom();
1003
+ left = { type: 'binary', op: opTok.value, left, right };
1004
+ }
1005
+ return left;
1006
+ }
1007
+
1008
+ parseValueAtom() {
986
1009
  const t = this.next();
987
1010
  if (t.type === 'number' || t.type === 'string') return t.value;
988
1011
  // MySQL: ON DUPLICATE KEY UPDATE col = VALUES(col) —— 引用"本次本应插入的值"
@@ -1007,17 +1030,15 @@ class Parser {
1007
1030
  if (num.type !== 'number') throw new Error('Expected number after +');
1008
1031
  return num.value;
1009
1032
  }
1010
- // P1-19/P2-3: parseValue 不支持表达式 RHS,如 col = col + 1
1011
- // 支持简单的二元表达式:left op right
1012
1033
  if (t.type === 'ident') {
1013
- const left = this.parseColumnRef();
1014
- const opTok = this.peek();
1015
- if (opTok.type === 'op' && ['+', '-', '*', '/', '%', '<<', '>>', '&', '|', '^'].includes(opTok.value)) {
1034
+ // Handle dotted column refs (a.b) without double-consuming the token
1035
+ if (this.peek().type === 'op' && this.peek().value === '.') {
1016
1036
  this.next();
1017
- const right = this.parseValue();
1018
- return { type: 'binary', op: opTok.value, left, right };
1037
+ const col = this.next();
1038
+ if (!isUsableNameToken(col)) throw new Error(`Expected column name after '.', got '${col.value}'`);
1039
+ return nameOfToken(t) + '.' + nameOfToken(col);
1019
1040
  }
1020
- return left;
1041
+ return nameOfToken(t);
1021
1042
  }
1022
1043
  throw new Error(`Expected value, got '${t.value}'`);
1023
1044
  }
@@ -2355,9 +2376,11 @@ class Parser {
2355
2376
  return { type: 'value', value: t.value === '-' ? -num.value : num.value };
2356
2377
  }
2357
2378
  // 负号/正号后面是列名或表达式:解析为 unary 运算符
2358
- if (num.type === 'column' || num.type === 'value') {
2359
- const operand = num.type === 'column' ? { type: 'column', name: num.value } : { type: 'value', value: num.value };
2360
- return { type: 'negate', operand };
2379
+ if (num.type === 'ident') {
2380
+ return { type: 'negate', operand: { type: 'column', name: nameOfToken(num) }, positive: t.value === '+' };
2381
+ }
2382
+ if (num.type === 'string') {
2383
+ return { type: 'negate', operand: { type: 'value', value: num.value }, positive: t.value === '+' };
2361
2384
  }
2362
2385
  throw new Error('Expected number or column after sign');
2363
2386
  }
@@ -2825,7 +2848,9 @@ function resolveOperand(operand, row, ctx) {
2825
2848
  case 'negate': {
2826
2849
  const v = resolveOperand(operand.operand, row, ctx);
2827
2850
  if (v === null || v === undefined) return null;
2828
- return -Number(v);
2851
+ const n = Number(v);
2852
+ if (isNaN(n)) return null;
2853
+ return operand.positive ? n : -n;
2829
2854
  }
2830
2855
  // P1-19: ON DUPLICATE KEY UPDATE 表达式赋值,如 count = count + 1
2831
2856
  case 'binary': {
@@ -3990,8 +4015,14 @@ class SQLExecutor {
3990
4015
  break;
3991
4016
  }
3992
4017
  case 'dropPrimary': {
3993
- for (const def of Object.values(table._schema)) { if (def && typeof def === 'object') { def.primaryKey = false; } }
3994
- // P1-23: 清除主键缓存,否则后续插入仍会检查主键冲突
4018
+ for (const [colName, def] of Object.entries(table._schema)) {
4019
+ if (def && typeof def === 'object' && def.primaryKey) {
4020
+ def.primaryKey = false;
4021
+ // P1-23: 同时清除 unique 标记,否则后续 INSERT 仍会拒绝重复值
4022
+ def.unique = false;
4023
+ }
4024
+ }
4025
+ // 清除主键缓存
3995
4026
  table._primaryKey = null;
3996
4027
  table._pkIndex = null;
3997
4028
  break;
@@ -6409,24 +6440,24 @@ class SQLExecutor {
6409
6440
  const tableName = statement.from ? (statement.from.tables[0].table || null) : null;
6410
6441
  if (statement.columns.length === 1 && statement.columns[0].expr === '*') {
6411
6442
  const schemaKeys = schema ? Object.keys(schema) : [];
6412
- const pkCols = schemaKeys.filter(k => schema && schema[k] && schema[k].primaryKey);
6413
- const pk = pkCols.length > 0 ? pkCols[0] : (schemaKeys[0] || 'id');
6414
6443
  let cols;
6415
6444
  // 有 JOIN 时,SELECT * 应展开所有表的列(合并后的行包含所有前缀列)
6416
6445
  const hasJoins = statement.from && statement.from.joins && statement.from.joins.length > 0;
6417
6446
  if (schema && !hasJoins) {
6418
- cols = [pk, ...schemaKeys.filter(k => k !== pk)];
6447
+ // P1-17: SELECT * 按列声明顺序输出,不得把主键强制排到首位(MySQL 语义)
6448
+ cols = schemaKeys.filter(k => k !== '_softDelete');
6419
6449
  } else if (rowsAll && rowsAll.columns) {
6420
6450
  cols = rowsAll.columns;
6421
6451
  } else {
6422
- // JOIN 时:只使用带前缀的键,避免同一列重复
6452
+ // JOIN 时:只使用带前缀的键,避免同一列重复。
6453
+ // 不能按裸名去重——右表同名列(如 b.id)会被丢弃,导致右表列全部丢失。
6423
6454
  if (hasJoins) {
6424
6455
  const seen = new Set();
6425
6456
  cols = [];
6426
6457
  for (const r of rows) {
6427
6458
  for (const k of Object.keys(r)) {
6428
- const bare = k.includes('.') ? k.slice(k.indexOf('.') + 1) : k;
6429
- if (!seen.has(bare)) { seen.add(bare); cols.push(k); }
6459
+ if (!k.includes('.')) continue; // 只取带前缀的键
6460
+ if (!seen.has(k)) { seen.add(k); cols.push(k); }
6430
6461
  }
6431
6462
  }
6432
6463
  } else {
@@ -6453,15 +6484,56 @@ class SQLExecutor {
6453
6484
  return { ok: true, type: 'select', table: tableName, columns: cols, rows: rows.map(r => cols.map(c => (r[c] !== undefined ? r[c] : r[String(c).slice(String(c).indexOf('.') + 1)]))), raw: rows };
6454
6485
  }
6455
6486
 
6487
+ // P1-18: SELECT *, col —— 星号必须展开为全部列,列名与值对齐。
6488
+ // 此前把 '*' 当成单列输出嵌套数组,导致每行值错位重复/输出 null。
6489
+ if (statement.columns.some(c => c.expr === '*')) {
6490
+ const hasJoins2 = statement.from && statement.from.joins && statement.from.joins.length > 0;
6491
+ let starKeys = [];
6492
+ if (schema && !hasJoins2) {
6493
+ starKeys = Object.keys(schema).filter(k => k !== '_softDelete');
6494
+ } else if (hasJoins2) {
6495
+ // JOIN 时:保留所有带前缀的键,不按裸名去重(否则右表同名列丢失)
6496
+ const seenK = new Set();
6497
+ for (const r of rows) {
6498
+ for (const k of Object.keys(r)) {
6499
+ if (!k.includes('.')) continue;
6500
+ if (!seenK.has(k)) { seenK.add(k); starKeys.push(k); }
6501
+ }
6502
+ }
6503
+ } else {
6504
+ const seenK = new Set();
6505
+ for (const r of rows) {
6506
+ for (const k of Object.keys(r)) {
6507
+ const bare = k.includes('.') ? k.slice(k.indexOf('.') + 1) : k;
6508
+ if (!seenK.has(bare)) { seenK.add(bare); starKeys.push(k); }
6509
+ }
6510
+ }
6511
+ }
6512
+ const cols = [];
6513
+ for (const c of statement.columns) {
6514
+ if (c.expr === '*') { for (const k of starKeys) cols.push(k.includes('.') ? k.slice(k.indexOf('.') + 1) : k); }
6515
+ else cols.push(scalarColumnName(c));
6516
+ }
6517
+ const mapped = await this._projectRows(statement, rows, (r) => {
6518
+ const out = [];
6519
+ for (const c of statement.columns) {
6520
+ if (c.expr === '*') {
6521
+ for (const k of starKeys) out.push(r[k] !== undefined ? r[k] : r[String(k).slice(String(k).indexOf('.') + 1)]);
6522
+ } else if (c.over || (c.scalar && c.scalar.over)) out.push(windowColumnValue(c, r));
6523
+ else if (c.scalar) out.push(projectScalar(resolveOperand(c.scalar, r, this.ctx)));
6524
+ else if (c.literal !== undefined) out.push(c.literal);
6525
+ else if (c.caseExpr) out.push(projectScalar(evaluateCaseVal(c.caseExpr, r)));
6526
+ else out.push(projectScalar(resolveOperand({ type: 'column', name: c.expr }, r, this.ctx)));
6527
+ }
6528
+ return out;
6529
+ });
6530
+ return { ok: true, type: 'select', table: tableName, columns: cols, rows: mapped, raw: rows };
6531
+ }
6532
+
6456
6533
  const cols = statement.columns.map(c => scalarColumnName(c));
6457
6534
  const mapped = await this._projectRows(statement, rows, (r) => statement.columns.map(c => {
6458
6535
  if (c.over || (c.scalar && c.scalar.over)) return windowColumnValue(c, r);
6459
6536
  if (c.scalar) return projectScalar(resolveOperand(c.scalar, r, this.ctx));
6460
- // P1-18: SELECT *, col 星号列输出 null——应展开为所有列
6461
- if (c.expr === '*') {
6462
- // 展开星号:返回行的所有值
6463
- return Object.keys(r).map(k => r[k]);
6464
- }
6465
6537
  if (c.literal !== undefined) return c.literal;
6466
6538
  if (c.caseExpr) return projectScalar(evaluateCaseVal(c.caseExpr, r));
6467
6539
  return projectScalar(resolveOperand({ type: 'column', name: c.expr }, r, this.ctx));
@@ -6604,7 +6676,10 @@ function escapeValue(value) {
6604
6676
  }
6605
6677
  return value.map(escapeValue).join(', ');
6606
6678
  }
6607
- if (typeof value === 'object') return "'" + JSON.stringify(value).replace(/'/g, "\\'") + "'";
6679
+ // P1-25: 对象参数先 JSON 序列化,再走与字符串完全相同的转义通道。
6680
+ // 此前只对单引号做替换,反斜杠原样拼进 SQL 文本,解析时丢失/翻倍,
6681
+ // 导致 C:\temp\x 这类值存成两个反斜杠或直接 JSON 解析失败。
6682
+ if (typeof value === 'object') return escapeValue(JSON.stringify(value));
6608
6683
  const str = String(value)
6609
6684
  .replace(/\\/g, '\\\\')
6610
6685
  .replace(/\0/g, '\\0')
package/lib/table.js CHANGED
@@ -175,6 +175,18 @@ class Table {
175
175
  this._autoCreateBTrees();
176
176
  }
177
177
 
178
+ // P1-2: DDL 后统一刷新 schema 缓存。
179
+ // dropColumn/renameColumn 过去只改 _cachedSchemaFields,unique/required/numeric/
180
+ // date/computed 缓存残留旧列 → insertMany 等路径拿到陈旧 schema 直接崩溃或漏校验。
181
+ _refreshSchemaCache() {
182
+ this._cachedSchemaFields = Object.keys(this._schema).filter(f => f !== '_softDelete');
183
+ this._cachedDateFields = Object.keys(this._dateFields);
184
+ this._cachedUniqueFields = this._cachedSchemaFields.filter(f => this._schema[f] && this._schema[f].unique);
185
+ this._cachedRequiredFields = this._cachedSchemaFields.filter(f => this._schema[f] && this._schema[f].required);
186
+ this._cachedNumericFields = this._cachedSchemaFields.filter(f => ['integer', 'int', 'float', 'number', 'decimal'].includes(this._schema[f] && this._schema[f].type));
187
+ this._cachedComputedFields = this._cachedSchemaFields.filter(f => this._computedFields[f]);
188
+ }
189
+
178
190
  _autoCreateBTrees() {
179
191
  for (const [field, def] of Object.entries(this._schema)) {
180
192
  if (field === '_softDelete') continue;
@@ -463,6 +475,10 @@ class Table {
463
475
  if (rowIndex === undefined) return 0;
464
476
  const row = this._rows[rowIndex];
465
477
  if (row && row._deleted) return 0;
478
+ // P1-3:主键被更新时传播到外键引用(CASCADE / SET NULL / RESTRICT)
479
+ if (updates[this._primaryKey] !== undefined && updates[this._primaryKey] !== id) {
480
+ this._propagatePKUpdate(id, updates[this._primaryKey]);
481
+ }
466
482
  const oldData = { ...row };
467
483
  const merged = { ...row, ...updates };
468
484
  this._validateConstraints(merged, rowIndex);
@@ -708,6 +724,7 @@ class Table {
708
724
  if (val !== undefined && val !== null) this._pkIndex.set(val, i);
709
725
  }
710
726
  }
727
+ this._refreshSchemaCache();
711
728
  this._dirty = true;
712
729
  this._db._markDirty(this._name);
713
730
  return this;
@@ -732,6 +749,7 @@ class Table {
732
749
  if (this._pkIndex) this._pkIndex.clear();
733
750
  }
734
751
  if (this._autoIncrementField === field) this._autoIncrementField = null;
752
+ this._refreshSchemaCache();
735
753
  this._dirty = true;
736
754
  this._db._markDirty(this._name);
737
755
  return this;
@@ -759,8 +777,27 @@ class Table {
759
777
  row[newName] = row[oldName];
760
778
  delete row[oldName];
761
779
  }
780
+ if (this._indexes[oldName]) {
781
+ this._indexes[newName] = this._indexes[oldName];
782
+ delete this._indexes[oldName];
783
+ }
784
+ this._foreignKeys = this._foreignKeys.filter(fk => true);
785
+ for (const fk of this._foreignKeys) {
786
+ if (fk.field === oldName) fk.field = newName;
787
+ }
788
+ this._checkConstraints = this._checkConstraints.map(c =>
789
+ c.field === oldName ? { ...c, field: newName } : c);
790
+ if (this._computedFields[oldName]) {
791
+ this._computedFields[newName] = this._computedFields[oldName];
792
+ delete this._computedFields[oldName];
793
+ }
794
+ if (this._dateFields[oldName]) {
795
+ this._dateFields[newName] = this._dateFields[oldName];
796
+ delete this._dateFields[oldName];
797
+ }
762
798
  if (this._primaryKey === oldName) this._primaryKey = newName;
763
799
  if (this._autoIncrementField === oldName) this._autoIncrementField = newName;
800
+ this._refreshSchemaCache();
764
801
  this._dirty = true;
765
802
  this._db._markDirty(this._name);
766
803
  return this;
@@ -780,6 +817,12 @@ class Table {
780
817
  delete this._btrees[field];
781
818
  }
782
819
  }
820
+ if (definition.type && ['date', 'datetime', 'timestamp', 'time'].includes(definition.type)) {
821
+ this._dateFields[field] = definition.type;
822
+ } else if (definition.type) {
823
+ delete this._dateFields[field];
824
+ }
825
+ this._refreshSchemaCache();
783
826
  this._dirty = true;
784
827
  this._db._markDirty(this._name);
785
828
  return this;
@@ -1212,20 +1255,55 @@ class Table {
1212
1255
 
1213
1256
  /**
1214
1257
  * 级联外键操作:当删除当前表的行时,查找所有引用此表的子表
1215
- * 并执行 CASCADE 或 SET NULL
1258
+ * 并执行 CASCADE / SET NULL / RESTRICT(P1-3:RESTRICT 之前静默放行)
1216
1259
  */
1217
- _cascadeForeignKeys(row) {
1260
+ _cascadeForeignKeys(row, opts = {}) {
1218
1261
  for (const [, table] of Object.entries(this._db._tables)) {
1219
1262
  if (table._name === this._name) continue;
1220
1263
  for (const fk of table._foreignKeys) {
1221
1264
  const refTable = table._resolveTable(fk.table);
1222
1265
  if (!refTable || refTable._name !== this._name) continue;
1223
1266
 
1224
- // 检查子表行是否引用当前行
1267
+ // 被引用列值为 NULL 时不构成引用(SQL 语义),不参与约束
1268
+ const refVal = row[fk.refField];
1269
+ if (refVal === undefined || refVal === null) continue;
1270
+
1271
+ // 检查子表行是否引用当前行(软删除模式下行已不可见则不阻断)
1272
+ const refs = table._applyFilterOptimized(table._getVisibleRows(), { [fk.field]: refVal });
1273
+ if (refs.length === 0) continue;
1274
+
1225
1275
  if (fk.onDelete === 'cascade') {
1226
1276
  table.remove({ [fk.field]: row[fk.refField] });
1227
1277
  } else if (fk.onDelete === 'set null') {
1228
1278
  table.update({ [fk.field]: row[fk.refField] }, { [fk.field]: null });
1279
+ } else {
1280
+ // RESTRICT / NO ACTION / 未指定:默认拒绝删除(ER_ROW_IS_REFERENCED)
1281
+ if (opts.skipRestrict) continue;
1282
+ throw createError('ER_ROW_IS_REFERENCED');
1283
+ }
1284
+ }
1285
+ }
1286
+ }
1287
+
1288
+ /**
1289
+ * P1-3:父表主键被更新时,传播到引用它的子表。
1290
+ * CASCADE → 同步新值;SET NULL → 置空;RESTRICT → 拒绝更新。
1291
+ */
1292
+ _propagatePKUpdate(oldVal, newVal) {
1293
+ for (const [, table] of Object.entries(this._db._tables)) {
1294
+ if (table._name === this._name) continue;
1295
+ for (const fk of table._foreignKeys) {
1296
+ const refTable = table._resolveTable(fk.table);
1297
+ if (!refTable || refTable._name !== this._name) continue;
1298
+ const refs = table._applyFilterOptimized(table._getVisibleRows(), { [fk.field]: oldVal });
1299
+ if (refs.length === 0) continue;
1300
+ const action = (fk.onUpdate || 'restrict').toLowerCase();
1301
+ if (action === 'cascade') {
1302
+ table.update({ [fk.field]: oldVal }, { [fk.field]: newVal });
1303
+ } else if (action === 'set null') {
1304
+ table.update({ [fk.field]: oldVal }, { [fk.field]: null });
1305
+ } else {
1306
+ throw createError('ER_ROW_IS_REFERENCED');
1229
1307
  }
1230
1308
  }
1231
1309
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jsql-neo",
3
- "version": "6.3.10",
3
+ "version": "6.4.0-beta.1",
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",
@@ -88,7 +88,7 @@
88
88
  "node": ">=14.0.0"
89
89
  },
90
90
  "dependencies": {
91
- "@vexify-org/yaggs": "^8.1.0"
91
+ "@vexify-org/yaggs": "^8.1.1"
92
92
  },
93
93
  "devDependencies": {
94
94
  "c8": "^10.1.2"
@@ -533,22 +533,23 @@ async function main() {
533
533
  const pkg = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'package.json'), 'utf8'));
534
534
  const raw = fs.readFileSync(path.join(__dirname, '..', 'README.md'), 'utf8');
535
535
  const v = pkg.version;
536
- ok('package.json 版本号格式合法', /^\d+\.\d+\.\d+$/.test(v), v);
536
+ // 支持 prerelease 版本如 6.4.0-beta.1
537
+ ok('package.json 版本号格式合法', /^\d+\.\d+\.\d+(-[a-zA-Z0-9.]+)?$/.test(v), v);
537
538
 
538
- // README 头部横幅必须与 package.json 一致
539
- const banner = raw.match(/^> \*\*v(\d+\.\d+\.\d+)\*\*/m);
539
+ // README 头部横幅必须与 package.json 一致(支持 prerelease)
540
+ const banner = raw.match(/^> \*\*v(\d+\.\d+\.\d+(?:-[a-zA-Z0-9.]+)?)\*\*/m);
540
541
  ok('README 顶部版本横幅与 package.json 一致', banner && banner[1] === v,
541
542
  banner ? `README=${banner[1]} package=${v}` : '未找到顶部横幅');
542
543
 
543
- // 所有 "jsql-neo vX.Y.Z" / "文档版本:vX.Y.Z" 形式的版本声明都必须等于当前版本
544
- const claims = [...raw.matchAll(/(?:jsql-neo v|文档版本:v|ver=)(\d+\.\d+\.\d+)/g)]
544
+ // 所有 "jsql-neo vX.Y.Z" / "文档版本:vX.Y.Z" 形式的版本声明都必须等于当前版本(支持 prerelease)
545
+ const claims = [...raw.matchAll(/(?:jsql-neo v|文档版本:v|ver=)(\d+\.\d+\.\d+(?:-[a-zA-Z0-9.]+)?)/g)]
545
546
  .map(m => m[1]);
546
547
  const stale = [...new Set(claims.filter(c => c !== v))];
547
548
  ok('README 无陈旧版本号声明', stale.length === 0, stale);
548
549
 
549
- // index.js 头部注释也必须同步
550
+ // index.js 头部注释也必须同步(支持 prerelease)
550
551
  const idx = fs.readFileSync(path.join(__dirname, '..', 'index.js'), 'utf8');
551
- const hdr = idx.match(/JSQL-NEO v(\d+\.\d+\.\d+)/);
552
+ const hdr = idx.match(/JSQL-NEO v(\d+\.\d+\.\d+(?:-[a-zA-Z0-9.]+)?)/);
552
553
  ok('index.js 头部版本注释与 package.json 一致', hdr && hdr[1] === v,
553
554
  hdr ? `index.js=${hdr[1]} package=${v}` : '未找到版本注释');
554
555
  }