jsql-neo 4.4.3 → 4.5.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/index.js CHANGED
@@ -23,6 +23,39 @@ const migrate = require('./lib/migrate');
23
23
  const { WebUI } = require('./lib/web_ui');
24
24
  const { RedisServer, createRedisServer } = require('./lib/redis_server');
25
25
 
26
+ /**
27
+ * 全局注入:把项目内 `require('mysql2')` 全部替换为 jsql-neo 内存引擎兼容层。
28
+ * 调用一次后,TypeORM / Drizzle / MikroORM / Kysely 等直接依赖 mysql2 的库
29
+ * 无需改代码即可享受本地内存速度。
30
+ */
31
+ function enableMySQLCompat() {
32
+ const fs = require('fs');
33
+ const path = require('path');
34
+ const seen = new Set();
35
+ const bases = new Set();
36
+ if (require.main && Array.isArray(require.main.paths)) {
37
+ for (const p of require.main.paths) bases.add(p);
38
+ }
39
+ if (process.env.NODE_PATH) {
40
+ for (const p of process.env.NODE_PATH.split(path.delimiter)) if (p) bases.add(p);
41
+ }
42
+ bases.add(path.join(process.cwd(), 'node_modules'));
43
+ const inject = (p, mod) => {
44
+ try {
45
+ const resolved = p;
46
+ if (seen.has(resolved)) return;
47
+ if (!fs.existsSync(resolved)) return;
48
+ seen.add(resolved);
49
+ require.cache[resolved] = { exports: mod, id: resolved, filename: resolved, loaded: true, children: [] };
50
+ } catch (e) { /* ignore */ }
51
+ };
52
+ for (const base of bases) {
53
+ inject(path.join(base, 'mysql2', 'index.js'), mysqlCompat);
54
+ inject(path.join(base, 'mysql2', 'promise.js'), mysqlCompat);
55
+ }
56
+ return mysqlCompat;
57
+ }
58
+
26
59
  module.exports = {
27
60
  JSQL: WasmClient.JSQL,
28
61
  NativeJSQL: NativeClient.JSQL,
@@ -45,6 +78,8 @@ module.exports = {
45
78
  createConnection: mysqlCompat.createConnection,
46
79
  createPool: mysqlCompat.createPool,
47
80
  mysql: mysqlCompat,
81
+ mysql2: mysqlCompat,
82
+ enableMySQLCompat,
48
83
  createMysqlServer,
49
84
  MysqlServer,
50
85
  // 迁移工具: mysqldump 导入 / JSON / CSV
package/lib/database.js CHANGED
@@ -1142,9 +1142,46 @@ class Database {
1142
1142
  async start() {
1143
1143
  this._runHooks('onStart', []);
1144
1144
  this._emit('start', {});
1145
+ if (this._dirMode && !this._sigInstalled) {
1146
+ this._sigInstalled = true;
1147
+ Database._installSignalHandler(this);
1148
+ }
1145
1149
  return this;
1146
1150
  }
1147
1151
 
1152
+ /**
1153
+ * 进程退出时兜底刷盘:拦截 SIGINT/SIGTERM,同步落盘脏表后再退出。
1154
+ * 使用静态共享 handler,避免多个数据库实例重复注册/互相覆盖。
1155
+ * @private
1156
+ */
1157
+ static _installSignalHandler(db) {
1158
+ if (!Database._signalHandlerInstalled) {
1159
+ Database._signalHandlerInstalled = true;
1160
+ Database._signalDBs = new Set();
1161
+ const handler = (sig) => {
1162
+ for (const d of Database._signalDBs) {
1163
+ try {
1164
+ if (d._dirMode) {
1165
+ if (d._flushTimer) { clearTimeout(d._flushTimer); d._flushTimer = null; }
1166
+ d._flushDirty();
1167
+ d._saveMeta();
1168
+ }
1169
+ } catch (e) { /* 兜底失败不阻塞退出 */ }
1170
+ }
1171
+ process.exit(128 + (sig === 'SIGINT' ? 2 : 15));
1172
+ };
1173
+ process.on('SIGINT', handler);
1174
+ process.on('SIGTERM', handler);
1175
+ Database._signalHandler = handler;
1176
+ }
1177
+ Database._signalDBs.add(db);
1178
+ }
1179
+
1180
+ /** 从进程退出兜底集合移除(stop 时调用) */
1181
+ static _uninstallSignalHandler(db) {
1182
+ if (Database._signalDBs) Database._signalDBs.delete(db);
1183
+ }
1184
+
1148
1185
  insert(tableName, data) {
1149
1186
  const table = this._ensureTable(tableName);
1150
1187
  if (!table) throw createError('ER_NO_SUCH_TABLE', tableName);
@@ -1333,11 +1370,16 @@ class Database {
1333
1370
  this._runHooks('onStop', []);
1334
1371
  this._emit('stop', {});
1335
1372
  if (this._dirMode) {
1373
+ if (this._flushTimer) { clearTimeout(this._flushTimer); this._flushTimer = null; }
1336
1374
  try {
1337
1375
  this._flushDirty();
1338
1376
  this._saveMeta();
1339
1377
  } catch (e) {}
1340
1378
  }
1379
+ if (this._sigInstalled) {
1380
+ this._sigInstalled = false;
1381
+ Database._uninstallSignalHandler(this);
1382
+ }
1341
1383
  return this;
1342
1384
  }
1343
1385
 
@@ -6,19 +6,16 @@
6
6
 
7
7
  const { executeSQL, applyParams, escapeValue, escapeId } = require('./sql');
8
8
  const Database = require('./database');
9
+ const EventEmitter = require('events');
9
10
 
10
11
  function isQueryResult(r) {
11
12
  return !!r && typeof r === 'object' && Array.isArray(r.rows) && Array.isArray(r.columns);
12
13
  }
13
14
 
14
- function toResultPacket(r, table) {
15
+ function toResultPacket(r, table, rowsAsArray) {
15
16
  if (isQueryResult(r)) {
16
17
  const columns = r.columns || [];
17
- const rows = (r.rows || []).map(vals => {
18
- const obj = {};
19
- columns.forEach((c, j) => { obj[c] = vals[j]; });
20
- return obj;
21
- });
18
+ const rawRows = r.rows || [];
22
19
  const fields = columns.map(name => ({
23
20
  name,
24
21
  table: table || '',
@@ -27,6 +24,13 @@ function toResultPacket(r, table) {
27
24
  flags: 0,
28
25
  charsetNr: 45,
29
26
  }));
27
+ const rows = rowsAsArray
28
+ ? rawRows
29
+ : rawRows.map(vals => {
30
+ const obj = {};
31
+ columns.forEach((c, j) => { obj[c] = vals[j]; });
32
+ return obj;
33
+ });
30
34
  return { rows, fields };
31
35
  }
32
36
  return {
@@ -41,8 +45,9 @@ function toResultPacket(r, table) {
41
45
  };
42
46
  }
43
47
 
44
- class Connection {
48
+ class Connection extends EventEmitter {
45
49
  constructor(options = {}) {
50
+ super();
46
51
  this.config = {
47
52
  host: options.host || 'localhost',
48
53
  port: options.port || 3306,
@@ -56,20 +61,38 @@ class Connection {
56
61
  this.database = options.database && typeof options.database === 'object' && !options.filename && typeof options.database.start === 'function'
57
62
  ? options.database
58
63
  : (options.engine || null);
59
- this.filename = options.filename || (typeof options.database === 'string' ? options.database : null);
64
+ this.filename = options.filename || null;
60
65
  this.engine = null;
61
66
  this._ownEngine = false;
62
67
  this.state = 'disconnected';
68
+ this.stream = {
69
+ destroyed: false,
70
+ writable: true,
71
+ destroy: () => { this.stream.destroyed = true; },
72
+ };
73
+ if (options.host !== undefined || options.database || options.engine || options.filename) {
74
+ queueMicrotask(() => {
75
+ this._getEngine().then(() => this.emit('connect'), err => {
76
+ if (this.listenerCount('error') > 0) this.emit('error', err);
77
+ });
78
+ });
79
+ }
63
80
  }
64
81
 
65
82
  async _getEngine() {
66
83
  if (this.engine) return this.engine;
67
- if (this.database) {
84
+ if (this.database && typeof this.database.start === 'function') {
68
85
  this.engine = this.database;
69
- if (typeof this.engine.start === 'function') await this.engine.start();
86
+ await this.engine.start();
87
+ } else if (this._pool) {
88
+ this.engine = await this._pool._sharedEngineFor(this.config.database || 'default');
89
+ } else if (this.filename) {
90
+ this.engine = new Database(this.filename);
91
+ await this.engine.start();
92
+ this._ownEngine = true;
70
93
  } else {
71
- this.engine = new Database(this.filename || ':memory:');
72
- if (typeof this.engine.start === 'function') await this.engine.start();
94
+ this.engine = new Database(':memory:');
95
+ await this.engine.start();
73
96
  this._ownEngine = true;
74
97
  }
75
98
  this.state = 'connected';
@@ -77,17 +100,21 @@ class Connection {
77
100
  }
78
101
 
79
102
  connect(cb) {
80
- const p = this._getEngine().then(() => this);
103
+ const p = this._getEngine().then(c => {
104
+ this.emit('connect');
105
+ return c;
106
+ });
81
107
  if (cb) p.then(c => cb(null, c), err => cb(err));
82
108
  return p;
83
109
  }
84
110
 
85
- async query(...args) {
86
- let sql, values, cb;
111
+ query(...args) {
112
+ let sql, values, cb, rowsAsArray;
87
113
  if (typeof args[0] === 'object' && args[0] !== null && typeof args[0].sql === 'string') {
88
114
  sql = args[0].sql;
89
- values = args[0].values;
90
- cb = typeof args[1] === 'function' ? args[1] : null;
115
+ rowsAsArray = args[0].rowsAsArray === true;
116
+ values = args[0].values !== undefined ? args[0].values : (Array.isArray(args[1]) ? args[1] : null);
117
+ cb = typeof args[1] === 'function' ? args[1] : (typeof args[2] === 'function' ? args[2] : null);
91
118
  } else {
92
119
  sql = args[0];
93
120
  values = Array.isArray(args[1]) ? args[1] : null;
@@ -103,7 +130,7 @@ class Connection {
103
130
  });
104
131
  const results = Array.isArray(r) ? r : [r];
105
132
  const last = results[results.length - 1];
106
- const packet = toResultPacket(last, last ? last.table : null);
133
+ const packet = toResultPacket(last, last ? last.table : null, rowsAsArray);
107
134
  if (isQueryResult(last)) {
108
135
  const arr = [packet.rows, packet.fields];
109
136
  arr.rows = packet.rows;
@@ -115,17 +142,34 @@ class Connection {
115
142
  arr.fields = null;
116
143
  return arr;
117
144
  })();
145
+ const q = new EventEmitter();
146
+ q.setMaxListeners = EventEmitter.prototype.setMaxListeners;
147
+ p.then(res => {
148
+ q.emit('result', res.rows || res[0]);
149
+ q.emit('fields', res.fields || []);
150
+ }, err => {
151
+ if (q.listenerCount('error') > 0) q.emit('error', err);
152
+ });
118
153
  if (cb) {
119
154
  p.then(res => {
120
155
  if (res && res.rows && res.fields) cb(null, res.rows, res.fields);
121
156
  else if (res && res[0] && res[0].insertId !== undefined) cb(null, res[0]);
122
157
  else cb(null, res[0]);
123
158
  }, err => cb(err));
124
- return undefined;
159
+ return q;
125
160
  }
161
+ p.setMaxListeners = q.setMaxListeners;
126
162
  return p;
127
163
  }
128
164
 
165
+ execute(sql, values, cb) {
166
+ if (typeof values === 'function') {
167
+ cb = values;
168
+ values = undefined;
169
+ }
170
+ return this.query(sql, values, cb);
171
+ }
172
+
129
173
  beginTransaction(cb) {
130
174
  const p = this.query('BEGIN');
131
175
  if (cb) p.then(() => cb(null), err => cb(err));
@@ -156,6 +200,8 @@ class Connection {
156
200
 
157
201
  destroy() {
158
202
  this.state = 'destroyed';
203
+ this.stream.destroy();
204
+ this.emit('close');
159
205
  }
160
206
 
161
207
  end(cb) {
@@ -164,6 +210,8 @@ class Connection {
164
210
  await this.engine.stop();
165
211
  }
166
212
  this.state = 'closed';
213
+ this.stream.destroy();
214
+ this.emit('close');
167
215
  return undefined;
168
216
  })();
169
217
  if (cb) p.then(() => cb(null), err => cb(err));
@@ -184,17 +232,25 @@ class Pool {
184
232
  this._connections = [];
185
233
  this._waiters = [];
186
234
  this._closed = false;
187
- this._sharedEngine = null;
235
+ this._sharedEngines = new Map();
188
236
  this._reaper = setInterval(() => this._reapIdle(), Math.max(1000, Math.floor(this.idleTimeout / 10) || 10000));
189
237
  this._reaper.unref();
190
238
  }
191
239
 
240
+ async _sharedEngineFor(database) {
241
+ const key = database || 'default';
242
+ let engine = this._sharedEngines.get(key);
243
+ if (!engine) {
244
+ engine = new Database(':memory:');
245
+ if (typeof engine.start === 'function') await engine.start();
246
+ this._sharedEngines.set(key, engine);
247
+ }
248
+ return engine;
249
+ }
250
+
192
251
  _createConnection() {
193
252
  const conn = new Connection(this.config);
194
253
  conn._pool = this;
195
- if (!conn.database && !conn.engine && !conn.filename) {
196
- conn.database = this._sharedEngine;
197
- }
198
254
  const origDestroy = conn.destroy.bind(conn);
199
255
  conn.destroy = () => {
200
256
  this._remove(conn);
@@ -203,14 +259,6 @@ class Pool {
203
259
  return conn;
204
260
  }
205
261
 
206
- async _ensureSharedEngine() {
207
- if (!this._sharedEngine) {
208
- this._sharedEngine = new Database(':memory:');
209
- if (typeof this._sharedEngine.start === 'function') await this._sharedEngine.start();
210
- }
211
- return this._sharedEngine;
212
- }
213
-
214
262
  _remove(conn) {
215
263
  const idx = this._connections.indexOf(conn);
216
264
  if (idx !== -1) this._connections.splice(idx, 1);
@@ -239,7 +287,7 @@ class Pool {
239
287
  return Promise.resolve(free);
240
288
  }
241
289
  if (this._connections.length < this.connectionLimit) {
242
- return this._ensureSharedEngine().then(() => {
290
+ return Promise.resolve().then(() => {
243
291
  if (this._connections.length >= this.connectionLimit) {
244
292
  return this._waitForConnection();
245
293
  }
@@ -321,6 +369,9 @@ class Pool {
321
369
  for (const w of this._waiters.splice(0)) w.reject(err);
322
370
  const conns = this._connections.splice(0);
323
371
  await Promise.allSettled(conns.map(c => c.end()));
372
+ const engines = Array.from(this._sharedEngines.values());
373
+ this._sharedEngines.clear();
374
+ await Promise.allSettled(engines.map(e => (typeof e.stop === 'function' ? e.stop() : null)));
324
375
  return undefined;
325
376
  })();
326
377
  if (cb) p.then(() => cb(null), err => cb(err));
@@ -340,9 +391,21 @@ function createPool(options) {
340
391
  return new Pool(options || {});
341
392
  }
342
393
 
394
+ class RowDataPacket {}
395
+ class OkPacket {}
396
+ class ResultSetHeader {}
397
+ class FieldPacket {}
398
+
343
399
  module.exports = {
344
400
  createConnection,
345
401
  createPool,
402
+ createConnectionPromise: () => Promise.resolve(createConnection()),
403
+ Connection,
404
+ Pool,
405
+ RowDataPacket,
406
+ OkPacket,
407
+ ResultSetHeader,
408
+ FieldPacket,
346
409
  escape: escapeValue,
347
410
  escapeId,
348
411
  format: applyParams,
@@ -104,8 +104,19 @@ function handshakePacket(connectionId, seed) {
104
104
  return b.build();
105
105
  }
106
106
 
107
- function okPacket(affectedRows = 0, insertId = 0, status = SERVER_STATUS_AUTOCOMMIT) {
108
- const b = new PacketBuilder();
107
+ /**
108
+ * 生成握手 seed:使用可打印 ASCII(33-126),避开 0x00。
109
+ * 老客户端按 C 字符串读取 seed,若含 \0 会提前截断导致握手失败/断连。
110
+ */
111
+ function genSeed(len = 20) {
112
+ const out = Buffer.alloc(len);
113
+ for (let i = 0; i < len; i++) {
114
+ out[i] = 33 + Math.floor(Math.random() * 94);
115
+ }
116
+ return out;
117
+ }
118
+
119
+ function okPacket(affectedRows = 0, insertId = 0, status = SERVER_STATUS_AUTOCOMMIT) { const b = new PacketBuilder();
109
120
  b.byte(0x00);
110
121
  b.raw(encodeLenenc(affectedRows));
111
122
  b.raw(encodeLenenc(insertId));
@@ -364,7 +375,7 @@ class MysqlConnection {
364
375
  this.socket = socket;
365
376
  this.server = server;
366
377
  this.connectionId = ++server._connectionCounter;
367
- this.seed = crypto.randomBytes(20);
378
+ this.seed = genSeed(20);
368
379
  this.buffer = Buffer.alloc(0);
369
380
  this.sequence = 0;
370
381
  this.authenticated = false;
@@ -257,14 +257,23 @@ class JSQL {
257
257
  _runHooks(hookName, args) {
258
258
  const hooks = this._hooks[hookName];
259
259
  if (!hooks || hooks.length === 0) return true;
260
- for (const fn of hooks) {
261
- const r = fn(...args);
262
- if (r === false) return false;
263
- if (r !== undefined && args.length > 0) {
264
- args[0] = r;
260
+ if (this._inHook) {
261
+ // 防止插件 hook 内重入 native 调用导致 N-API 崩溃(段错误)
262
+ throw new Error('ER_PLUGIN_REENTRY: plugin hook "' + hookName + '" re-entered native call; plugin must not call engine methods inside its own hook');
263
+ }
264
+ this._inHook = true;
265
+ try {
266
+ for (const fn of hooks) {
267
+ const r = fn(...args);
268
+ if (r === false) return false;
269
+ if (r !== undefined && args.length > 0) {
270
+ args[0] = r;
271
+ }
265
272
  }
273
+ return true;
274
+ } finally {
275
+ this._inHook = false;
266
276
  }
267
- return true;
268
277
  }
269
278
 
270
279
  async start() {
package/lib/sql.js CHANGED
@@ -53,7 +53,9 @@ const KEYWORDS = new Set([
53
53
  'DATABASES', 'DATABASE', 'DESCRIBE', 'DESC', 'ON', 'DUPLICATE',
54
54
  'JOIN', 'LEFT', 'RIGHT', 'INNER', 'OUTER', 'CROSS',
55
55
  'GROUP', 'HAVING', 'UNION', 'ALL', 'CASE', 'WHEN', 'THEN', 'ELSE', 'END',
56
- 'BETWEEN', 'USING', 'FULL', 'UNSIGNED', 'ZEROFILL', 'TRUNCATE', 'COLLATE', 'CHARACTER'
56
+ 'BETWEEN', 'USING', 'FULL', 'UNSIGNED', 'ZEROFILL', 'TRUNCATE', 'COLLATE', 'CHARACTER',
57
+ 'ALTER', 'ADD', 'COLUMN', 'MODIFY', 'CHANGE', 'INDEX', 'FOREIGN', 'REFERENCES',
58
+ 'CONSTRAINT', 'RENAME', 'TO', 'AFTER', 'FIRST', 'ENGINE', 'AUTO_INCREMENT', 'SPATIAL'
57
59
  ]);
58
60
 
59
61
  function tokenize(sql) {
@@ -229,6 +231,7 @@ class Parser {
229
231
  if (this.isKeyword('DATABASE', 1)) return this.parseDropDatabase();
230
232
  throw new Error('Unsupported DROP statement');
231
233
  case 'INSERT': return this.parseInsert();
234
+ case 'ALTER': return this.parseAlter();
232
235
  case 'TRUNCATE': this.expectKeyword('TRUNCATE'); if (this.isKeyword('TABLE')) this.next(); return { type: 'truncate', name: this.parseTableName() };
233
236
  case 'SELECT': return this.parseSelect();
234
237
  case 'UPDATE': return this.parseUpdate();
@@ -281,16 +284,28 @@ class Parser {
281
284
  if (t.value === 'PRIMARY') {
282
285
  this.expectKeyword('PRIMARY'); this.expectKeyword('KEY');
283
286
  this.expect('op', '(');
284
- const pkCol = this.parseTableName();
287
+ const pkCols = [this.parseTableName()];
288
+ while (this.peek().type === 'op' && this.peek().value === ',') {
289
+ this.next();
290
+ pkCols.push(this.parseTableName());
291
+ }
285
292
  this.expect('op', ')');
286
- if (schema[pkCol]) schema[pkCol].primaryKey = true;
293
+ for (const pkCol of pkCols) {
294
+ if (schema[pkCol]) schema[pkCol].primaryKey = true;
295
+ }
287
296
  hasPk = true;
288
297
  } else {
289
298
  this.expectKeyword('UNIQUE');
290
299
  this.expect('op', '(');
291
- const uCol = this.parseTableName();
300
+ const uCols = [this.parseTableName()];
301
+ while (this.peek().type === 'op' && this.peek().value === ',') {
302
+ this.next();
303
+ uCols.push(this.parseTableName());
304
+ }
292
305
  this.expect('op', ')');
293
- if (schema[uCol]) schema[uCol].unique = true;
306
+ for (const uCol of uCols) {
307
+ if (schema[uCol]) schema[uCol].unique = true;
308
+ }
294
309
  }
295
310
  } else if (t.type === 'keyword' && t.value === 'CONSTRAINT') {
296
311
  this.expectKeyword('CONSTRAINT');
@@ -495,7 +510,7 @@ class Parser {
495
510
  else if (!(this.peek().type === 'op' && this.peek().value === ')')) col = this.parseScalar();
496
511
  this.expect('op', ')');
497
512
  aggregate = { type: 'COUNT', column: col };
498
- if (this.isKeyword('AS')) { this.next(); aggregate.alias = this.parseAlias(); }
513
+ aggregate.alias = this.parseOptionalAlias();
499
514
  columns.push({ expr: col, aggregate: 'COUNT', column: col, alias: aggregate.alias });
500
515
  } else if (t.type === 'keyword' && ['SUM', 'AVG', 'MIN', 'MAX'].includes(t.value)) {
501
516
  this.next();
@@ -504,7 +519,7 @@ class Parser {
504
519
  const col = this.parseScalar();
505
520
  this.expect('op', ')');
506
521
  aggregate = { type: fn, column: col };
507
- if (this.isKeyword('AS')) { this.next(); aggregate.alias = this.parseAlias(); }
522
+ aggregate.alias = this.parseOptionalAlias();
508
523
  columns.push({ expr: col, aggregate: fn, column: col, alias: aggregate.alias });
509
524
  } else if (t.type === 'op' && t.value === '*') {
510
525
  this.next();
@@ -512,14 +527,16 @@ class Parser {
512
527
  } else if (t.type === 'keyword' && t.value === 'CASE') {
513
528
  const caseExpr = this.parseOperand();
514
529
  let alias = null;
515
- if (this.isKeyword('AS')) { this.next(); alias = this.parseAlias(); }
530
+ alias = this.parseOptionalAlias();
516
531
  columns.push({ expr: null, caseExpr, alias });
517
532
  } else {
518
533
  // 列 / 常量 / 函数 / 算术表达式
519
534
  const expr = this.parseScalar();
520
535
  let alias = null;
521
- if (this.isKeyword('AS')) { this.next(); alias = this.parseAlias(); }
522
- if (expr.type === 'aggregate') {
536
+ alias = this.parseOptionalAlias();
537
+ if (expr.type === 'star') {
538
+ columns.push({ expr: '*' });
539
+ } else if (expr.type === 'aggregate') {
523
540
  columns.push({ expr: expr.column, aggregate: expr.fn, column: expr.column, alias, scalar: expr });
524
541
  } else if (expr.type === 'column') {
525
542
  columns.push({ expr: expr.name, scalar: expr, alias });
@@ -641,6 +658,12 @@ class Parser {
641
658
  return t.value;
642
659
  }
643
660
 
661
+ parseOptionalAlias() {
662
+ if (this.isKeyword('AS')) { this.next(); return this.parseAlias(); }
663
+ if (this.peek().type === 'ident') return this.parseAlias();
664
+ return null;
665
+ }
666
+
644
667
  parseColumnRef() {
645
668
  const t = this.next();
646
669
  if (t.type !== 'ident') throw new Error(`Expected column name, got '${t.value}'`);
@@ -718,6 +741,121 @@ class Parser {
718
741
  return { type: 'dropDatabase', database: name, ifExists };
719
742
  }
720
743
 
744
+ _skipAlterTail() {
745
+ while (!(this.peek().type === 'eof' || this.peek().value === ';')) {
746
+ if (this.peek().type === 'op' && this.peek().value === ',') return;
747
+ if (this.peek().type === 'keyword' && ['ADD', 'DROP', 'MODIFY', 'CHANGE', 'RENAME', 'ENGINE', 'CONVERT', 'DEFAULT'].includes(this.peek().value)) return;
748
+ this.next();
749
+ }
750
+ }
751
+
752
+ _skipFirstAfter() {
753
+ if (this.isKeyword('FIRST')) { this.next(); return; }
754
+ if (this.isKeyword('AFTER')) { this.next(); if (this.peek().type === 'ident') this.next(); }
755
+ }
756
+
757
+ _parseIndexColumns() {
758
+ this.expect('op', '(');
759
+ const columns = [];
760
+ for (;;) {
761
+ const col = this.parseTableName();
762
+ if (this.isKeyword('ASC') || this.isKeyword('DESC')) this.next();
763
+ columns.push(col);
764
+ if (this.peek().value === ',') { this.next(); continue; }
765
+ break;
766
+ }
767
+ this.expect('op', ')');
768
+ return columns;
769
+ }
770
+
771
+ parseAlter() {
772
+ this.expectKeyword('ALTER');
773
+ this.expectKeyword('TABLE');
774
+ const name = this.parseTableName();
775
+ const ops = [];
776
+ for (;;) {
777
+ const t = this.next();
778
+ if (t.type !== 'keyword') throw new Error(`Expected ALTER operation, got '${t.value}'`);
779
+ switch (t.value) {
780
+ case 'ADD': {
781
+ if (this.isKeyword('COLUMN')) this.next();
782
+ if (this.isKeyword('INDEX') || this.isKeyword('KEY') || this.isKeyword('UNIQUE') || this.isKeyword('FULLTEXT') || this.isKeyword('SPATIAL')) {
783
+ const unique = this.isKeyword('UNIQUE');
784
+ if (unique || this.isKeyword('FULLTEXT') || this.isKeyword('SPATIAL')) this.next();
785
+ if (this.isKeyword('INDEX') || this.isKeyword('KEY')) this.next();
786
+ let indexName = null;
787
+ if (this.peek().type === 'ident') indexName = this.parseTableName();
788
+ if (this.isKeyword('USING')) { this.next(); this.next(); }
789
+ const columns = this._parseIndexColumns();
790
+ ops.push({ op: 'addIndex', columns, unique, name: indexName });
791
+ } else if (this.isKeyword('PRIMARY')) {
792
+ this.expectKeyword('PRIMARY'); this.expectKeyword('KEY');
793
+ if (this.peek().type === 'op' && this.peek().value === '(') {
794
+ const columns = this._parseIndexColumns();
795
+ ops.push({ op: 'addPrimary', columns });
796
+ }
797
+ } else if (this.isKeyword('CONSTRAINT')) {
798
+ this.next();
799
+ if (this.peek().type === 'ident') this.next();
800
+ if (this.isKeyword('UNIQUE')) { this.next(); }
801
+ if (this.isKeyword('INDEX') || this.isKeyword('KEY')) { this.next(); if (this.peek().type === 'ident') this.next(); }
802
+ if (this.isKeyword('FOREIGN')) {
803
+ this.expectKeyword('FOREIGN'); this.expectKeyword('KEY');
804
+ const columns = this._parseIndexColumns();
805
+ this.expectKeyword('REFERENCES');
806
+ const refTable = this.parseTableName();
807
+ const refCols = this._parseIndexColumns();
808
+ this._skipAlterTail();
809
+ ops.push({ op: 'addForeign', columns, refTable, refCols });
810
+ } else {
811
+ const columns = this._parseIndexColumns();
812
+ ops.push({ op: 'addIndex', columns, unique: true });
813
+ }
814
+ } else {
815
+ const column = this.parseTableName();
816
+ const def = this.parseColumnDef();
817
+ this._skipFirstAfter();
818
+ ops.push({ op: 'addColumn', column, def });
819
+ }
820
+ break;
821
+ }
822
+ case 'DROP': {
823
+ if (this.isKeyword('COLUMN')) this.next();
824
+ if (this.isKeyword('PRIMARY')) { this.expectKeyword('PRIMARY'); this.expectKeyword('KEY'); ops.push({ op: 'dropPrimary' }); break; }
825
+ if (this.isKeyword('FOREIGN')) { this.next(); this.expectKeyword('KEY'); if (this.peek().type === 'ident') this.next(); ops.push({ op: 'dropIndex' }); break; }
826
+ if (this.isKeyword('INDEX') || this.isKeyword('KEY')) { this.next(); if (this.peek().type === 'ident') this.next(); ops.push({ op: 'dropIndex' }); break; }
827
+ const column = this.parseTableName();
828
+ ops.push({ op: 'dropColumn', column });
829
+ break;
830
+ }
831
+ case 'MODIFY':
832
+ case 'CHANGE': {
833
+ if (this.isKeyword('COLUMN')) this.next();
834
+ const column = this.parseTableName();
835
+ let newColumn = column;
836
+ if (t.value === 'CHANGE') newColumn = this.parseTableName();
837
+ const def = this.parseColumnDef();
838
+ this._skipFirstAfter();
839
+ ops.push({ op: t.value === 'CHANGE' ? 'changeColumn' : 'modifyColumn', column, newColumn, def });
840
+ break;
841
+ }
842
+ case 'RENAME': {
843
+ if (this.isKeyword('TO')) this.next();
844
+ const newName = this.parseTableName();
845
+ ops.push({ op: 'rename', newName });
846
+ break;
847
+ }
848
+ default:
849
+ this._skipAlterTail();
850
+ break;
851
+ }
852
+ if (this.peek().type === 'op' && this.peek().value === ',') { this.next(); continue; }
853
+ break;
854
+ }
855
+ this.optionalTailSemicolon();
856
+ return { type: 'alterTable', name, ops };
857
+ }
858
+
721
859
  parseShow() {
722
860
  this.expectKeyword('SHOW');
723
861
  if (this.isKeyword('TABLES')) {
@@ -861,6 +999,7 @@ class Parser {
861
999
  if (this.peek().type === 'op' && this.peek().value === '.') {
862
1000
  this.next();
863
1001
  const col = this.next();
1002
+ if (col.type === 'op' && col.value === '*') return { type: 'star', alias: t.value };
864
1003
  if (col.type !== 'ident') throw new Error(`Expected column name after '.', got '${col.value}'`);
865
1004
  return { type: 'column', name: t.value + '.' + col.value };
866
1005
  }
@@ -885,7 +1024,7 @@ class Parser {
885
1024
  if (t.type === 'sysvar') return { type: 'sysvar', name: t.value };
886
1025
  if (t.type === 'keyword' && t.value === 'NULL') return { type: 'value', value: null };
887
1026
  if (t.type === 'keyword' && t.value === 'CASE') return this.parseCase();
888
- if (t.type === 'keyword' && this.peek().type === 'op' && this.peek().value === '(') {
1027
+ if (t.type === 'keyword' && !['SUM', 'AVG', 'MIN', 'MAX', 'COUNT'].includes(t.value) && this.peek().type === 'op' && this.peek().value === '(') {
889
1028
  const name = t.value;
890
1029
  this.next();
891
1030
  const args = [];
@@ -1371,9 +1510,69 @@ class SQLExecutor {
1371
1510
  if (this.engine.hasTable(statement.name)) await this.engine.truncate(statement.name);
1372
1511
  return { ok: true, type: 'truncate', table: statement.name, affectedRows: 0 };
1373
1512
  }
1513
+ case 'alterTable': {
1514
+ const table = this.engine._tables ? this.engine._tables[statement.name] : null;
1515
+ if (!table) throw new Error(`Table '${statement.name}' does not exist`);
1516
+ for (const op of statement.ops) {
1517
+ switch (op.op) {
1518
+ case 'addColumn': {
1519
+ table._schema[op.column] = op.def;
1520
+ for (const row of table._rows) if (!(op.column in row)) row[op.column] = null;
1521
+ break;
1522
+ }
1523
+ case 'dropColumn': {
1524
+ delete table._schema[op.column];
1525
+ for (const row of table._rows) delete row[op.column];
1526
+ break;
1527
+ }
1528
+ case 'changeColumn':
1529
+ case 'modifyColumn': {
1530
+ const def = { ...op.def };
1531
+ const prev = table._schema[op.column];
1532
+ if (prev && op.column === op.newColumn) {
1533
+ if (prev.autoIncrement && !def.autoIncrement) def.autoIncrement = true;
1534
+ if (prev.primaryKey && !def.primaryKey) { def.primaryKey = true; def.unique = true; }
1535
+ }
1536
+ table._schema[op.column] = def;
1537
+ if (op.newColumn !== op.column) {
1538
+ table._schema[op.newColumn] = def;
1539
+ delete table._schema[op.column];
1540
+ for (const row of table._rows) {
1541
+ if (op.column in row) { row[op.newColumn] = row[op.column]; delete row[op.column]; }
1542
+ }
1543
+ }
1544
+ break;
1545
+ }
1546
+ case 'addPrimary': {
1547
+ for (const c of op.columns) { table._schema[c].primaryKey = true; table._schema[c].unique = true; }
1548
+ break;
1549
+ }
1550
+ case 'dropPrimary': {
1551
+ for (const def of Object.values(table._schema)) { if (def && typeof def === 'object') { def.primaryKey = false; } }
1552
+ break;
1553
+ }
1554
+ case 'addIndex': {
1555
+ if (!op.unique) break;
1556
+ for (const c of op.columns) table._schema[c].unique = true;
1557
+ break;
1558
+ }
1559
+ case 'addForeign':
1560
+ case 'dropIndex':
1561
+ case 'rename':
1562
+ default:
1563
+ break;
1564
+ }
1565
+ }
1566
+ await this._rebuildTableCache(table);
1567
+ await this.engine.flush();
1568
+ return { ok: true, type: 'alterTable', table: statement.name, affectedRows: 0 };
1569
+ }
1374
1570
  case 'insert': {
1375
1571
  let dataRows = statement.dataRows;
1376
- let schema = null;
1572
+ let schema = this.engine.getTableSchema
1573
+ ? await this.engine.getTableSchema(statement.name)
1574
+ : (this.engine._schemas ? this.engine._schemas[statement.name] : null);
1575
+ if (!schema) throw new Error(`Table '${statement.name}' does not exist`);
1377
1576
  const stripDefault = (row) => {
1378
1577
  const out = {};
1379
1578
  for (const [k, v] of Object.entries(row)) {
@@ -1383,14 +1582,15 @@ class SQLExecutor {
1383
1582
  return out;
1384
1583
  };
1385
1584
  if (statement.dataRows) {
1386
- statement.dataRows = statement.dataRows.map(stripDefault);
1585
+ statement.dataRows = statement.dataRows.map(row => {
1586
+ for (const [c, def] of Object.entries(schema)) {
1587
+ if (def.autoIncrement && (row[c] === null || row[c] === undefined)) delete row[c];
1588
+ }
1589
+ return stripDefault(row);
1590
+ });
1387
1591
  dataRows = statement.dataRows;
1388
1592
  }
1389
1593
  if (dataRows === null && statement.values) {
1390
- schema = this.engine.getTableSchema
1391
- ? await this.engine.getTableSchema(statement.name)
1392
- : (this.engine._schemas ? this.engine._schemas[statement.name] : null);
1393
- if (!schema) throw new Error(`Table '${statement.name}' does not exist`);
1394
1594
  const colNames = Object.keys(schema);
1395
1595
  const skipAuto = statement.values[0].length < colNames.length;
1396
1596
  dataRows = statement.values.map(vals => {
@@ -1410,11 +1610,6 @@ class SQLExecutor {
1410
1610
  });
1411
1611
  dataRows = dataRows.map(stripDefault);
1412
1612
  }
1413
- if (!schema) {
1414
- schema = this.engine.getTableSchema
1415
- ? await this.engine.getTableSchema(statement.name)
1416
- : (this.engine._schemas ? this.engine._schemas[statement.name] : null);
1417
- }
1418
1613
  const pkCols = schema ? Object.keys(schema).filter(k => schema[k].primaryKey) : [];
1419
1614
  let toInsert = dataRows;
1420
1615
  let updated = 0;
@@ -1425,7 +1620,10 @@ class SQLExecutor {
1425
1620
  if (explicit.length > 0) {
1426
1621
  const all = (await this.engine.find(statement.name, {}, { limit: 1e9, offset: 0 })).map(r => normalizeRow(r, schema));
1427
1622
  const pkMap = new Map();
1428
- for (const row of all) pkMap.set(keyOf(row), row.id);
1623
+ for (const row of all) {
1624
+ const keys = pkCols.map(c => row[c]).filter(v => v !== undefined && v !== null);
1625
+ if (keys.length === pkCols.length) pkMap.set(keyOf(row), keys);
1626
+ }
1429
1627
  const conflicts = [];
1430
1628
  const fresh = [];
1431
1629
  for (const d of dataRows) {
@@ -1443,7 +1641,13 @@ class SQLExecutor {
1443
1641
  for (const { d, existingId } of conflicts) {
1444
1642
  const data = {};
1445
1643
  for (const [col, val] of statement.onDuplicate) data[col] = val;
1446
- this.engine.updateById(statement.name, existingId, data);
1644
+ if (this.engine.updateById && existingId.length === 1) {
1645
+ this.engine.updateById(statement.name, existingId[0], data);
1646
+ } else if (this.engine.update) {
1647
+ const filter = {};
1648
+ pkCols.forEach((c, i) => { filter[c] = existingId[i]; });
1649
+ this.engine.update(statement.name, filter, data);
1650
+ }
1447
1651
  updated++;
1448
1652
  }
1449
1653
  await this.engine.flush();
@@ -1681,6 +1885,144 @@ class SQLExecutor {
1681
1885
  return this._readTable(item.table);
1682
1886
  }
1683
1887
 
1888
+ _infoSchemaColumns(view) {
1889
+ const defs = {
1890
+ 'tables': ['TABLE_CATALOG', 'TABLE_SCHEMA', 'TABLE_NAME', 'TABLE_TYPE', 'ENGINE', 'VERSION', 'ROW_FORMAT', 'TABLE_ROWS', 'AVG_ROW_LENGTH', 'DATA_LENGTH', 'MAX_DATA_LENGTH', 'INDEX_LENGTH', 'DATA_FREE', 'AUTO_INCREMENT', 'CREATE_TIME', 'UPDATE_TIME', 'CHECK_TIME', 'TABLE_COLLATION', 'CHECKSUM', 'CREATE_OPTIONS', 'TABLE_COMMENT'],
1891
+ 'columns': ['TABLE_CATALOG', 'TABLE_SCHEMA', 'TABLE_NAME', 'COLUMN_NAME', 'ORDINAL_POSITION', 'COLUMN_DEFAULT', 'IS_NULLABLE', 'DATA_TYPE', 'CHARACTER_MAXIMUM_LENGTH', 'CHARACTER_OCTET_LENGTH', 'NUMERIC_PRECISION', 'NUMERIC_SCALE', 'DATETIME_PRECISION', 'CHARACTER_SET_NAME', 'COLLATION_NAME', 'COLUMN_TYPE', 'COLUMN_KEY', 'EXTRA', 'PRIVILEGES', 'COLUMN_COMMENT', 'GENERATION_EXPRESSION'],
1892
+ 'schemata': ['CATALOG_NAME', 'SCHEMA_NAME', 'DEFAULT_CHARACTER_SET_NAME', 'DEFAULT_COLLATION_NAME', 'SQL_PATH', 'DEFAULT_ENCRYPTION'],
1893
+ 'statistics': ['TABLE_CATALOG', 'TABLE_SCHEMA', 'TABLE_NAME', 'NON_UNIQUE', 'INDEX_SCHEMA', 'INDEX_NAME', 'SEQ_IN_INDEX', 'COLUMN_NAME', 'COLLATION', 'CARDINALITY', 'SUB_PART', 'PACKED', 'NULLABLE', 'INDEX_TYPE', 'COMMENT', 'INDEX_COMMENT'],
1894
+ 'key_column_usage': ['CONSTRAINT_CATALOG', 'CONSTRAINT_SCHEMA', 'CONSTRAINT_NAME', 'TABLE_CATALOG', 'TABLE_SCHEMA', 'TABLE_NAME', 'COLUMN_NAME', 'ORDINAL_POSITION', 'POSITION_IN_UNIQUE_CONSTRAINT', 'REFERENCED_TABLE_SCHEMA', 'REFERENCED_TABLE_NAME', 'REFERENCED_COLUMN_NAME'],
1895
+ 'referential_constraints': ['CONSTRAINT_CATALOG', 'CONSTRAINT_SCHEMA', 'CONSTRAINT_NAME', 'UNIQUE_CONSTRAINT_CATALOG', 'UNIQUE_CONSTRAINT_SCHEMA', 'UNIQUE_CONSTRAINT_NAME', 'MATCH_OPTION', 'UPDATE_RULE', 'DELETE_RULE', 'TABLE_NAME', 'REFERENCED_TABLE_NAME'],
1896
+ 'table_constraints': ['CONSTRAINT_CATALOG', 'CONSTRAINT_SCHEMA', 'CONSTRAINT_NAME', 'TABLE_SCHEMA', 'TABLE_NAME', 'CONSTRAINT_TYPE'],
1897
+ };
1898
+ return defs[view] || ['COLUMN_NAME'];
1899
+ }
1900
+
1901
+ _infoSchemaType(def) {
1902
+ const t = String(def.type || '').toLowerCase();
1903
+ const m = { int: 'int', integer: 'int', smallint: 'smallint', mediumint: 'mediumint', bigint: 'bigint', tinyint: 'tinyint', string: 'varchar', varchar: 'varchar', char: 'char', text: 'text', tinytext: 'tinytext', mediumtext: 'mediumtext', longtext: 'longtext', blob: 'blob', float: 'float', double: 'double', real: 'double', decimal: 'decimal', numeric: 'decimal', boolean: 'tinyint', bool: 'tinyint', date: 'date', datetime: 'datetime', timestamp: 'timestamp', time: 'time', year: 'year', json: 'json', enum: 'enum', uuid: 'varchar', binary: 'varbinary' };
1904
+ return m[t] || t || 'varchar';
1905
+ }
1906
+
1907
+ async _infoSchemaRows(view) {
1908
+ const tables = this.engine._tableNames ? Array.from(this.engine._tableNames) : (this.engine.tables ? this.engine.tables() : []);
1909
+ const dbName = 'default';
1910
+ const base = { TABLE_CATALOG: 'def' };
1911
+ if (view === 'tables' || view === 'views') {
1912
+ const out = [];
1913
+ for (const name of tables) {
1914
+ const schema = this.engine.getTableSchema(name) || {};
1915
+ const table = this.engine._tables ? this.engine._tables[name] : null;
1916
+ const rowCount = table && table._rows ? table._rows.length : 0;
1917
+ out.push(Object.assign({}, base, {
1918
+ TABLE_SCHEMA: dbName,
1919
+ TABLE_NAME: name,
1920
+ TABLE_TYPE: view === 'views' ? 'VIEW' : 'BASE TABLE',
1921
+ ENGINE: 'InnoDB',
1922
+ VERSION: 10,
1923
+ ROW_FORMAT: 'Dynamic',
1924
+ TABLE_ROWS: rowCount,
1925
+ AVG_ROW_LENGTH: 0,
1926
+ DATA_LENGTH: 0,
1927
+ MAX_DATA_LENGTH: 0,
1928
+ INDEX_LENGTH: 0,
1929
+ DATA_FREE: 0,
1930
+ AUTO_INCREMENT: table && table._autoIncrement ? table._autoIncrement : null,
1931
+ CREATE_TIME: null,
1932
+ UPDATE_TIME: null,
1933
+ CHECK_TIME: null,
1934
+ TABLE_COLLATION: 'utf8mb4_general_ci',
1935
+ CHECKSUM: null,
1936
+ CREATE_OPTIONS: '',
1937
+ TABLE_COMMENT: '',
1938
+ }));
1939
+ }
1940
+ return out;
1941
+ }
1942
+ if (view === 'columns') {
1943
+ const out = [];
1944
+ for (const name of tables) {
1945
+ const schema = this.engine.getTableSchema(name) || {};
1946
+ let pos = 0;
1947
+ for (const [col, def] of Object.entries(schema)) {
1948
+ pos++;
1949
+ const dataType = this._infoSchemaType(def);
1950
+ const len = def.length != null ? def.length : (def.maxLength != null ? def.maxLength : null);
1951
+ const colType = len != null ? `${dataType}(${len})` : dataType;
1952
+ out.push(Object.assign({}, base, {
1953
+ TABLE_SCHEMA: dbName,
1954
+ TABLE_NAME: name,
1955
+ COLUMN_NAME: col,
1956
+ ORDINAL_POSITION: pos,
1957
+ COLUMN_DEFAULT: def.default !== undefined ? def.default : null,
1958
+ IS_NULLABLE: def.required || def.autoIncrement ? 'NO' : 'YES',
1959
+ DATA_TYPE: dataType,
1960
+ CHARACTER_MAXIMUM_LENGTH: /char|text/.test(dataType) ? len : null,
1961
+ CHARACTER_OCTET_LENGTH: /char|text/.test(dataType) ? (len ? len * 4 : null) : null,
1962
+ NUMERIC_PRECISION: /int|decimal|float|double|numeric/.test(dataType) ? 10 : null,
1963
+ NUMERIC_SCALE: /decimal|numeric/.test(dataType) ? 0 : null,
1964
+ DATETIME_PRECISION: null,
1965
+ CHARACTER_SET_NAME: /char|text/.test(dataType) ? 'utf8mb4' : null,
1966
+ COLLATION_NAME: /char|text/.test(dataType) ? 'utf8mb4_general_ci' : null,
1967
+ COLUMN_TYPE: colType,
1968
+ COLUMN_KEY: def.primaryKey ? 'PRI' : (def.unique ? 'UNI' : ''),
1969
+ EXTRA: def.autoIncrement ? 'auto_increment' : '',
1970
+ PRIVILEGES: 'select,insert,update,references',
1971
+ COLUMN_COMMENT: def.comment || '',
1972
+ GENERATION_EXPRESSION: '',
1973
+ }));
1974
+ }
1975
+ }
1976
+ return out;
1977
+ }
1978
+ if (view === 'schemata') {
1979
+ return [Object.assign({}, base, {
1980
+ SCHEMA_NAME: dbName,
1981
+ CATALOG_NAME: 'def',
1982
+ DEFAULT_CHARACTER_SET_NAME: 'utf8mb4',
1983
+ DEFAULT_COLLATION_NAME: 'utf8mb4_general_ci',
1984
+ SQL_PATH: null,
1985
+ DEFAULT_ENCRYPTION: 'NO',
1986
+ })];
1987
+ }
1988
+ return [];
1989
+ }
1990
+
1991
+ async _rebuildTableCache(table) {
1992
+ const schema = table._schema;
1993
+ table._primaryKey = null;
1994
+ table._autoIncrementField = null;
1995
+ table._dateFields = {};
1996
+ for (const [f, def] of Object.entries(schema)) {
1997
+ if (f === '_softDelete') continue;
1998
+ const isPk = def.primaryKey || def.primary === true;
1999
+ if (isPk && !table._primaryKey) table._primaryKey = f;
2000
+ if (def.autoIncrement) {
2001
+ table._autoIncrementField = f;
2002
+ if (isPk) table._primaryKey = f;
2003
+ }
2004
+ if (['date', 'datetime', 'timestamp', 'time'].includes(def.type)) table._dateFields[f] = def.type;
2005
+ }
2006
+ table._cachedSchemaFields = Object.keys(schema).filter(f => f !== '_softDelete');
2007
+ table._cachedDateFields = Object.keys(table._dateFields);
2008
+ table._cachedUniqueFields = table._cachedSchemaFields.filter(f => schema[f].unique);
2009
+ table._cachedRequiredFields = table._cachedSchemaFields.filter(f => schema[f].required);
2010
+ table._pkIndex = table._primaryKey ? new Map() : null;
2011
+ table._btrees = {};
2012
+ for (const [f, def] of Object.entries(schema)) {
2013
+ if (f === '_softDelete') continue;
2014
+ if (def.primaryKey || def.unique) {
2015
+ table._btrees[f] = new (require('./btree'))(64, true);
2016
+ }
2017
+ }
2018
+ table._rows.forEach((row, idx) => {
2019
+ for (const [f, tree] of Object.entries(table._btrees)) {
2020
+ const v = row[f];
2021
+ if (v !== undefined && v !== null) tree.insert(v, idx);
2022
+ }
2023
+ });
2024
+ }
2025
+
1684
2026
  // 物化表达式树中的子查询:IN (SELECT ...) -> expr.list
1685
2027
  async _materialize(expr) {
1686
2028
  if (!expr || typeof expr !== 'object') return;
@@ -1805,13 +2147,24 @@ class SQLExecutor {
1805
2147
  // 读第一表
1806
2148
  const firstItem = statement.from.tables[0];
1807
2149
  if (firstItem && firstItem.table && String(firstItem.table).toLowerCase().startsWith('information_schema.')) {
2150
+ const view = String(firstItem.table).toLowerCase().split('.')[1];
2151
+ const all = await this._infoSchemaRows(view);
2152
+ const filtered = statement.where ? all.filter(r => evaluateExpr(statement.where, r)) : all;
1808
2153
  const cols = statement.columns.map(c => scalarColumnName(c));
1809
- return { ok: true, type: 'select', table: null, columns: cols, rows: [], raw: [] };
2154
+ const isStar = cols.length === 1 && cols[0] === '*';
2155
+ let outCols;
2156
+ if (isStar) {
2157
+ outCols = filtered.length > 0 ? Object.keys(filtered[0]) : this._infoSchemaColumns(view);
2158
+ } else {
2159
+ outCols = cols;
2160
+ }
2161
+ const rows = filtered.map(r => outCols.map(c => (c in r ? r[c] : null)));
2162
+ return { ok: true, type: 'select', table: firstItem.table, columns: outCols, rows, raw: filtered };
1810
2163
  }
1811
2164
  let rowsAll;
1812
2165
  if (firstItem.subquery) {
1813
2166
  const res = await this.executeSelect(firstItem.subquery);
1814
- rowsAll = { rows: this._subQueryRows(res), schema: null };
2167
+ rowsAll = { rows: this._subQueryRows(res), schema: null, columns: res.columns };
1815
2168
  } else {
1816
2169
  rowsAll = await this._readTable(firstItem.table);
1817
2170
  }
@@ -1962,7 +2315,14 @@ class SQLExecutor {
1962
2315
  const schemaKeys = schema ? Object.keys(schema) : [];
1963
2316
  const pkCols = schemaKeys.filter(k => schema && schema[k] && schema[k].primaryKey);
1964
2317
  const pk = pkCols.length > 0 ? pkCols[0] : (schemaKeys[0] || 'id');
1965
- const cols = schema ? [pk, ...schemaKeys.filter(k => k !== pk)] : Object.keys(all[0] || {}).filter((v, i, a) => a.indexOf(v) === i);
2318
+ let cols;
2319
+ if (schema) {
2320
+ cols = [pk, ...schemaKeys.filter(k => k !== pk)];
2321
+ } else if (rowsAll && rowsAll.columns) {
2322
+ cols = rowsAll.columns;
2323
+ } else {
2324
+ cols = Object.keys(all[0] || {}).filter((v, i, a) => a.indexOf(v) === i);
2325
+ }
1966
2326
  return { ok: true, type: 'select', table: tableName, columns: cols, rows: rows.map(r => cols.map(c => r[c])), raw: rows };
1967
2327
  }
1968
2328
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jsql-neo",
3
- "version": "4.4.3",
3
+ "version": "4.5.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",
@@ -13,6 +13,8 @@
13
13
  "default": "./index.js"
14
14
  },
15
15
  "./cli": "./bin/jsql",
16
+ "./mysql2": "./lib/mysql_compat.js",
17
+ "./mysql": "./lib/mysql_compat.js",
16
18
  "./wasm/browser.mjs": {
17
19
  "types": "./wasm/browser.d.ts",
18
20
  "default": "./wasm/browser.mjs"