jsql-neo 4.4.2 → 4.5.0

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
@@ -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,
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();
@@ -495,7 +498,7 @@ class Parser {
495
498
  else if (!(this.peek().type === 'op' && this.peek().value === ')')) col = this.parseScalar();
496
499
  this.expect('op', ')');
497
500
  aggregate = { type: 'COUNT', column: col };
498
- if (this.isKeyword('AS')) { this.next(); aggregate.alias = this.parseAlias(); }
501
+ aggregate.alias = this.parseOptionalAlias();
499
502
  columns.push({ expr: col, aggregate: 'COUNT', column: col, alias: aggregate.alias });
500
503
  } else if (t.type === 'keyword' && ['SUM', 'AVG', 'MIN', 'MAX'].includes(t.value)) {
501
504
  this.next();
@@ -504,7 +507,7 @@ class Parser {
504
507
  const col = this.parseScalar();
505
508
  this.expect('op', ')');
506
509
  aggregate = { type: fn, column: col };
507
- if (this.isKeyword('AS')) { this.next(); aggregate.alias = this.parseAlias(); }
510
+ aggregate.alias = this.parseOptionalAlias();
508
511
  columns.push({ expr: col, aggregate: fn, column: col, alias: aggregate.alias });
509
512
  } else if (t.type === 'op' && t.value === '*') {
510
513
  this.next();
@@ -512,14 +515,16 @@ class Parser {
512
515
  } else if (t.type === 'keyword' && t.value === 'CASE') {
513
516
  const caseExpr = this.parseOperand();
514
517
  let alias = null;
515
- if (this.isKeyword('AS')) { this.next(); alias = this.parseAlias(); }
518
+ alias = this.parseOptionalAlias();
516
519
  columns.push({ expr: null, caseExpr, alias });
517
520
  } else {
518
521
  // 列 / 常量 / 函数 / 算术表达式
519
522
  const expr = this.parseScalar();
520
523
  let alias = null;
521
- if (this.isKeyword('AS')) { this.next(); alias = this.parseAlias(); }
522
- if (expr.type === 'aggregate') {
524
+ alias = this.parseOptionalAlias();
525
+ if (expr.type === 'star') {
526
+ columns.push({ expr: '*' });
527
+ } else if (expr.type === 'aggregate') {
523
528
  columns.push({ expr: expr.column, aggregate: expr.fn, column: expr.column, alias, scalar: expr });
524
529
  } else if (expr.type === 'column') {
525
530
  columns.push({ expr: expr.name, scalar: expr, alias });
@@ -641,6 +646,12 @@ class Parser {
641
646
  return t.value;
642
647
  }
643
648
 
649
+ parseOptionalAlias() {
650
+ if (this.isKeyword('AS')) { this.next(); return this.parseAlias(); }
651
+ if (this.peek().type === 'ident') return this.parseAlias();
652
+ return null;
653
+ }
654
+
644
655
  parseColumnRef() {
645
656
  const t = this.next();
646
657
  if (t.type !== 'ident') throw new Error(`Expected column name, got '${t.value}'`);
@@ -718,6 +729,121 @@ class Parser {
718
729
  return { type: 'dropDatabase', database: name, ifExists };
719
730
  }
720
731
 
732
+ _skipAlterTail() {
733
+ while (!(this.peek().type === 'eof' || this.peek().value === ';')) {
734
+ if (this.peek().type === 'op' && this.peek().value === ',') return;
735
+ if (this.peek().type === 'keyword' && ['ADD', 'DROP', 'MODIFY', 'CHANGE', 'RENAME', 'ENGINE', 'CONVERT', 'DEFAULT'].includes(this.peek().value)) return;
736
+ this.next();
737
+ }
738
+ }
739
+
740
+ _skipFirstAfter() {
741
+ if (this.isKeyword('FIRST')) { this.next(); return; }
742
+ if (this.isKeyword('AFTER')) { this.next(); if (this.peek().type === 'ident') this.next(); }
743
+ }
744
+
745
+ _parseIndexColumns() {
746
+ this.expect('op', '(');
747
+ const columns = [];
748
+ for (;;) {
749
+ const col = this.parseTableName();
750
+ if (this.isKeyword('ASC') || this.isKeyword('DESC')) this.next();
751
+ columns.push(col);
752
+ if (this.peek().value === ',') { this.next(); continue; }
753
+ break;
754
+ }
755
+ this.expect('op', ')');
756
+ return columns;
757
+ }
758
+
759
+ parseAlter() {
760
+ this.expectKeyword('ALTER');
761
+ this.expectKeyword('TABLE');
762
+ const name = this.parseTableName();
763
+ const ops = [];
764
+ for (;;) {
765
+ const t = this.next();
766
+ if (t.type !== 'keyword') throw new Error(`Expected ALTER operation, got '${t.value}'`);
767
+ switch (t.value) {
768
+ case 'ADD': {
769
+ if (this.isKeyword('COLUMN')) this.next();
770
+ if (this.isKeyword('INDEX') || this.isKeyword('KEY') || this.isKeyword('UNIQUE') || this.isKeyword('FULLTEXT') || this.isKeyword('SPATIAL')) {
771
+ const unique = this.isKeyword('UNIQUE');
772
+ if (unique || this.isKeyword('FULLTEXT') || this.isKeyword('SPATIAL')) this.next();
773
+ if (this.isKeyword('INDEX') || this.isKeyword('KEY')) this.next();
774
+ let indexName = null;
775
+ if (this.peek().type === 'ident') indexName = this.parseTableName();
776
+ if (this.isKeyword('USING')) { this.next(); this.next(); }
777
+ const columns = this._parseIndexColumns();
778
+ ops.push({ op: 'addIndex', columns, unique, name: indexName });
779
+ } else if (this.isKeyword('PRIMARY')) {
780
+ this.expectKeyword('PRIMARY'); this.expectKeyword('KEY');
781
+ if (this.peek().type === 'op' && this.peek().value === '(') {
782
+ const columns = this._parseIndexColumns();
783
+ ops.push({ op: 'addPrimary', columns });
784
+ }
785
+ } else if (this.isKeyword('CONSTRAINT')) {
786
+ this.next();
787
+ if (this.peek().type === 'ident') this.next();
788
+ if (this.isKeyword('UNIQUE')) { this.next(); }
789
+ if (this.isKeyword('INDEX') || this.isKeyword('KEY')) { this.next(); if (this.peek().type === 'ident') this.next(); }
790
+ if (this.isKeyword('FOREIGN')) {
791
+ this.expectKeyword('FOREIGN'); this.expectKeyword('KEY');
792
+ const columns = this._parseIndexColumns();
793
+ this.expectKeyword('REFERENCES');
794
+ const refTable = this.parseTableName();
795
+ const refCols = this._parseIndexColumns();
796
+ this._skipAlterTail();
797
+ ops.push({ op: 'addForeign', columns, refTable, refCols });
798
+ } else {
799
+ const columns = this._parseIndexColumns();
800
+ ops.push({ op: 'addIndex', columns, unique: true });
801
+ }
802
+ } else {
803
+ const column = this.parseTableName();
804
+ const def = this.parseColumnDef();
805
+ this._skipFirstAfter();
806
+ ops.push({ op: 'addColumn', column, def });
807
+ }
808
+ break;
809
+ }
810
+ case 'DROP': {
811
+ if (this.isKeyword('COLUMN')) this.next();
812
+ if (this.isKeyword('PRIMARY')) { this.expectKeyword('PRIMARY'); this.expectKeyword('KEY'); ops.push({ op: 'dropPrimary' }); break; }
813
+ if (this.isKeyword('FOREIGN')) { this.next(); this.expectKeyword('KEY'); if (this.peek().type === 'ident') this.next(); ops.push({ op: 'dropIndex' }); break; }
814
+ if (this.isKeyword('INDEX') || this.isKeyword('KEY')) { this.next(); if (this.peek().type === 'ident') this.next(); ops.push({ op: 'dropIndex' }); break; }
815
+ const column = this.parseTableName();
816
+ ops.push({ op: 'dropColumn', column });
817
+ break;
818
+ }
819
+ case 'MODIFY':
820
+ case 'CHANGE': {
821
+ if (this.isKeyword('COLUMN')) this.next();
822
+ const column = this.parseTableName();
823
+ let newColumn = column;
824
+ if (t.value === 'CHANGE') newColumn = this.parseTableName();
825
+ const def = this.parseColumnDef();
826
+ this._skipFirstAfter();
827
+ ops.push({ op: t.value === 'CHANGE' ? 'changeColumn' : 'modifyColumn', column, newColumn, def });
828
+ break;
829
+ }
830
+ case 'RENAME': {
831
+ if (this.isKeyword('TO')) this.next();
832
+ const newName = this.parseTableName();
833
+ ops.push({ op: 'rename', newName });
834
+ break;
835
+ }
836
+ default:
837
+ this._skipAlterTail();
838
+ break;
839
+ }
840
+ if (this.peek().type === 'op' && this.peek().value === ',') { this.next(); continue; }
841
+ break;
842
+ }
843
+ this.optionalTailSemicolon();
844
+ return { type: 'alterTable', name, ops };
845
+ }
846
+
721
847
  parseShow() {
722
848
  this.expectKeyword('SHOW');
723
849
  if (this.isKeyword('TABLES')) {
@@ -861,6 +987,7 @@ class Parser {
861
987
  if (this.peek().type === 'op' && this.peek().value === '.') {
862
988
  this.next();
863
989
  const col = this.next();
990
+ if (col.type === 'op' && col.value === '*') return { type: 'star', alias: t.value };
864
991
  if (col.type !== 'ident') throw new Error(`Expected column name after '.', got '${col.value}'`);
865
992
  return { type: 'column', name: t.value + '.' + col.value };
866
993
  }
@@ -1371,9 +1498,69 @@ class SQLExecutor {
1371
1498
  if (this.engine.hasTable(statement.name)) await this.engine.truncate(statement.name);
1372
1499
  return { ok: true, type: 'truncate', table: statement.name, affectedRows: 0 };
1373
1500
  }
1501
+ case 'alterTable': {
1502
+ const table = this.engine._tables ? this.engine._tables[statement.name] : null;
1503
+ if (!table) throw new Error(`Table '${statement.name}' does not exist`);
1504
+ for (const op of statement.ops) {
1505
+ switch (op.op) {
1506
+ case 'addColumn': {
1507
+ table._schema[op.column] = op.def;
1508
+ for (const row of table._rows) if (!(op.column in row)) row[op.column] = null;
1509
+ break;
1510
+ }
1511
+ case 'dropColumn': {
1512
+ delete table._schema[op.column];
1513
+ for (const row of table._rows) delete row[op.column];
1514
+ break;
1515
+ }
1516
+ case 'changeColumn':
1517
+ case 'modifyColumn': {
1518
+ const def = { ...op.def };
1519
+ const prev = table._schema[op.column];
1520
+ if (prev && op.column === op.newColumn) {
1521
+ if (prev.autoIncrement && !def.autoIncrement) def.autoIncrement = true;
1522
+ if (prev.primaryKey && !def.primaryKey) { def.primaryKey = true; def.unique = true; }
1523
+ }
1524
+ table._schema[op.column] = def;
1525
+ if (op.newColumn !== op.column) {
1526
+ table._schema[op.newColumn] = def;
1527
+ delete table._schema[op.column];
1528
+ for (const row of table._rows) {
1529
+ if (op.column in row) { row[op.newColumn] = row[op.column]; delete row[op.column]; }
1530
+ }
1531
+ }
1532
+ break;
1533
+ }
1534
+ case 'addPrimary': {
1535
+ for (const c of op.columns) { table._schema[c].primaryKey = true; table._schema[c].unique = true; }
1536
+ break;
1537
+ }
1538
+ case 'dropPrimary': {
1539
+ for (const def of Object.values(table._schema)) { if (def && typeof def === 'object') { def.primaryKey = false; } }
1540
+ break;
1541
+ }
1542
+ case 'addIndex': {
1543
+ if (!op.unique) break;
1544
+ for (const c of op.columns) table._schema[c].unique = true;
1545
+ break;
1546
+ }
1547
+ case 'addForeign':
1548
+ case 'dropIndex':
1549
+ case 'rename':
1550
+ default:
1551
+ break;
1552
+ }
1553
+ }
1554
+ await this._rebuildTableCache(table);
1555
+ await this.engine.flush();
1556
+ return { ok: true, type: 'alterTable', table: statement.name, affectedRows: 0 };
1557
+ }
1374
1558
  case 'insert': {
1375
1559
  let dataRows = statement.dataRows;
1376
- let schema = null;
1560
+ let schema = this.engine.getTableSchema
1561
+ ? await this.engine.getTableSchema(statement.name)
1562
+ : (this.engine._schemas ? this.engine._schemas[statement.name] : null);
1563
+ if (!schema) throw new Error(`Table '${statement.name}' does not exist`);
1377
1564
  const stripDefault = (row) => {
1378
1565
  const out = {};
1379
1566
  for (const [k, v] of Object.entries(row)) {
@@ -1383,14 +1570,15 @@ class SQLExecutor {
1383
1570
  return out;
1384
1571
  };
1385
1572
  if (statement.dataRows) {
1386
- statement.dataRows = statement.dataRows.map(stripDefault);
1573
+ statement.dataRows = statement.dataRows.map(row => {
1574
+ for (const [c, def] of Object.entries(schema)) {
1575
+ if (def.autoIncrement && (row[c] === null || row[c] === undefined)) delete row[c];
1576
+ }
1577
+ return stripDefault(row);
1578
+ });
1387
1579
  dataRows = statement.dataRows;
1388
1580
  }
1389
1581
  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
1582
  const colNames = Object.keys(schema);
1395
1583
  const skipAuto = statement.values[0].length < colNames.length;
1396
1584
  dataRows = statement.values.map(vals => {
@@ -1410,11 +1598,6 @@ class SQLExecutor {
1410
1598
  });
1411
1599
  dataRows = dataRows.map(stripDefault);
1412
1600
  }
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
1601
  const pkCols = schema ? Object.keys(schema).filter(k => schema[k].primaryKey) : [];
1419
1602
  let toInsert = dataRows;
1420
1603
  let updated = 0;
@@ -1681,6 +1864,144 @@ class SQLExecutor {
1681
1864
  return this._readTable(item.table);
1682
1865
  }
1683
1866
 
1867
+ _infoSchemaColumns(view) {
1868
+ const defs = {
1869
+ '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'],
1870
+ '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'],
1871
+ 'schemata': ['CATALOG_NAME', 'SCHEMA_NAME', 'DEFAULT_CHARACTER_SET_NAME', 'DEFAULT_COLLATION_NAME', 'SQL_PATH', 'DEFAULT_ENCRYPTION'],
1872
+ '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'],
1873
+ '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'],
1874
+ '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'],
1875
+ 'table_constraints': ['CONSTRAINT_CATALOG', 'CONSTRAINT_SCHEMA', 'CONSTRAINT_NAME', 'TABLE_SCHEMA', 'TABLE_NAME', 'CONSTRAINT_TYPE'],
1876
+ };
1877
+ return defs[view] || ['COLUMN_NAME'];
1878
+ }
1879
+
1880
+ _infoSchemaType(def) {
1881
+ const t = String(def.type || '').toLowerCase();
1882
+ 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' };
1883
+ return m[t] || t || 'varchar';
1884
+ }
1885
+
1886
+ async _infoSchemaRows(view) {
1887
+ const tables = this.engine._tableNames ? Array.from(this.engine._tableNames) : (this.engine.tables ? this.engine.tables() : []);
1888
+ const dbName = 'default';
1889
+ const base = { TABLE_CATALOG: 'def' };
1890
+ if (view === 'tables' || view === 'views') {
1891
+ const out = [];
1892
+ for (const name of tables) {
1893
+ const schema = this.engine.getTableSchema(name) || {};
1894
+ const table = this.engine._tables ? this.engine._tables[name] : null;
1895
+ const rowCount = table && table._rows ? table._rows.length : 0;
1896
+ out.push(Object.assign({}, base, {
1897
+ TABLE_SCHEMA: dbName,
1898
+ TABLE_NAME: name,
1899
+ TABLE_TYPE: view === 'views' ? 'VIEW' : 'BASE TABLE',
1900
+ ENGINE: 'InnoDB',
1901
+ VERSION: 10,
1902
+ ROW_FORMAT: 'Dynamic',
1903
+ TABLE_ROWS: rowCount,
1904
+ AVG_ROW_LENGTH: 0,
1905
+ DATA_LENGTH: 0,
1906
+ MAX_DATA_LENGTH: 0,
1907
+ INDEX_LENGTH: 0,
1908
+ DATA_FREE: 0,
1909
+ AUTO_INCREMENT: table && table._autoIncrement ? table._autoIncrement : null,
1910
+ CREATE_TIME: null,
1911
+ UPDATE_TIME: null,
1912
+ CHECK_TIME: null,
1913
+ TABLE_COLLATION: 'utf8mb4_general_ci',
1914
+ CHECKSUM: null,
1915
+ CREATE_OPTIONS: '',
1916
+ TABLE_COMMENT: '',
1917
+ }));
1918
+ }
1919
+ return out;
1920
+ }
1921
+ if (view === 'columns') {
1922
+ const out = [];
1923
+ for (const name of tables) {
1924
+ const schema = this.engine.getTableSchema(name) || {};
1925
+ let pos = 0;
1926
+ for (const [col, def] of Object.entries(schema)) {
1927
+ pos++;
1928
+ const dataType = this._infoSchemaType(def);
1929
+ const len = def.length != null ? def.length : (def.maxLength != null ? def.maxLength : null);
1930
+ const colType = len != null ? `${dataType}(${len})` : dataType;
1931
+ out.push(Object.assign({}, base, {
1932
+ TABLE_SCHEMA: dbName,
1933
+ TABLE_NAME: name,
1934
+ COLUMN_NAME: col,
1935
+ ORDINAL_POSITION: pos,
1936
+ COLUMN_DEFAULT: def.default !== undefined ? def.default : null,
1937
+ IS_NULLABLE: def.required || def.autoIncrement ? 'NO' : 'YES',
1938
+ DATA_TYPE: dataType,
1939
+ CHARACTER_MAXIMUM_LENGTH: /char|text/.test(dataType) ? len : null,
1940
+ CHARACTER_OCTET_LENGTH: /char|text/.test(dataType) ? (len ? len * 4 : null) : null,
1941
+ NUMERIC_PRECISION: /int|decimal|float|double|numeric/.test(dataType) ? 10 : null,
1942
+ NUMERIC_SCALE: /decimal|numeric/.test(dataType) ? 0 : null,
1943
+ DATETIME_PRECISION: null,
1944
+ CHARACTER_SET_NAME: /char|text/.test(dataType) ? 'utf8mb4' : null,
1945
+ COLLATION_NAME: /char|text/.test(dataType) ? 'utf8mb4_general_ci' : null,
1946
+ COLUMN_TYPE: colType,
1947
+ COLUMN_KEY: def.primaryKey ? 'PRI' : (def.unique ? 'UNI' : ''),
1948
+ EXTRA: def.autoIncrement ? 'auto_increment' : '',
1949
+ PRIVILEGES: 'select,insert,update,references',
1950
+ COLUMN_COMMENT: def.comment || '',
1951
+ GENERATION_EXPRESSION: '',
1952
+ }));
1953
+ }
1954
+ }
1955
+ return out;
1956
+ }
1957
+ if (view === 'schemata') {
1958
+ return [Object.assign({}, base, {
1959
+ SCHEMA_NAME: dbName,
1960
+ CATALOG_NAME: 'def',
1961
+ DEFAULT_CHARACTER_SET_NAME: 'utf8mb4',
1962
+ DEFAULT_COLLATION_NAME: 'utf8mb4_general_ci',
1963
+ SQL_PATH: null,
1964
+ DEFAULT_ENCRYPTION: 'NO',
1965
+ })];
1966
+ }
1967
+ return [];
1968
+ }
1969
+
1970
+ async _rebuildTableCache(table) {
1971
+ const schema = table._schema;
1972
+ table._primaryKey = null;
1973
+ table._autoIncrementField = null;
1974
+ table._dateFields = {};
1975
+ for (const [f, def] of Object.entries(schema)) {
1976
+ if (f === '_softDelete') continue;
1977
+ const isPk = def.primaryKey || def.primary === true;
1978
+ if (isPk && !table._primaryKey) table._primaryKey = f;
1979
+ if (def.autoIncrement) {
1980
+ table._autoIncrementField = f;
1981
+ if (isPk) table._primaryKey = f;
1982
+ }
1983
+ if (['date', 'datetime', 'timestamp', 'time'].includes(def.type)) table._dateFields[f] = def.type;
1984
+ }
1985
+ table._cachedSchemaFields = Object.keys(schema).filter(f => f !== '_softDelete');
1986
+ table._cachedDateFields = Object.keys(table._dateFields);
1987
+ table._cachedUniqueFields = table._cachedSchemaFields.filter(f => schema[f].unique);
1988
+ table._cachedRequiredFields = table._cachedSchemaFields.filter(f => schema[f].required);
1989
+ table._pkIndex = table._primaryKey ? new Map() : null;
1990
+ table._btrees = {};
1991
+ for (const [f, def] of Object.entries(schema)) {
1992
+ if (f === '_softDelete') continue;
1993
+ if (def.primaryKey || def.unique) {
1994
+ table._btrees[f] = new (require('./btree'))(64, true);
1995
+ }
1996
+ }
1997
+ table._rows.forEach((row, idx) => {
1998
+ for (const [f, tree] of Object.entries(table._btrees)) {
1999
+ const v = row[f];
2000
+ if (v !== undefined && v !== null) tree.insert(v, idx);
2001
+ }
2002
+ });
2003
+ }
2004
+
1684
2005
  // 物化表达式树中的子查询:IN (SELECT ...) -> expr.list
1685
2006
  async _materialize(expr) {
1686
2007
  if (!expr || typeof expr !== 'object') return;
@@ -1805,13 +2126,24 @@ class SQLExecutor {
1805
2126
  // 读第一表
1806
2127
  const firstItem = statement.from.tables[0];
1807
2128
  if (firstItem && firstItem.table && String(firstItem.table).toLowerCase().startsWith('information_schema.')) {
2129
+ const view = String(firstItem.table).toLowerCase().split('.')[1];
2130
+ const all = await this._infoSchemaRows(view);
2131
+ const filtered = statement.where ? all.filter(r => evaluateExpr(statement.where, r)) : all;
1808
2132
  const cols = statement.columns.map(c => scalarColumnName(c));
1809
- return { ok: true, type: 'select', table: null, columns: cols, rows: [], raw: [] };
2133
+ const isStar = cols.length === 1 && cols[0] === '*';
2134
+ let outCols;
2135
+ if (isStar) {
2136
+ outCols = filtered.length > 0 ? Object.keys(filtered[0]) : this._infoSchemaColumns(view);
2137
+ } else {
2138
+ outCols = cols;
2139
+ }
2140
+ const rows = filtered.map(r => outCols.map(c => (c in r ? r[c] : null)));
2141
+ return { ok: true, type: 'select', table: firstItem.table, columns: outCols, rows, raw: filtered };
1810
2142
  }
1811
2143
  let rowsAll;
1812
2144
  if (firstItem.subquery) {
1813
2145
  const res = await this.executeSelect(firstItem.subquery);
1814
- rowsAll = { rows: this._subQueryRows(res), schema: null };
2146
+ rowsAll = { rows: this._subQueryRows(res), schema: null, columns: res.columns };
1815
2147
  } else {
1816
2148
  rowsAll = await this._readTable(firstItem.table);
1817
2149
  }
@@ -1962,7 +2294,14 @@ class SQLExecutor {
1962
2294
  const schemaKeys = schema ? Object.keys(schema) : [];
1963
2295
  const pkCols = schemaKeys.filter(k => schema && schema[k] && schema[k].primaryKey);
1964
2296
  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);
2297
+ let cols;
2298
+ if (schema) {
2299
+ cols = [pk, ...schemaKeys.filter(k => k !== pk)];
2300
+ } else if (rowsAll && rowsAll.columns) {
2301
+ cols = rowsAll.columns;
2302
+ } else {
2303
+ cols = Object.keys(all[0] || {}).filter((v, i, a) => a.indexOf(v) === i);
2304
+ }
1966
2305
  return { ok: true, type: 'select', table: tableName, columns: cols, rows: rows.map(r => cols.map(c => r[c])), raw: rows };
1967
2306
  }
1968
2307
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jsql-neo",
3
- "version": "4.4.2",
3
+ "version": "4.5.0",
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"
@@ -20,7 +22,8 @@
20
22
  "./wasm/browser_bg.mjs": "./wasm/browser_bg.mjs",
21
23
  "./wasm/browser.d.ts": "./wasm/browser.d.ts",
22
24
  "./wasm/*": "./wasm/*",
23
- "./lib/*": "./lib/*",
25
+ "./lib/*.js": "./lib/*.js",
26
+ "./lib/*": "./lib/*.js",
24
27
  "./package.json": "./package.json"
25
28
  },
26
29
  "bin": {