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 +35 -0
- package/lib/database.js +42 -0
- package/lib/mysql_compat.js +95 -32
- package/lib/mysql_server.js +14 -3
- package/lib/native_client.js +15 -6
- package/lib/sql.js +387 -27
- package/package.json +3 -1
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
|
|
package/lib/mysql_compat.js
CHANGED
|
@@ -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
|
|
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 ||
|
|
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
|
-
|
|
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(
|
|
72
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
90
|
-
|
|
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
|
|
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.
|
|
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
|
|
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/mysql_server.js
CHANGED
|
@@ -104,8 +104,19 @@ function handshakePacket(connectionId, seed) {
|
|
|
104
104
|
return b.build();
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
-
|
|
108
|
-
|
|
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 =
|
|
378
|
+
this.seed = genSeed(20);
|
|
368
379
|
this.buffer = Buffer.alloc(0);
|
|
369
380
|
this.sequence = 0;
|
|
370
381
|
this.authenticated = false;
|
package/lib/native_client.js
CHANGED
|
@@ -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
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
522
|
-
if (expr.type === '
|
|
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 =
|
|
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(
|
|
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)
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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"
|