jsql-neo 5.2.0 → 5.3.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/README.md +7626 -193
- package/bin/jsql +13 -0
- package/index.js +16 -0
- package/lib/database.js +20 -1
- package/lib/mongo_server.js +608 -0
- package/lib/multiserver.js +141 -0
- package/lib/mysql_server.js +1 -1
- package/lib/pg_server.js +864 -0
- package/lib/redis_server.js +129 -15
- package/lib/tui.js +503 -0
- package/package.json +1 -1
package/bin/jsql
CHANGED
|
@@ -215,6 +215,19 @@ const cli = yaggs({ pkg: require('../package.json') })
|
|
|
215
215
|
setInterval(() => {}, 1 << 30);
|
|
216
216
|
} catch (e) { console.error(`Error: ${e.message}`); process.exitCode = 1; }
|
|
217
217
|
})
|
|
218
|
+
.command('tui', 'Interactive SQL terminal (zero-dependency TUI)', (sub) => {
|
|
219
|
+
sub.option('data-dir', { alias: ['d'], type: 'string', description: 'Data directory (default: in-memory)' });
|
|
220
|
+
sub.option('db', { type: 'string', description: 'Database name (default: default)' });
|
|
221
|
+
sub.option('dialect', { alias: ['t'], type: 'string', description: 'SQL dialect: mysql|pg (default: mysql)' });
|
|
222
|
+
}, (argv) => {
|
|
223
|
+
const { createTUI } = require('../lib/tui');
|
|
224
|
+
const tui = createTUI({
|
|
225
|
+
dataDir: argv['data-dir'],
|
|
226
|
+
db: argv.db || 'default',
|
|
227
|
+
dialect: argv.dialect || 'mysql',
|
|
228
|
+
});
|
|
229
|
+
tui.run().catch((e) => { console.error(`Error: ${e.message}`); process.exit(1); });
|
|
230
|
+
})
|
|
218
231
|
.command('redis', 'Run the Redis-compatible server', (sub) => {
|
|
219
232
|
sub.option('port', { alias: ['p'], type: 'number', description: 'Listen port (default 6379)' });
|
|
220
233
|
sub.option('host', { type: 'string', description: 'Listen host (default 127.0.0.1)' });
|
package/index.js
CHANGED
|
@@ -22,6 +22,10 @@ const { createMysqlServer, MysqlServer } = require('./lib/mysql_server');
|
|
|
22
22
|
const migrate = require('./lib/migrate');
|
|
23
23
|
const { WebUI } = require('./lib/web_ui');
|
|
24
24
|
const { RedisServer, createRedisServer } = require('./lib/redis_server');
|
|
25
|
+
const { PgServer, createPgServer } = require('./lib/pg_server');
|
|
26
|
+
const { MongoServer, createMongoServer } = require('./lib/mongo_server');
|
|
27
|
+
const { MultiServer, createMultiServer } = require('./lib/multiserver');
|
|
28
|
+
const { TUIShell, createTUI } = require('./lib/tui');
|
|
25
29
|
|
|
26
30
|
/**
|
|
27
31
|
* 全局注入:把项目内 `require('mysql2')` 全部替换为 jsql-neo 内存引擎兼容层。
|
|
@@ -99,4 +103,16 @@ module.exports = {
|
|
|
99
103
|
// Redis 兼容服务器
|
|
100
104
|
RedisServer,
|
|
101
105
|
createRedisServer,
|
|
106
|
+
// PostgreSQL wire protocol 服务器
|
|
107
|
+
PgServer,
|
|
108
|
+
createPgServer,
|
|
109
|
+
// MongoDB wire protocol 服务器
|
|
110
|
+
MongoServer,
|
|
111
|
+
createMongoServer,
|
|
112
|
+
// 多协议嗅探服务器(MySQL + PG + Redis + Mongo 同端口)
|
|
113
|
+
MultiServer,
|
|
114
|
+
createMultiServer,
|
|
115
|
+
// 交互式 TUI
|
|
116
|
+
TUIShell,
|
|
117
|
+
createTUI,
|
|
102
118
|
};
|
package/lib/database.js
CHANGED
|
@@ -1350,7 +1350,8 @@ class Database {
|
|
|
1350
1350
|
for (const id of ids) removed += table.removeById(id);
|
|
1351
1351
|
} else {
|
|
1352
1352
|
for (const id of ids) {
|
|
1353
|
-
const
|
|
1353
|
+
const row = this._resolveId(table, id);
|
|
1354
|
+
const idx = row ? table._rows.findIndex(r => r === row) : -1;
|
|
1354
1355
|
if (idx !== -1) {
|
|
1355
1356
|
table._rows.splice(idx, 1);
|
|
1356
1357
|
removed++;
|
|
@@ -1366,6 +1367,24 @@ class Database {
|
|
|
1366
1367
|
return { ok: true, count: removed };
|
|
1367
1368
|
}
|
|
1368
1369
|
|
|
1370
|
+
removeWhere(tableName, filter) {
|
|
1371
|
+
const table = this._ensureTable(tableName);
|
|
1372
|
+
if (!table) throw createError('ER_NO_SUCH_TABLE', tableName);
|
|
1373
|
+
if (!this._runHooks('beforeDelete', [tableName, [filter]])) return { ok: true, count: 0 };
|
|
1374
|
+
const before = table._rows.length;
|
|
1375
|
+
const matched = table._applyFilter(table._rows, filter || {});
|
|
1376
|
+
const matchSet = new Set(matched);
|
|
1377
|
+
table._rows = table._rows.filter(r => !matchSet.has(r));
|
|
1378
|
+
const removed = before - table._rows.length;
|
|
1379
|
+
table._rebuildPKIndex();
|
|
1380
|
+
table._rebuildAllBTrees();
|
|
1381
|
+
for (const field of Object.keys(table._indexes)) table.createIndex(field);
|
|
1382
|
+
this._emit('delete', { table: tableName, ids: [], result: { ok: true, count: removed } });
|
|
1383
|
+
this._runHooks('afterDelete', [tableName, [filter], { ok: true, count: removed }]);
|
|
1384
|
+
this._markDirty(tableName);
|
|
1385
|
+
return { ok: true, count: removed };
|
|
1386
|
+
}
|
|
1387
|
+
|
|
1369
1388
|
insertMany(tableName, items) {
|
|
1370
1389
|
const table = this._ensureTable(tableName);
|
|
1371
1390
|
if (!table) throw createError('ER_NO_SUCH_TABLE', tableName);
|
|
@@ -0,0 +1,608 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* MongoDB wire protocol (OP_MSG, 3.6+) server for JSQL-NEO.
|
|
3
|
+
*
|
|
4
|
+
* Lets the official `mongodb` npm driver (and mongosh / Compass) connect to
|
|
5
|
+
* jsql-neo as if it were a MongoDB server. Documents are stored as rows of a
|
|
6
|
+
* JSQL table named after the MongoDB collection.
|
|
7
|
+
*
|
|
8
|
+
* Commands supported (minimal but practical):
|
|
9
|
+
* hello / isMaster / ping / buildInfo / getParameter / endSessions
|
|
10
|
+
* listDatabases / listCollections / create / drop
|
|
11
|
+
* insert / find / getMore / update / delete / count / aggregate($match,$count)
|
|
12
|
+
*/
|
|
13
|
+
const net = require('net');
|
|
14
|
+
const path = require('path');
|
|
15
|
+
const zlib = require('zlib');
|
|
16
|
+
const Database = require('./database');
|
|
17
|
+
|
|
18
|
+
const OP_MSG = 2013;
|
|
19
|
+
const OP_COMPRESSED = 2012;
|
|
20
|
+
const OP_QUERY = 2004;
|
|
21
|
+
const OP_REPLY = 1;
|
|
22
|
+
|
|
23
|
+
/* ------------------------------------------------------------------ */
|
|
24
|
+
/* BSON */
|
|
25
|
+
/* ------------------------------------------------------------------ */
|
|
26
|
+
|
|
27
|
+
function appendBytes(buf, b) {
|
|
28
|
+
const out = Buffer.concat([buf, b]);
|
|
29
|
+
return out;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function bsonDocument(obj) {
|
|
33
|
+
// 简易 BSON 文档编码(定长后修正 size)
|
|
34
|
+
let body = Buffer.alloc(4); // size placeholder
|
|
35
|
+
for (const [k, v] of Object.entries(obj || {})) {
|
|
36
|
+
const name = Buffer.from(k, 'utf8');
|
|
37
|
+
if (v === null || v === undefined) {
|
|
38
|
+
body = Buffer.concat([body, Buffer.from([0x0A]), name, Buffer.from([0])]);
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
if (typeof v === 'number') {
|
|
42
|
+
const b = Buffer.alloc(8);
|
|
43
|
+
b.writeDoubleLE(v, 0);
|
|
44
|
+
body = Buffer.concat([body, Buffer.from([0x01]), name, Buffer.from([0]), b]);
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
if (v && v.$long !== undefined) {
|
|
48
|
+
const b = Buffer.alloc(8);
|
|
49
|
+
b.writeBigInt64LE(BigInt(Math.trunc(Number(v.$long))), 0);
|
|
50
|
+
body = Buffer.concat([body, Buffer.from([0x12]), name, Buffer.from([0]), b]);
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
if (typeof v === 'boolean') {
|
|
54
|
+
body = Buffer.concat([body, Buffer.from([0x08]), name, Buffer.from([0]), Buffer.from([v ? 1 : 0])]);
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
if (typeof v === 'string') {
|
|
58
|
+
const str = Buffer.from(v, 'utf8');
|
|
59
|
+
const lenBuf = Buffer.alloc(4);
|
|
60
|
+
lenBuf.writeInt32LE(str.length + 1, 0);
|
|
61
|
+
body = Buffer.concat([body, Buffer.from([0x02]), name, Buffer.from([0]), lenBuf, str, Buffer.from([0])]);
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
if (Buffer.isBuffer(v)) {
|
|
65
|
+
const lenBuf = Buffer.alloc(4);
|
|
66
|
+
lenBuf.writeInt32LE(v.length, 0);
|
|
67
|
+
body = Buffer.concat([body, Buffer.from([0x05]), name, Buffer.from([0]), lenBuf, Buffer.from([0, v.buffer ? 0 : 0]), v]);
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
if (v instanceof Date) {
|
|
71
|
+
const b = Buffer.alloc(8);
|
|
72
|
+
b.writeBigInt64LE(BigInt(v.getTime()), 0);
|
|
73
|
+
body = Buffer.concat([body, Buffer.from([0x09]), name, Buffer.from([0]), b]);
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
if (Array.isArray(v)) {
|
|
77
|
+
const arrObj = {};
|
|
78
|
+
v.forEach((x, i) => { arrObj[String(i)] = x; });
|
|
79
|
+
const sub = bsonDocument(arrObj);
|
|
80
|
+
body = Buffer.concat([body, Buffer.from([0x04]), name, Buffer.from([0]), sub]);
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
if (typeof v === 'object') {
|
|
84
|
+
const sub = bsonDocument(v);
|
|
85
|
+
body = Buffer.concat([body, Buffer.from([0x03]), name, Buffer.from([0]), sub]);
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
// fallback: int32
|
|
89
|
+
const b = Buffer.alloc(4);
|
|
90
|
+
b.writeInt32LE(Number(v) || 0, 0);
|
|
91
|
+
body = Buffer.concat([body, Buffer.from([0x10]), name, Buffer.from([0]), b]);
|
|
92
|
+
}
|
|
93
|
+
body = Buffer.concat([body, Buffer.from([0])]);
|
|
94
|
+
body.writeInt32LE(body.length, 0);
|
|
95
|
+
return body;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function bsonReadDocument(buf, pos) {
|
|
99
|
+
const start = pos;
|
|
100
|
+
const size = buf.readInt32LE(pos); pos += 4;
|
|
101
|
+
const end = start + size;
|
|
102
|
+
const obj = {};
|
|
103
|
+
while (pos < end - 1) {
|
|
104
|
+
const type = buf[pos++];
|
|
105
|
+
// cstring field name
|
|
106
|
+
const nameEnd = buf.indexOf(0, pos);
|
|
107
|
+
const name = buf.toString('utf8', pos, nameEnd);
|
|
108
|
+
pos = nameEnd + 1;
|
|
109
|
+
const r = bsonReadValue(buf, pos, type, end);
|
|
110
|
+
obj[name] = r.value;
|
|
111
|
+
pos = r.pos;
|
|
112
|
+
}
|
|
113
|
+
return { value: obj, pos: end };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function bsonReadValue(buf, pos, type, end) {
|
|
117
|
+
switch (type) {
|
|
118
|
+
case 0x01: { const v = buf.readDoubleLE(pos); return { value: v, pos: pos + 8 }; }
|
|
119
|
+
case 0x02: {
|
|
120
|
+
const len = buf.readInt32LE(pos); pos += 4;
|
|
121
|
+
const v = buf.toString('utf8', pos, pos + len - 1);
|
|
122
|
+
return { value: v, pos: pos + len };
|
|
123
|
+
}
|
|
124
|
+
case 0x03: { const r = bsonReadDocument(buf, pos); return { value: r.value, pos: r.pos }; }
|
|
125
|
+
case 0x04: {
|
|
126
|
+
const r = bsonReadDocument(buf, pos);
|
|
127
|
+
const arr = [];
|
|
128
|
+
for (let i = 0; i < Object.keys(r.value).length; i++) arr.push(r.value[String(i)]);
|
|
129
|
+
return { value: arr, pos: r.pos };
|
|
130
|
+
}
|
|
131
|
+
case 0x05: { const len = buf.readInt32LE(pos); return { value: buf.slice(pos + 5, pos + 5 + len), pos: pos + 5 + len }; }
|
|
132
|
+
case 0x07: { return { value: buf.slice(pos, pos + 12).toString('hex'), pos: pos + 12 }; }
|
|
133
|
+
case 0x08: { return { value: buf[pos] !== 0, pos: pos + 1 }; }
|
|
134
|
+
case 0x09: { return { value: new Date(Number(buf.readBigInt64LE(pos))), pos: pos + 8 }; }
|
|
135
|
+
case 0x0A: { return { value: null, pos }; }
|
|
136
|
+
case 0x10: { return { value: buf.readInt32LE(pos), pos: pos + 4 }; }
|
|
137
|
+
case 0x11: { return { value: Number(buf.readBigInt64LE(pos)), pos: pos + 8 }; }
|
|
138
|
+
case 0x12: { return { value: Number(buf.readBigInt64LE(pos)), pos: pos + 8 }; }
|
|
139
|
+
case 0x13: { return { value: buf.readDoubleLE(pos + 1), pos: pos + 17 }; }
|
|
140
|
+
default: return { value: null, pos: end };
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/* ------------------------------------------------------------------ */
|
|
145
|
+
/* Server */
|
|
146
|
+
/* ------------------------------------------------------------------ */
|
|
147
|
+
|
|
148
|
+
class MongoServer {
|
|
149
|
+
constructor(options = {}) {
|
|
150
|
+
this.options = options;
|
|
151
|
+
this.port = options.port || 27017;
|
|
152
|
+
this.host = options.host || '127.0.0.1';
|
|
153
|
+
this.dataDir = options.dataDir || null;
|
|
154
|
+
this._engine = null;
|
|
155
|
+
this._server = null;
|
|
156
|
+
this._sockets = new Set();
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
async _getEngine() {
|
|
160
|
+
if (this._engine) return this._engine;
|
|
161
|
+
if (this.dataDir && this.dataDir !== ':memory:') {
|
|
162
|
+
this._engine = new Database(path.join(this.dataDir, 'default'), { autoSave: true });
|
|
163
|
+
} else {
|
|
164
|
+
this._engine = new Database(':memory:', { autoSave: false });
|
|
165
|
+
}
|
|
166
|
+
return this._engine;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
async _ensureCollection(name) {
|
|
170
|
+
const engine = await this._getEngine();
|
|
171
|
+
if (!engine.getTableSchema(name)) {
|
|
172
|
+
engine.createTable(name, {});
|
|
173
|
+
}
|
|
174
|
+
return engine;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
listen(cb) {
|
|
178
|
+
this._server = net.createServer((socket) => this._handleSocket(socket));
|
|
179
|
+
this._server.listen(this.port, this.host, cb || (() => {}));
|
|
180
|
+
return this;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
_handleSocket(socket, existing) {
|
|
184
|
+
this._sockets.add(socket);
|
|
185
|
+
socket.on('close', () => this._sockets.delete(socket));
|
|
186
|
+
let buf = Buffer.isBuffer(existing) ? Buffer.from(existing) : Buffer.alloc(0);
|
|
187
|
+
socket.on('error', () => {});
|
|
188
|
+
const process = async () => {
|
|
189
|
+
for (;;) {
|
|
190
|
+
if (buf.length < 4) break;
|
|
191
|
+
const len = buf.readInt32LE(0);
|
|
192
|
+
if (len < 16 || buf.length < len) break;
|
|
193
|
+
const msg = buf.slice(0, len);
|
|
194
|
+
buf = buf.slice(len);
|
|
195
|
+
try {
|
|
196
|
+
await this._handleMessage(socket, msg);
|
|
197
|
+
} catch (e) {
|
|
198
|
+
try { socket.write(this._wrapMsg(bsonDocument({ ok: 0, errmsg: e.message || String(e) }), this._reqId(msg))); } catch (_) {}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
socket.on('data', async (chunk) => {
|
|
203
|
+
buf = Buffer.concat([buf, chunk]);
|
|
204
|
+
await process();
|
|
205
|
+
});
|
|
206
|
+
if (buf.length > 0) process();
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
_reqId(msg) {
|
|
210
|
+
return msg.readInt32LE(4);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
_wrapMsg(payload, responseTo) {
|
|
214
|
+
const id = Math.floor(Math.random() * 0x7fffffff);
|
|
215
|
+
const head = Buffer.alloc(16);
|
|
216
|
+
head.writeInt32LE(16 + 4 + 1 + payload.length, 0); // messageLength
|
|
217
|
+
head.writeInt32LE(id, 4); // requestID
|
|
218
|
+
head.writeInt32LE(responseTo, 8); // responseTo
|
|
219
|
+
head.writeInt32LE(OP_MSG, 12); // opCode
|
|
220
|
+
const flags = Buffer.alloc(4);
|
|
221
|
+
return Buffer.concat([head, flags, Buffer.from([0x00]), payload]); // flagBits + kind0
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
_wrapReply(payload, requestId) {
|
|
225
|
+
// OP_REPLY: header + responseFlags + cursorID + startingFrom + numberReturned + documents
|
|
226
|
+
const n = 16 + 4 + 8 + 4 + 4 + payload.length;
|
|
227
|
+
const head = Buffer.alloc(16);
|
|
228
|
+
head.writeInt32LE(n, 0);
|
|
229
|
+
head.writeInt32LE(requestId + 1000, 4);
|
|
230
|
+
head.writeInt32LE(requestId, 8);
|
|
231
|
+
head.writeInt32LE(OP_REPLY, 12);
|
|
232
|
+
const rest = Buffer.alloc(4 + 8 + 4 + 4); // responseFlags=0, cursorID=0, startingFrom=0, numberReturned=1
|
|
233
|
+
rest.writeInt32LE(1, 4 + 8 + 4);
|
|
234
|
+
return Buffer.concat([head, rest, payload]);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
async _handleMessage(socket, msg) {
|
|
238
|
+
const op = msg.readInt32LE(12);
|
|
239
|
+
if (op === OP_QUERY) {
|
|
240
|
+
let p = 20; // header + flags
|
|
241
|
+
while (p < msg.length && msg[p] !== 0) p++;
|
|
242
|
+
const ns = msg.toString('utf8', 20, p);
|
|
243
|
+
p = p + 1 + 8; // null terminator + numberToSkip + numberToReturn
|
|
244
|
+
if (p >= msg.length || !ns.endsWith('.$cmd')) {
|
|
245
|
+
socket.write(this._wrapReply(bsonDocument({ ok: 0, errmsg: 'legacy op only supports $cmd' }), this._reqId(msg)));
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
try {
|
|
249
|
+
const body = bsonReadDocument(msg, p).value;
|
|
250
|
+
const command = this._commandName(body);
|
|
251
|
+
const reply = await this._dispatch(command, body);
|
|
252
|
+
socket.write(this._wrapReply(bsonDocument(reply), this._reqId(msg)));
|
|
253
|
+
} catch (e) {
|
|
254
|
+
socket.write(this._wrapReply(bsonDocument({ ok: 0, errmsg: e.message || String(e) }), this._reqId(msg)));
|
|
255
|
+
}
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
if (op === OP_COMPRESSED) {
|
|
259
|
+
msg = this._decompress(msg);
|
|
260
|
+
}
|
|
261
|
+
if (!msg) {
|
|
262
|
+
socket.write(this._wrapMsg(bsonDocument({ ok: 0, errmsg: 'compression not supported' }), this._reqId(msg || Buffer.alloc(16))));
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
const op2 = msg.readInt32LE(12);
|
|
266
|
+
if (op2 !== OP_MSG) {
|
|
267
|
+
socket.write(this._wrapMsg(bsonDocument({ ok: 0, errmsg: `opCode ${op2} not supported` }), this._reqId(msg)));
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
let pos = 16 + 4; // header + flagBits
|
|
271
|
+
const kind = msg[pos]; pos++; // section kind: 0 = single BSON doc
|
|
272
|
+
if (kind !== 0) {
|
|
273
|
+
socket.write(this._wrapMsg(bsonDocument({ ok: 0, errmsg: `section kind ${kind} not supported` }), this._reqId(msg)));
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
const body = bsonReadDocument(msg, pos).value;
|
|
277
|
+
const command = this._commandName(body);
|
|
278
|
+
const reply = await this._dispatch(command, body);
|
|
279
|
+
socket.write(this._wrapMsg(bsonDocument(reply), this._reqId(msg)));
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
_commandName(doc) {
|
|
283
|
+
for (const k of Object.keys(doc)) {
|
|
284
|
+
if (k === '$db' || k === 'lsid' || k === '$clusterTime' || k === '$readPreference' || k === 'readConcern' || k === 'writeConcern') continue;
|
|
285
|
+
return k;
|
|
286
|
+
}
|
|
287
|
+
return 'ping';
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
_decompress(msg) {
|
|
291
|
+
try {
|
|
292
|
+
const origOp = msg.readInt32LE(16);
|
|
293
|
+
const uncompSize = msg.readInt32LE(20);
|
|
294
|
+
const compId = msg[24];
|
|
295
|
+
const data = msg.slice(25);
|
|
296
|
+
let out;
|
|
297
|
+
if (compId === 2) out = zlib.inflateSync(data); // zlib
|
|
298
|
+
else if (compId === 3) { /* zstd 无内置支持 */ return null; }
|
|
299
|
+
else if (compId === 1) { /* snappy 无内置支持 */ return null; }
|
|
300
|
+
else return null;
|
|
301
|
+
const head = Buffer.from(msg.slice(0, 16));
|
|
302
|
+
head.writeInt32LE(origOp, 12);
|
|
303
|
+
return Buffer.concat([head, out]);
|
|
304
|
+
} catch (e) {
|
|
305
|
+
return null;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
_dbOf(doc, fallback = 'test') {
|
|
310
|
+
return doc.$db || fallback;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
async _dispatch(cmd, doc) {
|
|
314
|
+
|
|
315
|
+
switch (cmd) {
|
|
316
|
+
case 'hello':
|
|
317
|
+
case 'isMaster':
|
|
318
|
+
case 'ismaster':
|
|
319
|
+
return {
|
|
320
|
+
ok: 1, isWritablePrimary: true, helloOk: true, maxWireVersion: 17, minWireVersion: 0,
|
|
321
|
+
maxBsonObjectSize: 16777216, maxMessageSizeBytes: 48000000, maxWriteBatchSize: 100000,
|
|
322
|
+
localTime: new Date(), logicalSessionTimeoutMinutes: 30, connectionId: 1,
|
|
323
|
+
};
|
|
324
|
+
case 'ping': return { ok: 1 };
|
|
325
|
+
case 'buildInfo': return { ok: 1, version: '5.3.0-jsql-neo', gitVersion: 'jsql-neo', versionArray: [5, 3, 0, 0] };
|
|
326
|
+
case 'getParameter': return { ok: 1 };
|
|
327
|
+
case 'endSessions': return { ok: 1 };
|
|
328
|
+
case 'listDatabases':
|
|
329
|
+
return { ok: 1, databases: [{ name: this._dbOf(doc), sizeOnDisk: 1, empty: false }], totalSize: 1 };
|
|
330
|
+
case 'listCollections': {
|
|
331
|
+
const engine = await this._getEngine();
|
|
332
|
+
const tables = Object.keys(engine._meta && engine._meta.tables ? engine._meta.tables : (engine._tables || {}));
|
|
333
|
+
const firstBatch = tables.map((t) => ({ name: t, type: 'collection', options: {}, info: { readOnly: false, uuid: '' } }));
|
|
334
|
+
return { ok: 1, cursor: { id: { $long: 0 }, ns: `${this._dbOf(doc)}.$cmd.listCollections`, firstBatch } };
|
|
335
|
+
}
|
|
336
|
+
case 'create': {
|
|
337
|
+
const engine = await this._ensureCollection(String(doc.create));
|
|
338
|
+
return { ok: 1, ns: engine ? `${this._dbOf(doc)}.${doc.create}` : '' };
|
|
339
|
+
}
|
|
340
|
+
case 'drop': {
|
|
341
|
+
try {
|
|
342
|
+
const engine = await this._getEngine();
|
|
343
|
+
engine.dropTable(String(doc.drop));
|
|
344
|
+
return { ok: 1, ns: `${this._dbOf(doc)}.${doc.drop}` };
|
|
345
|
+
} catch (e) {
|
|
346
|
+
return { ok: 1, ns: `${this._dbOf(doc)}.${doc.drop}` }; // Mongo 对不存在的 collection drop 也返回 ok
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
case 'insert': {
|
|
350
|
+
const coll = String(doc.insert);
|
|
351
|
+
const engine = await this._ensureCollection(coll);
|
|
352
|
+
const docs = doc.documents || [];
|
|
353
|
+
const ids = [];
|
|
354
|
+
for (const d of docs) {
|
|
355
|
+
const doc2 = { ...d };
|
|
356
|
+
ids.push(engine.insert(coll, doc2));
|
|
357
|
+
}
|
|
358
|
+
return { ok: 1, n: docs.length };
|
|
359
|
+
}
|
|
360
|
+
case 'findAndModify': {
|
|
361
|
+
const coll = String(doc.findAndModify);
|
|
362
|
+
const engine = await this._ensureCollection(coll);
|
|
363
|
+
const q = doc.query || {};
|
|
364
|
+
const rows = this._matching(engine, coll, q);
|
|
365
|
+
const found = rows[0] || null;
|
|
366
|
+
const set = doc.update && doc.update.$set ? doc.update.$set : (doc.update || {});
|
|
367
|
+
let value = null;
|
|
368
|
+
if (found) {
|
|
369
|
+
if (doc.remove) {
|
|
370
|
+
this._removeRows(engine, coll, [found]);
|
|
371
|
+
value = found;
|
|
372
|
+
} else {
|
|
373
|
+
const patched = { ...found, ...set };
|
|
374
|
+
this._patchRows(engine, coll, [found], set);
|
|
375
|
+
value = doc.new ? patched : found;
|
|
376
|
+
}
|
|
377
|
+
} else if (doc.upsert) {
|
|
378
|
+
const merged = { ...q, ...set };
|
|
379
|
+
const id = engine.insert(coll, merged);
|
|
380
|
+
value = doc.new ? { ...merged, _id: id } : null;
|
|
381
|
+
}
|
|
382
|
+
return { ok: 1, value, lastErrorObject: { n: found ? 1 : (doc.upsert ? 1 : 0), updatedExisting: !!found && !doc.remove } };
|
|
383
|
+
}
|
|
384
|
+
case 'distinct': {
|
|
385
|
+
const coll = String(doc.distinct);
|
|
386
|
+
const engine = await this._ensureCollection(coll);
|
|
387
|
+
const key = String(doc.key || '');
|
|
388
|
+
const rows = this._matching(engine, coll, doc.query);
|
|
389
|
+
const seen = new Set();
|
|
390
|
+
const values = [];
|
|
391
|
+
for (const r of rows) {
|
|
392
|
+
const v = r[key];
|
|
393
|
+
if (!seen.has(JSON.stringify(v))) { seen.add(JSON.stringify(v)); values.push(v); }
|
|
394
|
+
}
|
|
395
|
+
return { ok: 1, values };
|
|
396
|
+
}
|
|
397
|
+
case 'dropDatabase':
|
|
398
|
+
return { ok: 1, dropped: String(doc.dropDatabase || this._dbOf(doc)) };
|
|
399
|
+
case 'find': {
|
|
400
|
+
const coll = String(doc.find);
|
|
401
|
+
const engine = await this._ensureCollection(coll);
|
|
402
|
+
let rows = this._matching(engine, coll, doc.filter);
|
|
403
|
+
const skip = typeof doc.skip === 'number' ? doc.skip : 0;
|
|
404
|
+
if (skip > 0) rows = rows.slice(skip);
|
|
405
|
+
const limit = typeof doc.limit === 'number' ? doc.limit : 0;
|
|
406
|
+
if (limit > 0) rows = rows.slice(0, limit);
|
|
407
|
+
return { ok: 1, cursor: { id: { $long: 0 }, ns: `${this._dbOf(doc)}.${coll}`, firstBatch: rows } };
|
|
408
|
+
}
|
|
409
|
+
case 'getMore':
|
|
410
|
+
return { ok: 1, cursor: { id: { $long: 0 }, ns: String(doc.collection || ''), nextBatch: [] } };
|
|
411
|
+
case 'update': {
|
|
412
|
+
const coll = String(doc.update);
|
|
413
|
+
const engine = await this._ensureCollection(coll);
|
|
414
|
+
let n = 0;
|
|
415
|
+
const updates = doc.updates || [];
|
|
416
|
+
for (const u of updates) {
|
|
417
|
+
const q = u.q || {};
|
|
418
|
+
const set = u.u && u.u.$set ? u.u.$set : (u.u || {});
|
|
419
|
+
// 单文档更新
|
|
420
|
+
let targets = this._matching(engine, coll, q);
|
|
421
|
+
let target = targets[0] || null;
|
|
422
|
+
if (!target && u.upsert) {
|
|
423
|
+
const merged = { ...q, ...set };
|
|
424
|
+
engine.insert(coll, merged);
|
|
425
|
+
n++;
|
|
426
|
+
continue;
|
|
427
|
+
}
|
|
428
|
+
if (target) {
|
|
429
|
+
this._patchRows(engine, coll, [target], set);
|
|
430
|
+
n++;
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
return { ok: 1, n, nModified: n };
|
|
434
|
+
}
|
|
435
|
+
case 'delete': {
|
|
436
|
+
const coll = String(doc.delete);
|
|
437
|
+
const engine = await this._ensureCollection(coll);
|
|
438
|
+
let n = 0;
|
|
439
|
+
for (const d of (doc.deletes || [])) {
|
|
440
|
+
const rows = this._matching(engine, coll, d.q || {});
|
|
441
|
+
if (rows.length > 0) this._removeRows(engine, coll, rows);
|
|
442
|
+
n += rows.length;
|
|
443
|
+
}
|
|
444
|
+
return { ok: 1, n };
|
|
445
|
+
}
|
|
446
|
+
case 'count': {
|
|
447
|
+
const coll = String(doc.count);
|
|
448
|
+
const engine = await this._ensureCollection(coll);
|
|
449
|
+
const rows = this._matching(engine, coll, doc.query);
|
|
450
|
+
return { ok: 1, n: Number(doc.limit) > 0 ? Math.min(rows.length, doc.limit) : rows.length };
|
|
451
|
+
}
|
|
452
|
+
case 'aggregate': {
|
|
453
|
+
const coll = String(doc.aggregate);
|
|
454
|
+
const engine = await this._ensureCollection(coll);
|
|
455
|
+
const pipeline = doc.pipeline || [];
|
|
456
|
+
let rows = engine.find(coll, {});
|
|
457
|
+
for (const stage of pipeline) {
|
|
458
|
+
if (stage.$match) rows = rows.filter((r) => this._match(r, stage.$match));
|
|
459
|
+
else if (stage.$count) rows = [{ [stage.$count]: rows.length }];
|
|
460
|
+
else if (stage.$limit) rows = rows.slice(0, stage.$limit);
|
|
461
|
+
else if (stage.$skip) rows = rows.slice(Number(stage.$skip) || 0);
|
|
462
|
+
else if (stage.$sort) {
|
|
463
|
+
const sortKeys = Object.entries(stage.$sort);
|
|
464
|
+
rows = [...rows].sort((a, b) => {
|
|
465
|
+
for (const [k, dir] of sortKeys) {
|
|
466
|
+
const av = a[k]; const bv = b[k];
|
|
467
|
+
if (av === bv) continue;
|
|
468
|
+
if (av == null) return 1;
|
|
469
|
+
if (bv == null) return -1;
|
|
470
|
+
const cmp = av < bv ? -1 : 1;
|
|
471
|
+
return Number(dir) < 0 ? -cmp : cmp;
|
|
472
|
+
}
|
|
473
|
+
return 0;
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
else if (stage.$project) {
|
|
477
|
+
const proj = stage.$project;
|
|
478
|
+
rows = rows.map((r) => {
|
|
479
|
+
const out = {};
|
|
480
|
+
for (const [k, v] of Object.entries(proj)) {
|
|
481
|
+
if (k === '_id' && v === 0) continue;
|
|
482
|
+
if (v === 0) continue;
|
|
483
|
+
if (typeof v === 'string' && v.startsWith('$')) out[k] = r[v.slice(1)];
|
|
484
|
+
else if (v === 1) out[k] = r[k];
|
|
485
|
+
else if (v === 0) { /* exclude */ }
|
|
486
|
+
else out[k] = v;
|
|
487
|
+
}
|
|
488
|
+
return out;
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
else if (stage.$unwind) {
|
|
492
|
+
const field = String(stage.$unwind).replace(/^\$/, '');
|
|
493
|
+
const out = [];
|
|
494
|
+
for (const r of rows) {
|
|
495
|
+
const arr = r[field];
|
|
496
|
+
if (!Array.isArray(arr) || arr.length === 0) { out.push({ ...r, [field]: null }); continue; }
|
|
497
|
+
for (const item of arr) out.push({ ...r, [field]: item });
|
|
498
|
+
}
|
|
499
|
+
rows = out;
|
|
500
|
+
}
|
|
501
|
+
else if (stage.$group) {
|
|
502
|
+
const acc = {};
|
|
503
|
+
for (const [k, v] of Object.entries(stage.$group)) {
|
|
504
|
+
if (v && v.$sum === 1 && k !== '_id') acc[k] = rows.length;
|
|
505
|
+
}
|
|
506
|
+
rows = [{ _id: 1, ...acc }];
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
return { ok: 1, cursor: { id: { $long: 0 }, ns: `${this._dbOf(doc)}.${coll}`, firstBatch: rows } };
|
|
510
|
+
}
|
|
511
|
+
default:
|
|
512
|
+
return { ok: 1, n: 0 };
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
_idOf(row) {
|
|
517
|
+
return row._id != null ? row._id : row.id;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
_matching(engine, coll, filter) {
|
|
521
|
+
return engine.find(coll, {}).filter((r) => this._match(r, filter || {}));
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
_patchRows(engine, coll, rows, set) {
|
|
525
|
+
const table = engine._ensureTable(coll);
|
|
526
|
+
for (const r of rows) Object.assign(r, set);
|
|
527
|
+
table._rebuildPKIndex && table._rebuildPKIndex();
|
|
528
|
+
table._rebuildAllBTrees && table._rebuildAllBTrees();
|
|
529
|
+
engine._markDirty && engine._markDirty(coll);
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
_removeRows(engine, coll, rows) {
|
|
533
|
+
const table = engine._ensureTable(coll);
|
|
534
|
+
const gone = new Set(rows);
|
|
535
|
+
table._rows = table._rows.filter((r) => !gone.has(r));
|
|
536
|
+
table._rebuildPKIndex && table._rebuildPKIndex();
|
|
537
|
+
table._rebuildAllBTrees && table._rebuildAllBTrees();
|
|
538
|
+
engine._markDirty && engine._markDirty(coll);
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
_match(row, filter) {
|
|
542
|
+
for (const [k, cond] of Object.entries(filter || {})) {
|
|
543
|
+
if (k === '$or') {
|
|
544
|
+
if (!cond.some((f) => this._match(row, f))) return false;
|
|
545
|
+
continue;
|
|
546
|
+
}
|
|
547
|
+
if (k === '$and') {
|
|
548
|
+
if (!cond.every((f) => this._match(row, f))) return false;
|
|
549
|
+
continue;
|
|
550
|
+
}
|
|
551
|
+
if (k === '$nor') {
|
|
552
|
+
if (cond.some((f) => this._match(row, f))) return false;
|
|
553
|
+
continue;
|
|
554
|
+
}
|
|
555
|
+
if (k === '$not') {
|
|
556
|
+
if (this._match(row, cond)) return false;
|
|
557
|
+
continue;
|
|
558
|
+
}
|
|
559
|
+
if (cond && typeof cond === 'object' && !Array.isArray(cond)) {
|
|
560
|
+
for (const [op, v] of Object.entries(cond)) {
|
|
561
|
+
const rv = row[k];
|
|
562
|
+
if (op === '$gt' && !(rv > v)) return false;
|
|
563
|
+
if (op === '$gte' && !(rv >= v)) return false;
|
|
564
|
+
if (op === '$lt' && !(rv < v)) return false;
|
|
565
|
+
if (op === '$lte' && !(rv <= v)) return false;
|
|
566
|
+
if (op === '$ne' && !(rv !== v)) return false;
|
|
567
|
+
if (op === '$in' && !(v.includes(rv))) return false;
|
|
568
|
+
if (op === '$nin' && v.includes(rv)) return false;
|
|
569
|
+
if (op === '$exists' && (v ? (rv === undefined) : (rv !== undefined))) return false;
|
|
570
|
+
if (op === '$regex') {
|
|
571
|
+
const flags = String(cond.$options || '').includes('i') ? 'i' : '';
|
|
572
|
+
if (typeof rv !== 'string' || !new RegExp(String(v), flags).test(rv)) return false;
|
|
573
|
+
}
|
|
574
|
+
if (op === '$type') {
|
|
575
|
+
const t = v === 'string' ? 'string' : v === 'int' || v === 'long' || v === 'double' || v === 'number' ? 'number' : v === 'bool' ? 'boolean' : v === 'null' ? 'null' : v === 'array' ? 'array' : typeof rv;
|
|
576
|
+
if (typeof rv !== t) return false;
|
|
577
|
+
}
|
|
578
|
+
if (op === '$size' && !(Array.isArray(rv) && rv.length === v)) return false;
|
|
579
|
+
if (op === '$elemMatch' && !(Array.isArray(rv) && rv.some((el) => this._match(el, v)))) return false;
|
|
580
|
+
}
|
|
581
|
+
} else if (row[k] !== cond) {
|
|
582
|
+
return false;
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
return true;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
get address() {
|
|
589
|
+
return this._server ? this._server.address() : null;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
close(cb) {
|
|
593
|
+
for (const s of this._sockets) s.destroy();
|
|
594
|
+
const done = () => {
|
|
595
|
+
if (this._server) { this._server.close(cb || (() => {})); this._server = null; }
|
|
596
|
+
else if (cb) cb();
|
|
597
|
+
};
|
|
598
|
+
if (this._engine && typeof this._engine.stop === 'function') this._engine.stop().then(done).catch(done);
|
|
599
|
+
else done();
|
|
600
|
+
return this;
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
function createMongoServer(options) {
|
|
605
|
+
return new MongoServer(options || {});
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
module.exports = { MongoServer, createMongoServer, bsonDocument, bsonReadDocument };
|