jsql-neo 6.5.1-beta.1 → 6.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/README.md +22 -11
- package/bin/jsql +58 -13
- package/index.js +1 -1
- package/lib/mongo_server.js +121 -58
- package/lib/multiserver.js +12 -4
- package/lib/star-nag.js +147 -18
- package/native/jsql-neo-native.node +0 -0
- package/package.json +3 -2
- package/test/mongo-group.test.js +5 -16
- package/test/multiprotocol.test.js +155 -0
package/README.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
> **One engine to rule them all** — a Rust-powered embedded database that speaks your language:
|
|
4
4
|
> MySQL, PostgreSQL, MongoDB, Redis, SQL, TypeScript, **and the browser**.
|
|
5
5
|
|
|
6
|
-
> **v6.5.1
|
|
6
|
+
> **v6.5.1** — stable release · [github.com/vexify-org/JSQL-neo](https://github.com/vexify-org/JSQL-neo) · [jsql.vexify.top](https://jsql.vexify.top)
|
|
7
7
|
|
|
8
8
|

|
|
9
9
|

|
|
@@ -38,16 +38,17 @@ npm install jsql-neo
|
|
|
38
38
|
|
|
39
39
|
**JSQL-NEO** is a Rust-powered embedded database with **zero runtime dependencies**. It runs in Node.js, CLI, Web UI, and the browser (WASM).
|
|
40
40
|
|
|
41
|
-
- **
|
|
41
|
+
- **Four wire protocols on one port** — `jsql serve` accepts MySQL, PostgreSQL, MongoDB and Redis clients; `mysql2`, `pg`, `mongodb` and `ioredis` connect directly
|
|
42
42
|
- **Zero dependencies** — no native libs to compile, no system deps to install
|
|
43
43
|
- **Three engines, same API** — `native` (Rust N-API), `wasm` (same Rust core), `js` (pure JS)
|
|
44
44
|
- **~2× faster than better-sqlite3** on indexed point lookups
|
|
45
45
|
|
|
46
|
-
> **
|
|
47
|
-
>
|
|
48
|
-
>
|
|
49
|
-
>
|
|
50
|
-
>
|
|
46
|
+
> **Wire protocols** — MySQL, PostgreSQL, MongoDB and Redis share a single listener
|
|
47
|
+
> (`jsql serve`, default port 3306). The server sniffs the first packet of each
|
|
48
|
+
> connection and hands it to the matching protocol handler. MySQL / PostgreSQL /
|
|
49
|
+
> MongoDB read the same `Database` engine (same tables), Redis keeps its own key
|
|
50
|
+
> namespace. Verified with `mysql2`, `pg`, `mongodb` (driver 7.x) and `ioredis`;
|
|
51
|
+
> pass `--mysql-only` to serve the MySQL protocol alone.
|
|
51
52
|
|
|
52
53
|
---
|
|
53
54
|
|
|
@@ -100,13 +101,23 @@ db.stop();
|
|
|
100
101
|
jsql serve --port 3306 --data-dir ./data
|
|
101
102
|
```
|
|
102
103
|
|
|
104
|
+
All four protocols share the same port:
|
|
105
|
+
|
|
103
106
|
```bash
|
|
104
|
-
# MySQL clients (mysql CLI, mysql2, DBeaver, DataGrip …)
|
|
105
|
-
mysql -h 127.0.0.1 -P 3306
|
|
107
|
+
# MySQL clients (mysql CLI, mysql2, DBeaver, DataGrip …)
|
|
108
|
+
mysql -h 127.0.0.1 -P 3306 -u root
|
|
109
|
+
|
|
110
|
+
# PostgreSQL clients (psql, pg, pgAdmin …)
|
|
111
|
+
psql "host=127.0.0.1 port=3306 user=postgres dbname=app"
|
|
112
|
+
|
|
113
|
+
# MongoDB clients (mongosh, mongodb driver, Compass …)
|
|
114
|
+
mongosh "mongodb://127.0.0.1:3306/app?directConnection=true"
|
|
115
|
+
|
|
116
|
+
# Redis clients (redis-cli, ioredis …)
|
|
117
|
+
redis-cli -h 127.0.0.1 -p 3306 PING
|
|
106
118
|
```
|
|
107
119
|
|
|
108
|
-
|
|
109
|
-
available as libraries — see the note near the top of this file.
|
|
120
|
+
Pass `--mysql-only` to serve the MySQL wire protocol alone.
|
|
110
121
|
|
|
111
122
|
### Browser / WASM
|
|
112
123
|
|
package/bin/jsql
CHANGED
|
@@ -8,7 +8,11 @@ const migrate = require('../lib/migrate');
|
|
|
8
8
|
const { nag, setStarred, readConfig } = require('../lib/star-nag');
|
|
9
9
|
|
|
10
10
|
// star 提示:任何异常都不该挡住命令本身
|
|
11
|
-
|
|
11
|
+
// version / --version 只回版本号、star 本身就是干这件事的 —— 这些不提示,其余全部提示
|
|
12
|
+
const NO_NAG_ARGS = new Set(['version', '--version', '-v', 'star']);
|
|
13
|
+
if (!process.env.JSQL_NO_STAR_NAG && !NO_NAG_ARGS.has(process.argv[2])) {
|
|
14
|
+
try { nag(); } catch (e) { /* 提示失败与命令无关 */ }
|
|
15
|
+
}
|
|
12
16
|
|
|
13
17
|
function printModules(list) {
|
|
14
18
|
if (list.length === 0) {
|
|
@@ -152,14 +156,14 @@ const cli = yaggs({ pkg: require('../package.json') })
|
|
|
152
156
|
}
|
|
153
157
|
} catch (e) { fail(e); }
|
|
154
158
|
})
|
|
155
|
-
.command('serve', 'Run the
|
|
159
|
+
.command('serve', 'Run the multi-protocol server (MySQL / PostgreSQL / MongoDB / Redis on one port)', (sub) => {
|
|
156
160
|
sub.option('port', { alias: ['p'], type: 'number', description: 'Listen port (default 3306)' });
|
|
157
161
|
sub.option('host', { type: 'string', description: 'Listen host (default 127.0.0.1)' });
|
|
158
162
|
sub.option('data-dir', { type: 'string', description: 'Directory to store databases' });
|
|
159
163
|
sub.option('no-auth', { type: 'boolean', description: 'Allow connections without authentication' });
|
|
160
164
|
sub.option('log', { type: 'boolean', description: 'Log queries to stdout' });
|
|
165
|
+
sub.option('mysql-only', { type: 'boolean', description: 'Serve the MySQL wire protocol only' });
|
|
161
166
|
}, async (argv) => {
|
|
162
|
-
const { createMysqlServer } = require('../lib/mysql_server');
|
|
163
167
|
const options = {
|
|
164
168
|
port: argv.port != null ? argv.port : 3306,
|
|
165
169
|
host: argv.host || '127.0.0.1',
|
|
@@ -167,18 +171,59 @@ const cli = yaggs({ pkg: require('../package.json') })
|
|
|
167
171
|
noAuth: argv['no-auth'] === true,
|
|
168
172
|
};
|
|
169
173
|
if (argv.log) options.onQuery = (q) => console.log('[sql]', q);
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
174
|
+
options.onError = (err) => {
|
|
175
|
+
console.error(`serve failed: ${err.message}`);
|
|
176
|
+
process.exit(1);
|
|
177
|
+
};
|
|
178
|
+
const mysqlOnly = argv['mysql-only'] === true;
|
|
179
|
+
if (mysqlOnly) {
|
|
180
|
+
const { createMysqlServer } = require('../lib/mysql_server');
|
|
181
|
+
const srv = createMysqlServer(options);
|
|
182
|
+
srv.listen();
|
|
183
|
+
console.log(`jsql-neo MySQL server on ${options.host}:${options.port}${options.dataDir ? ' (data: ' + options.dataDir + ')' : ' (in-memory)'}`);
|
|
184
|
+
} else {
|
|
185
|
+
const { createMultiServer } = require('../lib/multiserver');
|
|
186
|
+
const srv = createMultiServer(options);
|
|
187
|
+
srv.listen(() => {
|
|
188
|
+
console.log(`jsql-neo multi-protocol server on ${options.host}:${options.port}${options.dataDir ? ' (data: ' + options.dataDir + ')' : ' (in-memory)'}`);
|
|
189
|
+
console.log(' MySQL / PostgreSQL / MongoDB / Redis — one port, shared data');
|
|
190
|
+
});
|
|
191
|
+
const shutdown = () => srv.close(() => process.exit(0));
|
|
192
|
+
process.once('SIGINT', shutdown);
|
|
193
|
+
process.once('SIGTERM', shutdown);
|
|
194
|
+
}
|
|
173
195
|
setInterval(() => {}, 1 << 30);
|
|
174
196
|
})
|
|
175
|
-
.command('star', '
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
197
|
+
.command('star', 'Confirm whether you starred this repo (opens browser once)', (sub) => {
|
|
198
|
+
sub.option('reset', { type: 'boolean', description: 'Forget the saved star state and re-check' });
|
|
199
|
+
}, async (argv) => {
|
|
200
|
+
const sn = require('../lib/star-nag');
|
|
201
|
+
if (argv.reset) sn.setStarred(false);
|
|
202
|
+
const c = sn.readConfig();
|
|
203
|
+
if (c.starred) {
|
|
204
|
+
console.log('Already confirmed starred (config: ' + sn.configPath() + ')');
|
|
205
|
+
console.log('Run with --reset to re-check.');
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
console.log('Opening browser to confirm your star…');
|
|
209
|
+
const nonceUrl = sn.startOauthCheck(); // 打开浏览器 + 后台轮询写回 ~/.jsqlmr
|
|
210
|
+
if (!process.stdout.isTTY) console.log(' ' + nonceUrl);
|
|
211
|
+
console.log('After you authorize, this config is updated: ' + sn.configPath());
|
|
212
|
+
console.log('Waiting for authorization (up to 3 minutes)…');
|
|
213
|
+
// startOauthCheck 的定时器是 unref 的,命令必须自己前台等,否则接不到结果
|
|
214
|
+
const res = sn.waitForResult(nonceUrl, 180000);
|
|
215
|
+
if (res && res.starred) {
|
|
216
|
+
sn.setStarred(true);
|
|
217
|
+
console.log(`Confirmed: ${res.login} has starred ${res.repo}. Thanks!`);
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
if (res) {
|
|
221
|
+
console.log(`Authorization finished, but ${res.login} has not starred ${res.repo} yet.`);
|
|
222
|
+
console.log(`Star it at https://github.com/${res.repo} then run \`jsql star\` again.`);
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
console.log('Timed out or the link expired. Run `jsql star` again to retry.');
|
|
226
|
+
})
|
|
182
227
|
.command('bench', 'Insert + query benchmark against a data directory', (sub) => {
|
|
183
228
|
sub.option('n', { type: 'number', description: 'Number of rows (default 10000)' });
|
|
184
229
|
sub.option('data-dir', { type: 'string', description: 'Data directory (default :memory:)' });
|
package/index.js
CHANGED
package/lib/mongo_server.js
CHANGED
|
@@ -155,20 +155,28 @@ class MongoServer {
|
|
|
155
155
|
this._server = null;
|
|
156
156
|
this._sockets = new Set();
|
|
157
157
|
this._dbCollections = new Map(); // db name → Set of collection names
|
|
158
|
+
this._engineByDb = new Map(); // db name → Database (standalone 模式)
|
|
158
159
|
}
|
|
159
160
|
|
|
160
|
-
async _getEngine() {
|
|
161
|
+
async _getEngine(db = null) {
|
|
162
|
+
if (this._engineByDb && db) {
|
|
163
|
+
if (!this._engineByDb.has(db)) this._engineByDb.set(db, this._createEngine(db));
|
|
164
|
+
return this._engineByDb.get(db);
|
|
165
|
+
}
|
|
161
166
|
if (this._engine) return this._engine;
|
|
167
|
+
this._engine = this._createEngine(null);
|
|
168
|
+
return this._engine;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
_createEngine(db) {
|
|
162
172
|
if (this.dataDir && this.dataDir !== ':memory:') {
|
|
163
|
-
|
|
164
|
-
} else {
|
|
165
|
-
this._engine = new Database(':memory:', { autoSave: false });
|
|
173
|
+
return new Database(path.join(this.dataDir, db || 'default'), { autoSave: true });
|
|
166
174
|
}
|
|
167
|
-
return
|
|
175
|
+
return new Database(':memory:', { autoSave: false });
|
|
168
176
|
}
|
|
169
177
|
|
|
170
178
|
async _ensureCollection(name, db = null) {
|
|
171
|
-
const engine = await this._getEngine();
|
|
179
|
+
const engine = await this._getEngine(db);
|
|
172
180
|
if (!engine.getTableSchema(name)) {
|
|
173
181
|
engine.createTable(name, {});
|
|
174
182
|
}
|
|
@@ -190,25 +198,35 @@ class MongoServer {
|
|
|
190
198
|
socket.on('close', () => this._sockets.delete(socket));
|
|
191
199
|
let buf = Buffer.isBuffer(existing) ? Buffer.from(existing) : Buffer.alloc(0);
|
|
192
200
|
socket.on('error', () => {});
|
|
201
|
+
let processing = false;
|
|
202
|
+
let again = false;
|
|
193
203
|
const processLoop = async () => {
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
+
if (processing) { again = true; return; }
|
|
205
|
+
processing = true;
|
|
206
|
+
try {
|
|
207
|
+
for (;;) {
|
|
208
|
+
if (buf.length < 4) break;
|
|
209
|
+
const len = buf.readInt32LE(0);
|
|
210
|
+
if (len < 16 || len > 48 * 1024 * 1024) break;
|
|
211
|
+
if (buf.length < len) break;
|
|
212
|
+
const msg = buf.slice(0, len);
|
|
213
|
+
buf = buf.slice(len);
|
|
214
|
+
try {
|
|
215
|
+
await this._handleMessage(socket, msg);
|
|
216
|
+
} catch (e) {
|
|
217
|
+
try { socket.write(this._wrapMsg(bsonDocument({ ok: 0, errmsg: e.message || String(e) }), this._reqId(msg))); } catch (_) {}
|
|
218
|
+
}
|
|
204
219
|
}
|
|
220
|
+
} finally {
|
|
221
|
+
processing = false;
|
|
222
|
+
if (again) { again = false; processLoop(); }
|
|
205
223
|
}
|
|
206
224
|
};
|
|
207
|
-
socket.on('data',
|
|
225
|
+
socket.on('data', (chunk) => {
|
|
208
226
|
buf = Buffer.concat([buf, chunk]);
|
|
209
|
-
|
|
227
|
+
processLoop();
|
|
210
228
|
});
|
|
211
|
-
if (buf.length > 0)
|
|
229
|
+
if (buf.length > 0) processLoop();
|
|
212
230
|
}
|
|
213
231
|
|
|
214
232
|
_reqId(msg) {
|
|
@@ -272,13 +290,45 @@ class MongoServer {
|
|
|
272
290
|
socket.write(this._wrapMsg(bsonDocument({ ok: 0, errmsg: `opCode ${op2} not supported` }), this._reqId(msg)));
|
|
273
291
|
return;
|
|
274
292
|
}
|
|
293
|
+
const flags = msg.readInt32LE(16);
|
|
294
|
+
const msgEnd = (flags & 1) ? msg.length - 4 : msg.length; // checksumPresent
|
|
275
295
|
let pos = 16 + 4; // header + flagBits
|
|
276
|
-
const
|
|
277
|
-
|
|
278
|
-
|
|
296
|
+
const body = {};
|
|
297
|
+
let sawBody = false;
|
|
298
|
+
while (pos < msgEnd) {
|
|
299
|
+
const kind = msg[pos++];
|
|
300
|
+
if (kind === 0) {
|
|
301
|
+
const r = bsonReadDocument(msg, pos);
|
|
302
|
+
Object.assign(body, r.value);
|
|
303
|
+
pos = r.pos;
|
|
304
|
+
sawBody = true;
|
|
305
|
+
} else if (kind === 1) {
|
|
306
|
+
// Document sequence: int32 size, cstring identifier, BSON docs
|
|
307
|
+
const size = msg.readInt32LE(pos);
|
|
308
|
+
const end = pos + size;
|
|
309
|
+
if (size < 5 || end > msgEnd) throw new Error('malformed document sequence');
|
|
310
|
+
let p = pos + 4;
|
|
311
|
+
const nameEnd = msg.indexOf(0, p);
|
|
312
|
+
if (nameEnd < 0 || nameEnd >= end) throw new Error('malformed document sequence identifier');
|
|
313
|
+
const ident = msg.toString('utf8', p, nameEnd);
|
|
314
|
+
p = nameEnd + 1;
|
|
315
|
+
const seq = [];
|
|
316
|
+
while (p < end) {
|
|
317
|
+
const r = bsonReadDocument(msg, p);
|
|
318
|
+
seq.push(r.value);
|
|
319
|
+
p = r.pos;
|
|
320
|
+
}
|
|
321
|
+
body[ident] = seq;
|
|
322
|
+
pos = end;
|
|
323
|
+
} else {
|
|
324
|
+
socket.write(this._wrapMsg(bsonDocument({ ok: 0, errmsg: `section kind ${kind} not supported` }), this._reqId(msg)));
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
if (!sawBody) {
|
|
329
|
+
socket.write(this._wrapMsg(bsonDocument({ ok: 0, errmsg: 'missing OP_MSG body section' }), this._reqId(msg)));
|
|
279
330
|
return;
|
|
280
331
|
}
|
|
281
|
-
const body = bsonReadDocument(msg, pos).value;
|
|
282
332
|
const command = this._commandName(body);
|
|
283
333
|
const reply = await this._dispatch(command, body);
|
|
284
334
|
socket.write(this._wrapMsg(bsonDocument(reply), this._reqId(msg)));
|
|
@@ -332,13 +382,13 @@ class MongoServer {
|
|
|
332
382
|
case 'endSessions': return { ok: 1 };
|
|
333
383
|
case 'listDatabases': {
|
|
334
384
|
// P3-17: listDatabases 只返回当前库——返回所有数据库
|
|
335
|
-
const engine = await this._getEngine();
|
|
385
|
+
const engine = await this._getEngine(this._dbOf(doc));
|
|
336
386
|
const dbs = Object.keys(engine._meta && engine._meta.databases ? engine._meta.databases : { [this._dbOf(doc)]: 1 });
|
|
337
387
|
const databases = Object.keys(dbs).map(name => ({ name, sizeOnDisk: 1, empty: false }));
|
|
338
388
|
return { ok: 1, databases, totalSize: databases.length };
|
|
339
389
|
}
|
|
340
390
|
case 'listCollections': {
|
|
341
|
-
const engine = await this._getEngine();
|
|
391
|
+
const engine = await this._getEngine(this._dbOf(doc));
|
|
342
392
|
const tables = Object.keys(engine._meta && engine._meta.tables ? engine._meta.tables : (engine._tables || {}));
|
|
343
393
|
const firstBatch = tables.map((t) => ({ name: t, type: 'collection', options: {}, info: { readOnly: false, uuid: '' } }));
|
|
344
394
|
return { ok: 1, cursor: { id: { $long: 0 }, ns: `${this._dbOf(doc)}.$cmd.listCollections`, firstBatch } };
|
|
@@ -349,7 +399,7 @@ class MongoServer {
|
|
|
349
399
|
}
|
|
350
400
|
case 'drop': {
|
|
351
401
|
try {
|
|
352
|
-
const engine = await this._getEngine();
|
|
402
|
+
const engine = await this._getEngine(this._dbOf(doc));
|
|
353
403
|
engine.dropTable(String(doc.drop));
|
|
354
404
|
return { ok: 1, ns: `${this._dbOf(doc)}.${doc.drop}` };
|
|
355
405
|
} catch (e) {
|
|
@@ -365,10 +415,11 @@ class MongoServer {
|
|
|
365
415
|
const doc2 = { ...d };
|
|
366
416
|
// 检查 _id 唯一性
|
|
367
417
|
if (doc2._id !== undefined) {
|
|
368
|
-
const existing = engine.find(coll, { _id: doc2._id });
|
|
418
|
+
const existing = await engine.find(coll, { _id: doc2._id });
|
|
369
419
|
if (existing.length > 0) continue; // 跳过重复 _id
|
|
370
420
|
}
|
|
371
|
-
|
|
421
|
+
await engine.insert(coll, doc2);
|
|
422
|
+
ids.push(doc2._id);
|
|
372
423
|
}
|
|
373
424
|
return { ok: 1, n: ids.length };
|
|
374
425
|
}
|
|
@@ -376,23 +427,23 @@ class MongoServer {
|
|
|
376
427
|
const coll = String(doc.findAndModify);
|
|
377
428
|
const engine = await this._ensureCollection(coll, this._dbOf(doc));
|
|
378
429
|
const q = doc.query || {};
|
|
379
|
-
const rows = this._matching(engine, coll, q);
|
|
430
|
+
const rows = await this._matching(engine, coll, q);
|
|
380
431
|
const found = rows[0] || null;
|
|
381
432
|
const set = doc.update && doc.update.$set ? doc.update.$set : (doc.update || {});
|
|
382
433
|
let value = null;
|
|
383
434
|
if (found) {
|
|
384
435
|
if (doc.remove) {
|
|
385
|
-
this._removeRows(engine, coll, [found]);
|
|
436
|
+
await this._removeRows(engine, coll, [found]);
|
|
386
437
|
value = found;
|
|
387
438
|
} else {
|
|
388
439
|
const patched = { ...found, ...set };
|
|
389
|
-
this._patchRows(engine, coll, [found], set);
|
|
440
|
+
await this._patchRows(engine, coll, [found], set);
|
|
390
441
|
value = doc.new ? patched : found;
|
|
391
442
|
}
|
|
392
443
|
} else if (doc.upsert) {
|
|
393
444
|
const merged = { ...q, ...set };
|
|
394
|
-
|
|
395
|
-
value = doc.new ? { ...merged
|
|
445
|
+
await engine.insert(coll, merged);
|
|
446
|
+
value = doc.new ? { ...merged } : null;
|
|
396
447
|
}
|
|
397
448
|
return { ok: 1, value, lastErrorObject: { n: found ? 1 : (doc.upsert ? 1 : 0), updatedExisting: !!found && !doc.remove } };
|
|
398
449
|
}
|
|
@@ -400,7 +451,7 @@ class MongoServer {
|
|
|
400
451
|
const coll = String(doc.distinct);
|
|
401
452
|
const engine = await this._ensureCollection(coll, this._dbOf(doc));
|
|
402
453
|
const key = String(doc.key || '');
|
|
403
|
-
const rows = this._matching(engine, coll, doc.query);
|
|
454
|
+
const rows = await this._matching(engine, coll, doc.query);
|
|
404
455
|
const seen = new Set();
|
|
405
456
|
const values = [];
|
|
406
457
|
for (const r of rows) {
|
|
@@ -411,7 +462,7 @@ class MongoServer {
|
|
|
411
462
|
}
|
|
412
463
|
case 'dropDatabase': {
|
|
413
464
|
const dbName = String(doc.$db || 'test');
|
|
414
|
-
const engine = await this._getEngine();
|
|
465
|
+
const engine = await this._getEngine(dbName);
|
|
415
466
|
const colls = this._dbCollections.get(dbName);
|
|
416
467
|
if (colls) {
|
|
417
468
|
for (const c of colls) {
|
|
@@ -424,7 +475,7 @@ class MongoServer {
|
|
|
424
475
|
case 'find': {
|
|
425
476
|
const coll = String(doc.find);
|
|
426
477
|
const engine = await this._ensureCollection(coll, this._dbOf(doc));
|
|
427
|
-
let rows = this._matching(engine, coll, doc.filter);
|
|
478
|
+
let rows = await this._matching(engine, coll, doc.filter);
|
|
428
479
|
const skip = typeof doc.skip === 'number' ? doc.skip : 0;
|
|
429
480
|
if (skip > 0) rows = rows.slice(skip);
|
|
430
481
|
const limit = typeof doc.limit === 'number' ? doc.limit : 0;
|
|
@@ -442,24 +493,24 @@ class MongoServer {
|
|
|
442
493
|
const q = u.q || {};
|
|
443
494
|
const set = u.u && u.u.$set ? u.u.$set : (u.u || {});
|
|
444
495
|
const multi = u.multi === true;
|
|
445
|
-
let targets = this._matching(engine, coll, q);
|
|
496
|
+
let targets = await this._matching(engine, coll, q);
|
|
446
497
|
if (!targets.length && u.upsert) {
|
|
447
498
|
const merged = { ...q, ...set };
|
|
448
|
-
engine.insert(coll, merged);
|
|
499
|
+
await engine.insert(coll, merged);
|
|
449
500
|
n++;
|
|
450
501
|
continue;
|
|
451
502
|
}
|
|
452
503
|
if (multi) {
|
|
453
504
|
// 更新所有匹配的行
|
|
454
505
|
for (const target of targets) {
|
|
455
|
-
this._patchRows(engine, coll, [target], u.u || { $set: set });
|
|
506
|
+
await this._patchRows(engine, coll, [target], u.u || { $set: set });
|
|
456
507
|
n++;
|
|
457
508
|
}
|
|
458
509
|
} else {
|
|
459
510
|
// 只更新第一条
|
|
460
511
|
let target = targets[0] || null;
|
|
461
512
|
if (target) {
|
|
462
|
-
this._patchRows(engine, coll, [target], u.u || { $set: set });
|
|
513
|
+
await this._patchRows(engine, coll, [target], u.u || { $set: set });
|
|
463
514
|
n++;
|
|
464
515
|
}
|
|
465
516
|
}
|
|
@@ -471,10 +522,10 @@ class MongoServer {
|
|
|
471
522
|
const engine = await this._ensureCollection(coll, this._dbOf(doc));
|
|
472
523
|
let n = 0;
|
|
473
524
|
for (const d of (doc.deletes || [])) {
|
|
474
|
-
const rows = this._matching(engine, coll, d.q || {});
|
|
525
|
+
const rows = await this._matching(engine, coll, d.q || {});
|
|
475
526
|
// MongoDB delete: limit=1 means delete only one, limit=0 means delete all
|
|
476
527
|
const toDelete = d.limit === 1 ? rows.slice(0, 1) : rows;
|
|
477
|
-
if (toDelete.length > 0) this._removeRows(engine, coll, toDelete);
|
|
528
|
+
if (toDelete.length > 0) await this._removeRows(engine, coll, toDelete);
|
|
478
529
|
n += toDelete.length;
|
|
479
530
|
}
|
|
480
531
|
return { ok: 1, n };
|
|
@@ -482,14 +533,14 @@ class MongoServer {
|
|
|
482
533
|
case 'count': {
|
|
483
534
|
const coll = String(doc.count);
|
|
484
535
|
const engine = await this._ensureCollection(coll, this._dbOf(doc));
|
|
485
|
-
const rows = this._matching(engine, coll, doc.query);
|
|
536
|
+
const rows = await this._matching(engine, coll, doc.query);
|
|
486
537
|
return { ok: 1, n: Number(doc.limit) > 0 ? Math.min(rows.length, doc.limit) : rows.length };
|
|
487
538
|
}
|
|
488
539
|
case 'aggregate': {
|
|
489
540
|
const coll = String(doc.aggregate);
|
|
490
541
|
const engine = await this._ensureCollection(coll, this._dbOf(doc));
|
|
491
542
|
const pipeline = doc.pipeline || [];
|
|
492
|
-
let rows = engine.find(coll, {});
|
|
543
|
+
let rows = await engine.find(coll, {});
|
|
493
544
|
for (const stage of pipeline) {
|
|
494
545
|
if (stage.$match) rows = rows.filter((r) => this._match(r, stage.$match));
|
|
495
546
|
else if (stage.$count) rows = [{ [stage.$count]: rows.length }];
|
|
@@ -613,11 +664,19 @@ class MongoServer {
|
|
|
613
664
|
return row._id != null ? row._id : row.id;
|
|
614
665
|
}
|
|
615
666
|
|
|
616
|
-
_matching(engine, coll, filter) {
|
|
617
|
-
|
|
667
|
+
async _matching(engine, coll, filter) {
|
|
668
|
+
const rows = await engine.find(coll, {});
|
|
669
|
+
return rows.filter((r) => this._match(r, filter || {}));
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
_rowKey(engine, coll, row) {
|
|
673
|
+
const table = engine._tables && engine._tables[coll];
|
|
674
|
+
const pk = table && (table._primaryKey || table._autoIncrementField);
|
|
675
|
+
// 有主键用主键值;Mongo 自建集合没有主键,传行对象走深度匹配
|
|
676
|
+
return pk ? row[pk] : { ...row };
|
|
618
677
|
}
|
|
619
678
|
|
|
620
|
-
_patchRows(engine, coll, rows, updateDoc) {
|
|
679
|
+
async _patchRows(engine, coll, rows, updateDoc) {
|
|
621
680
|
const entries = [];
|
|
622
681
|
// 检查是否有更新操作符
|
|
623
682
|
const operators = ['$inc', '$unset', '$push', '$set'];
|
|
@@ -626,14 +685,14 @@ class MongoServer {
|
|
|
626
685
|
if (!hasOperators) {
|
|
627
686
|
// 没有操作符,直接 set
|
|
628
687
|
for (const r of rows) {
|
|
629
|
-
const
|
|
630
|
-
if (
|
|
688
|
+
const key = this._rowKey(engine, coll, r);
|
|
689
|
+
if (key !== undefined) entries.push([key, { ...updateDoc }]);
|
|
631
690
|
}
|
|
632
691
|
} else {
|
|
633
692
|
// 有操作符,需要特殊处理
|
|
634
693
|
for (const r of rows) {
|
|
635
|
-
const
|
|
636
|
-
if (
|
|
694
|
+
const key = this._rowKey(engine, coll, r);
|
|
695
|
+
if (key === undefined) continue;
|
|
637
696
|
const update = {};
|
|
638
697
|
if (updateDoc.$set) Object.assign(update, updateDoc.$set);
|
|
639
698
|
if (updateDoc.$inc) {
|
|
@@ -654,21 +713,21 @@ class MongoServer {
|
|
|
654
713
|
update[field] = null;
|
|
655
714
|
}
|
|
656
715
|
}
|
|
657
|
-
if (Object.keys(update).length > 0) entries.push([
|
|
716
|
+
if (Object.keys(update).length > 0) entries.push([key, update]);
|
|
658
717
|
}
|
|
659
718
|
}
|
|
660
|
-
if (entries.length > 0) engine.updateByIds(coll, entries);
|
|
719
|
+
if (entries.length > 0) await engine.updateByIds(coll, entries);
|
|
661
720
|
}
|
|
662
721
|
|
|
663
|
-
_removeRows(engine, coll, rows) {
|
|
722
|
+
async _removeRows(engine, coll, rows) {
|
|
664
723
|
// 收集所有需要删除的行的 id,走 engine.removeByIds
|
|
665
724
|
// 这样会写 WAL、触发 hooks、检查约束
|
|
666
725
|
const ids = [];
|
|
667
726
|
for (const r of rows) {
|
|
668
|
-
const
|
|
669
|
-
if (
|
|
727
|
+
const key = this._rowKey(engine, coll, r);
|
|
728
|
+
if (key !== undefined) ids.push(key);
|
|
670
729
|
}
|
|
671
|
-
if (ids.length > 0) engine.removeByIds(coll, ids);
|
|
730
|
+
if (ids.length > 0) await engine.removeByIds(coll, ids);
|
|
672
731
|
}
|
|
673
732
|
|
|
674
733
|
_match(row, filter) {
|
|
@@ -729,7 +788,11 @@ class MongoServer {
|
|
|
729
788
|
if (this._server) { this._server.close(cb || (() => {})); this._server = null; }
|
|
730
789
|
else if (cb) cb();
|
|
731
790
|
};
|
|
732
|
-
|
|
791
|
+
const engines = new Set(this._engineByDb ? this._engineByDb.values() : []);
|
|
792
|
+
if (this._engine) engines.add(this._engine);
|
|
793
|
+
const stops = [];
|
|
794
|
+
for (const e of engines) if (e && typeof e.stop === 'function') stops.push(e.stop());
|
|
795
|
+
if (stops.length > 0) Promise.allSettled(stops).then(done).catch(done);
|
|
733
796
|
else done();
|
|
734
797
|
return this;
|
|
735
798
|
}
|
package/lib/multiserver.js
CHANGED
|
@@ -42,6 +42,10 @@ class MultiServer {
|
|
|
42
42
|
this.port = options.port ?? 5432;
|
|
43
43
|
this.host = options.host || '127.0.0.1';
|
|
44
44
|
this.dataDir = options.dataDir || null;
|
|
45
|
+
// 静默连接的兜底协议:MySQL 客户端等待服务器先发握手包,
|
|
46
|
+
// 而空闲的 Redis 连接也不主动发包,只能按配置的二选一处理。
|
|
47
|
+
this.idleProtocol = options.idleProtocol || 'mysql';
|
|
48
|
+
this.sniffTimeout = options.sniffTimeout != null ? options.sniffTimeout : 200;
|
|
45
49
|
// 共享 SQL 引擎:三种 SQL 协议读写同一份数据
|
|
46
50
|
this._engines = new Map();
|
|
47
51
|
const getEngine = async (dbName) => {
|
|
@@ -72,13 +76,17 @@ class MultiServer {
|
|
|
72
76
|
listen(cb) {
|
|
73
77
|
this._server = net.createServer((socket) => {
|
|
74
78
|
this._sockets.add(socket);
|
|
79
|
+
socket.on('error', () => {});
|
|
75
80
|
socket.on('close', () => this._sockets.delete(socket));
|
|
76
81
|
let first = Buffer.alloc(0);
|
|
77
82
|
let routed = false;
|
|
78
|
-
// MySQL 客户端会先等服务器握手包,不主动发字节;超时后按
|
|
83
|
+
// MySQL 客户端会先等服务器握手包,不主动发字节;超时后按 idleProtocol 处理
|
|
79
84
|
const t = setTimeout(() => {
|
|
80
|
-
if (
|
|
81
|
-
|
|
85
|
+
if (routed) return;
|
|
86
|
+
routed = true;
|
|
87
|
+
socket.removeListener('data', sniff);
|
|
88
|
+
try { this._route(socket, first); } catch (e) { socket.destroy(); }
|
|
89
|
+
}, this.sniffTimeout);
|
|
82
90
|
t.unref();
|
|
83
91
|
const sniff = (chunk) => {
|
|
84
92
|
first = Buffer.concat([first, chunk]);
|
|
@@ -104,7 +112,7 @@ class MultiServer {
|
|
|
104
112
|
}
|
|
105
113
|
|
|
106
114
|
_route(socket, first) {
|
|
107
|
-
const proto = sniffProtocol(first) ||
|
|
115
|
+
const proto = sniffProtocol(first) || this.idleProtocol;
|
|
108
116
|
if (proto === 'mysql') {
|
|
109
117
|
const conn = new MysqlConnection(socket, this.mysql);
|
|
110
118
|
conn._onData(first);
|
package/lib/star-nag.js
CHANGED
|
@@ -24,6 +24,57 @@ const REPO = 'vexify-org/JSQL-neo';
|
|
|
24
24
|
const URL = `https://github.com/${REPO}`;
|
|
25
25
|
const NAG_INTERVAL_DAYS = 30;
|
|
26
26
|
|
|
27
|
+
const AUTH_BASE = process.env.JSQL_AUTH_BASE || 'https://auth.vexify.top';
|
|
28
|
+
const AUTH_START = AUTH_BASE + '/start.php';
|
|
29
|
+
const AUTH_RESULT = AUTH_BASE + '/result.php';
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* 发起 OAuth 浏览器流并在后台轮询结果。
|
|
33
|
+
*
|
|
34
|
+
* 绝不阻塞命令:开完浏览器就返回,轮询用 unref 过的定时器跑。
|
|
35
|
+
* 拿到结果写进 ~/.jsqlmr(starred: true/false)。
|
|
36
|
+
* 进程提前退出就放弃这一次,下次启动会再提示(30 天节流管频率)。
|
|
37
|
+
*/
|
|
38
|
+
function startOauthCheck() {
|
|
39
|
+
const crypto = require('crypto');
|
|
40
|
+
const nonce = crypto.randomBytes(24).toString('base64url');
|
|
41
|
+
const url = AUTH_START + '?nonce=' + nonce;
|
|
42
|
+
|
|
43
|
+
// 开浏览器:三平台各一个命令;失败就只打印链接让人手动点
|
|
44
|
+
const cmd = process.platform === 'win32' ? 'start'
|
|
45
|
+
: process.platform === 'darwin' ? 'open' : 'xdg-open';
|
|
46
|
+
try {
|
|
47
|
+
require('child_process').spawn(cmd, [url], {
|
|
48
|
+
detached: true, stdio: 'ignore', shell: process.platform === 'win32',
|
|
49
|
+
}).unref();
|
|
50
|
+
} catch (e) {
|
|
51
|
+
console.log(' Open this to confirm your star: ' + url);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// 后台轮询,最多约 90 秒
|
|
55
|
+
let tries = 0;
|
|
56
|
+
const t = setInterval(() => {
|
|
57
|
+
if (++tries > 45) { clearInterval(t); return; }
|
|
58
|
+
const { execFile } = require('child_process');
|
|
59
|
+
execFile('curl', ['-sS', '-m', '5', AUTH_RESULT + '?nonce=' + nonce],
|
|
60
|
+
{ timeout: 6000, encoding: 'utf8' }, (err, stdout) => {
|
|
61
|
+
if (err) return;
|
|
62
|
+
let j = null;
|
|
63
|
+
try { j = JSON.parse(stdout); } catch (e) { return; }
|
|
64
|
+
if (!j || j.state !== 'done' || !j.result) return;
|
|
65
|
+
clearInterval(t);
|
|
66
|
+
const cfg = readConfig();
|
|
67
|
+
cfg.oauthCheckedAt = Date.now();
|
|
68
|
+
cfg.login = j.result.login || undefined;
|
|
69
|
+
cfg.starred = !!j.result.starred;
|
|
70
|
+
if (cfg.starred) cfg.starredAt = Date.now();
|
|
71
|
+
writeConfig(cfg);
|
|
72
|
+
});
|
|
73
|
+
}, 2000);
|
|
74
|
+
t.unref();
|
|
75
|
+
return url;
|
|
76
|
+
}
|
|
77
|
+
|
|
27
78
|
function configPath() {
|
|
28
79
|
return process.env.JSQL_MR_PATH || path.join(os.homedir(), '.jsqlmr');
|
|
29
80
|
}
|
|
@@ -47,21 +98,57 @@ function writeConfig(cfg) {
|
|
|
47
98
|
}
|
|
48
99
|
|
|
49
100
|
/** 同步问一下 GitHub:这个身份 star 过没有。查不到就返回 null(三态)。 */
|
|
101
|
+
/**
|
|
102
|
+
* 找一个可用的 GitHub 凭据。
|
|
103
|
+
* 1. GITHUB_TOKEN / GH_TOKEN 环境变量
|
|
104
|
+
* 2. 官方 gh CLI(`gh auth token`)—— 多数开发机上它已经登录过了,
|
|
105
|
+
* 不该要求用户为了看一句提示语再去配环境变量
|
|
106
|
+
* 3. 都没有 → null
|
|
107
|
+
*/
|
|
108
|
+
function findToken() {
|
|
109
|
+
const env = process.env.GITHUB_TOKEN || process.env.GH_TOKEN;
|
|
110
|
+
if (env) return env;
|
|
111
|
+
try {
|
|
112
|
+
const { execFileSync } = require('child_process');
|
|
113
|
+
const t = execFileSync('gh', ['auth', 'token'], {
|
|
114
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
115
|
+
timeout: 2500,
|
|
116
|
+
encoding: 'utf8',
|
|
117
|
+
}).trim();
|
|
118
|
+
if (t) return t;
|
|
119
|
+
} catch (e) {
|
|
120
|
+
// 没装 gh / 没登录 / 太慢 —— 都当作没有
|
|
121
|
+
}
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* 尽力而为地问一次「这个身份 star 过没有」。查不到返回 null(三态)。
|
|
127
|
+
*
|
|
128
|
+
* 只在「真的准备显示提示语时」才发这个网络请求;已标记 starred 或被
|
|
129
|
+
* 30 天节流挡掉时一次请求都不会发。
|
|
130
|
+
*
|
|
131
|
+
* @returns {boolean|null} true=star 过 false=没 star null=无法确认
|
|
132
|
+
*/
|
|
50
133
|
function checkStarred() {
|
|
51
|
-
const token =
|
|
52
|
-
if (!token) return null; //
|
|
134
|
+
const token = findToken();
|
|
135
|
+
if (!token) return null; // 没有可用凭据 → 无法确认
|
|
53
136
|
// 用同步 API:CLI 启动阶段还没有 event loop 可用
|
|
54
137
|
try {
|
|
55
138
|
const { execFileSync } = require('child_process');
|
|
56
139
|
const out = execFileSync('curl', [
|
|
57
|
-
'-sS', '-m', '
|
|
140
|
+
'-sS', '-m', '4',
|
|
58
141
|
'-H', `Authorization: Bearer ${token}`,
|
|
59
142
|
'-H', 'Accept: application/vnd.github+json',
|
|
60
143
|
'-H', 'X-GitHub-Api-Version: 2022-11-28',
|
|
61
144
|
`https://api.github.com/user/starred/${REPO}`,
|
|
62
|
-
], { stdio: ['ignore', 'pipe', 'ignore'] }).toString();
|
|
145
|
+
], { stdio: ['ignore', 'pipe', 'ignore'], timeout: 5000 }).toString();
|
|
63
146
|
if (out.trim() === '') return true; // 200 + 空数组 = 列了但没有这个仓库
|
|
64
147
|
const j = JSON.parse(out);
|
|
148
|
+
// GitHub 出错时返回的是 {"message":"Bad credentials"} 之类,不是数组。
|
|
149
|
+
// 那说明 token 无效/过期 —— 属于「无法确认」,不能当成「没 star」,
|
|
150
|
+
// 否则会永远提示下去。
|
|
151
|
+
if (j && !Array.isArray(j) && typeof j.message === 'string') return null;
|
|
65
152
|
return Array.isArray(j) && j.length > 0;
|
|
66
153
|
} catch (e) {
|
|
67
154
|
return null; // 网络不通 / 401 / 没装 curl → 不确定
|
|
@@ -74,34 +161,37 @@ function checkStarred() {
|
|
|
74
161
|
* @param {boolean} [opts.force] 忽略节流,强制提示一次
|
|
75
162
|
* @returns {boolean} 是否真的打印了
|
|
76
163
|
*/
|
|
164
|
+
/**
|
|
165
|
+
* 在 stdout 上印一行 star 提示。
|
|
166
|
+
*
|
|
167
|
+
* 规则(用户明确要求):
|
|
168
|
+
* 1. **不强制** —— 只印文字,绝不自动弹浏览器。要确认请用户自己跑 `jsql star`。
|
|
169
|
+
* 2. **除 `version` 外的每个命令都印**,没有 30 天节流这类「只提醒一次」的设计。
|
|
170
|
+
* 3. `~/.jsqlmr` 记录是否 star 过 —— `starred: true` 是唯一的闭嘴开关
|
|
171
|
+
* (另外 `JSQL_NO_STAR_NAG=1` 可临时静音)。
|
|
172
|
+
*
|
|
173
|
+
* @returns {boolean} 是否真的印了
|
|
174
|
+
*/
|
|
77
175
|
function nag(opts = {}) {
|
|
78
176
|
const out = opts.out || process.stdout;
|
|
79
177
|
const cfg = readConfig();
|
|
80
178
|
|
|
179
|
+
// 已确认 star 过 → 永久闭嘴。jsqlmr 就是干这个的。
|
|
81
180
|
if (!opts.force && cfg.starred) return false;
|
|
82
181
|
|
|
83
|
-
|
|
84
|
-
if (!opts.force && last && Date.now() - last < NAG_INTERVAL_DAYS * 86400000) {
|
|
85
|
-
// 顺便有机会补记 star 状态(不打印)
|
|
86
|
-
const s = checkStarred();
|
|
87
|
-
if (s) { cfg.starred = true; writeConfig(cfg); }
|
|
88
|
-
return false;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
// 有 token 就先查,省得对已经 star 的人重复啰嗦
|
|
182
|
+
// 有凭据就先查一下,省得对已经 star 的人反复啰嗦(查不到就当没查,照印)
|
|
92
183
|
const starred = checkStarred();
|
|
93
184
|
if (starred) {
|
|
94
185
|
cfg.starred = true;
|
|
95
186
|
cfg.starredAt = Date.now();
|
|
96
187
|
writeConfig(cfg);
|
|
97
|
-
return false;
|
|
188
|
+
return false;
|
|
98
189
|
}
|
|
99
190
|
|
|
100
191
|
out.write(`\n If JSQL-NEO saved you time, a star helps others find it: ${URL}\n`);
|
|
101
|
-
out.write(`
|
|
192
|
+
out.write(` Run \`jsql star\` to confirm and stop seeing this (or set JSQL_NO_STAR_NAG=1 to hide)\n\n`);
|
|
102
193
|
|
|
103
|
-
|
|
104
|
-
writeConfig(cfg);
|
|
194
|
+
// 只记录「有没有 star」,不再写 lastNagAt —— 没有节流,写它没有意义。
|
|
105
195
|
return true;
|
|
106
196
|
}
|
|
107
197
|
|
|
@@ -114,4 +204,43 @@ function setStarred(v) {
|
|
|
114
204
|
return cfg.starred;
|
|
115
205
|
}
|
|
116
206
|
|
|
117
|
-
|
|
207
|
+
/**
|
|
208
|
+
* 同步阻塞轮询授权结果,直到拿到 done / expired 或超时。
|
|
209
|
+
*
|
|
210
|
+
* `jsql star` 用:startOauthCheck 的定时器是 unref 的(不能拖住别的命令),
|
|
211
|
+
* 所以 star 命令必须自己前台等,否则授权完成后 CLI 早已退出,结果没人接。
|
|
212
|
+
*
|
|
213
|
+
* @param {string} startUrl startOauthCheck 返回的 URL(含 nonce)
|
|
214
|
+
* @param {number} [timeoutMs] 最长等待时间,默认 180 秒
|
|
215
|
+
* @returns {object|null} result 对象(含 login/starred/repo),超时或失败返回 null
|
|
216
|
+
*/
|
|
217
|
+
function waitForResult(startUrl, timeoutMs = 180000) {
|
|
218
|
+
const { execFileSync } = require('child_process');
|
|
219
|
+
// 注意:本模块顶部有 const URL = 'https://github.com/...' 会遮蔽全局 URL 类,
|
|
220
|
+
// 必须显式从 url 模块取,否则 new URL(...) 抛 TypeError
|
|
221
|
+
const { URL: NodeURL } = require('url');
|
|
222
|
+
let nonce = '';
|
|
223
|
+
try {
|
|
224
|
+
nonce = new NodeURL(startUrl).searchParams.get('nonce') || '';
|
|
225
|
+
} catch (e) {
|
|
226
|
+
return null;
|
|
227
|
+
}
|
|
228
|
+
if (!nonce) return null;
|
|
229
|
+
|
|
230
|
+
const deadline = Date.now() + timeoutMs;
|
|
231
|
+
while (Date.now() < deadline) {
|
|
232
|
+
try {
|
|
233
|
+
const out = execFileSync('curl', ['-sS', '-m', '5', AUTH_RESULT + '?nonce=' + nonce],
|
|
234
|
+
{ stdio: ['ignore', 'pipe', 'ignore'], timeout: 8000, encoding: 'utf8' });
|
|
235
|
+
const j = JSON.parse(out);
|
|
236
|
+
if (j && j.state === 'done' && j.result) return j.result;
|
|
237
|
+
if (j && j.state === 'expired') return null;
|
|
238
|
+
} catch (e) {
|
|
239
|
+
// 网络抖动 / 超时 —— 继续等
|
|
240
|
+
}
|
|
241
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 2000);
|
|
242
|
+
}
|
|
243
|
+
return null;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
module.exports = { nag, checkStarred, startOauthCheck, waitForResult, readConfig, setStarred, configPath, REPO, URL };
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jsql-neo",
|
|
3
|
-
"version": "6.5.1
|
|
3
|
+
"version": "6.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",
|
|
@@ -51,13 +51,14 @@
|
|
|
51
51
|
"test:btree": "node test/btree.test.js",
|
|
52
52
|
"test:native": "node test/native.test.js",
|
|
53
53
|
"test:regress": "node test/regress-5.1.0.js",
|
|
54
|
-
"test:all": "node test/smoke.js && node test/sql-parser.test.js && node test/readme-audit.test.js && node test/native.test.js && node test/native-multi-instance.test.js && node test/wal-recovery.test.js && node test/mysql-compat-hijack.test.js && node test/wasm.test.js && node test/coverage.js && node test/regress-5.1.0.js && node test/join.test.js && node test/btree.test.js && node test/dml-constraints.test.js && node test/savepoint.test.js && node test/alter-table.test.js && node test/null-semantics.test.js && node test/identifier-keywords.test.js && node test/sql-semantics.test.js && node test/extra-coverage.test.js && node test/filter-compile.test.js && node test/recursive-cte.test.js && node test/index-ddl.test.js",
|
|
54
|
+
"test:all": "node test/smoke.js && node test/sql-parser.test.js && node test/readme-audit.test.js && node test/native.test.js && node test/native-multi-instance.test.js && node test/wal-recovery.test.js && node test/mysql-compat-hijack.test.js && node test/wasm.test.js && node test/coverage.js && node test/regress-5.1.0.js && node test/join.test.js && node test/btree.test.js && node test/dml-constraints.test.js && node test/savepoint.test.js && node test/alter-table.test.js && node test/null-semantics.test.js && node test/identifier-keywords.test.js && node test/sql-semantics.test.js && node test/extra-coverage.test.js && node test/filter-compile.test.js && node test/recursive-cte.test.js && node test/index-ddl.test.js && node test/mongo-group.test.js && node test/multiprotocol.test.js",
|
|
55
55
|
"test:coverage": "c8 --all --include='lib/**' --exclude='lib/web_ui.js' --reporter=text --reporter=lcov node test/coverage.js",
|
|
56
56
|
"test:orms": "node examples/orms/run-all.js",
|
|
57
57
|
"test:dml": "node test/dml-constraints.test.js",
|
|
58
58
|
"test:cte": "node test/recursive-cte.test.js",
|
|
59
59
|
"test:index": "node test/index-ddl.test.js",
|
|
60
60
|
"test:mongo": "node test/mongo-group.test.js",
|
|
61
|
+
"test:multi": "node test/multiprotocol.test.js",
|
|
61
62
|
"test:savepoint": "node test/savepoint.test.js",
|
|
62
63
|
"test:alter": "node test/alter-table.test.js",
|
|
63
64
|
"test:ident": "node test/identifier-keywords.test.js",
|
package/test/mongo-group.test.js
CHANGED
|
@@ -1,16 +1,5 @@
|
|
|
1
|
-
/*
|
|
2
|
-
*
|
|
3
|
-
* 现状:本文件会崩在 `rows is not iterable`(lib/mongo_server.js:541)。
|
|
4
|
-
* 另外 MongoDB 协议栈与**官方现代 driver 的握手尚未打通** ——
|
|
5
|
-
* 用官方 mongodb driver 连不上。
|
|
6
|
-
*
|
|
7
|
-
* 因此「四协议」目前实际是「一个半」:MySQL 可用;PG / Redis 以库形式
|
|
8
|
-
* 提供、需自行装配;MongoDB 仅供实验。
|
|
9
|
-
* 文档已按此更正(doc/why.md、doc/cli.md)。
|
|
10
|
-
*
|
|
11
|
-
* 修好前请勿把本文件加进 CI —— 它一崩就会像之前那样掐断 `&&` 链。
|
|
12
|
-
* 修 MongoDB 需要单独排期,不属于性能/打包类改动。
|
|
13
|
-
*/
|
|
1
|
+
/* Test: MongoDB $group aggregation operators
|
|
2
|
+
* Directly tests the _dispatch method of createMongoServer */
|
|
14
3
|
|
|
15
4
|
// Test: MongoDB $group aggregation operators
|
|
16
5
|
// Directly tests the _dispatch method of createMongoServer
|
|
@@ -26,9 +15,9 @@ const ok = (name, cond, extra) => {
|
|
|
26
15
|
const srv = createMongoServer({ port: 0, noAuth: true, dataDir: ':memory:' });
|
|
27
16
|
srv.listen(() => {
|
|
28
17
|
(async () => {
|
|
29
|
-
// Get the engine and pre-populate
|
|
30
|
-
const engine = await srv._getEngine();
|
|
31
|
-
engine.createTable('sales', {
|
|
18
|
+
// Get the engine and pre-populate(库名必须与 dispatch 的 $db 一致)
|
|
19
|
+
const engine = await srv._getEngine('test');
|
|
20
|
+
await engine.createTable('sales', { _id: { type: 'integer' }, item: { type: 'string' }, qty: { type: 'integer' }, price: { type: 'integer' } });
|
|
32
21
|
const table = engine._ensureTable('sales');
|
|
33
22
|
table._rows = [
|
|
34
23
|
{ _id: 1, item: 'A', qty: 10, price: 5 },
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 单端口多协议回归测试
|
|
3
|
+
*
|
|
4
|
+
* 校验 README 里「One Port」这条承诺:
|
|
5
|
+
* 1. 同一个 TCP 端口按首包嗅探到 MySQL / PostgreSQL / MongoDB / Redis
|
|
6
|
+
* 2. 静默连接(MySQL 客户端等服务器先握手)按 idleProtocol 兜底
|
|
7
|
+
* 3. `jsql serve` 默认启动多协议服务器,`--mysql-only` 保留单协议行为
|
|
8
|
+
*
|
|
9
|
+
* 全部使用原始 socket,不依赖 mysql2 / pg / mongodb / ioredis。
|
|
10
|
+
*/
|
|
11
|
+
const net = require('net');
|
|
12
|
+
const path = require('path');
|
|
13
|
+
const { spawn } = require('child_process');
|
|
14
|
+
const { createMultiServer, sniffProtocol } = require('../lib/multiserver');
|
|
15
|
+
const { bsonDocument } = require('../lib/mongo_server');
|
|
16
|
+
|
|
17
|
+
let pass = 0, fail = 0;
|
|
18
|
+
function ok(name, cond, extra) {
|
|
19
|
+
if (cond) { pass++; console.log('[OK] ' + name); }
|
|
20
|
+
else { fail++; console.log('[FAIL] ' + name + (extra !== undefined ? ' → ' + JSON.stringify(extra) : '')); }
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function connect(port) {
|
|
24
|
+
return new Promise((resolve, reject) => {
|
|
25
|
+
const s = net.connect(port, '127.0.0.1', () => resolve(s));
|
|
26
|
+
s.on('error', reject);
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function readSome(socket, ms = 1500) {
|
|
31
|
+
return new Promise((resolve) => {
|
|
32
|
+
let buf = Buffer.alloc(0);
|
|
33
|
+
const done = () => { clearTimeout(t); socket.removeListener('data', onData); resolve(buf); };
|
|
34
|
+
const onData = (c) => { buf = Buffer.concat([buf, c]); };
|
|
35
|
+
const t = setTimeout(done, ms);
|
|
36
|
+
socket.on('data', onData);
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function mongoHello() {
|
|
41
|
+
const payload = bsonDocument({ hello: 1, helloOk: true, $db: 'admin' });
|
|
42
|
+
const head = Buffer.alloc(16);
|
|
43
|
+
head.writeInt32LE(16 + 4 + 1 + payload.length, 0);
|
|
44
|
+
head.writeInt32LE(1, 4);
|
|
45
|
+
head.writeInt32LE(0, 8);
|
|
46
|
+
head.writeInt32LE(2013, 12);
|
|
47
|
+
return Buffer.concat([head, Buffer.alloc(4), Buffer.from([0]), payload]);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function main() {
|
|
51
|
+
console.log('--- 首包嗅探 ---');
|
|
52
|
+
ok('PG StartupMessage 判为 pg', sniffProtocol(Buffer.from([0, 0, 0, 60, 0, 3, 0, 0])) === 'pg');
|
|
53
|
+
ok('MySQL 握手版本判为 mysql', sniffProtocol(Buffer.from([0x0a, 0x38, 0x2e, 0x30])) === 'mysql');
|
|
54
|
+
ok('RESP 数组判为 redis', sniffProtocol(Buffer.from('*1\r\n$4\r\nPING\r\n')) === 'redis');
|
|
55
|
+
{
|
|
56
|
+
const m = mongoHello();
|
|
57
|
+
ok('Mongo OP_MSG 判为 mongo', sniffProtocol(m) === 'mongo');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
console.log('\n--- 同一端口四协议 ---');
|
|
61
|
+
const srv = createMultiServer({ port: 0, host: '127.0.0.1', noAuth: true });
|
|
62
|
+
await new Promise((r) => srv.listen(r));
|
|
63
|
+
const port = srv.address.port;
|
|
64
|
+
ok('监听成功', port > 0, port);
|
|
65
|
+
|
|
66
|
+
{
|
|
67
|
+
const s = await connect(port);
|
|
68
|
+
const buf = await readSome(s);
|
|
69
|
+
s.destroy();
|
|
70
|
+
ok('MySQL 静默连接收到握手包', buf.length > 5 && buf[4] === 0x0a, buf.slice(0, 8).toString('hex'));
|
|
71
|
+
ok('握手包含服务器版本', buf.includes(Buffer.from('jsql-neo')));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
{
|
|
75
|
+
const s = await connect(port);
|
|
76
|
+
const ssl = Buffer.alloc(8);
|
|
77
|
+
ssl.writeInt32BE(8, 0);
|
|
78
|
+
ssl.writeInt32BE(80877103, 4); // SSLRequest
|
|
79
|
+
s.write(ssl);
|
|
80
|
+
const buf = await readSome(s, 800);
|
|
81
|
+
s.destroy();
|
|
82
|
+
ok('PostgreSQL SSLRequest 被拒绝并回 N', buf.toString('utf8') === 'N', buf.toString('utf8'));
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
{
|
|
86
|
+
const s = await connect(port);
|
|
87
|
+
s.write(Buffer.from('PING\r\n'));
|
|
88
|
+
const buf = await readSome(s, 800);
|
|
89
|
+
s.destroy();
|
|
90
|
+
ok('Redis PING 返回 +PONG', buf.toString('utf8').startsWith('+PONG'), buf.toString('utf8'));
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
{
|
|
94
|
+
const s = await connect(port);
|
|
95
|
+
s.write(mongoHello());
|
|
96
|
+
const buf = await readSome(s, 1500);
|
|
97
|
+
s.destroy();
|
|
98
|
+
ok('Mongo hello 返回 OP_MSG', buf.length >= 16 && buf.readInt32LE(12) === 2013);
|
|
99
|
+
ok('Mongo hello ok:1', buf.includes(Buffer.from('isWritablePrimary')));
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
console.log('\n--- 共享同一份 SQL 引擎 ---');
|
|
103
|
+
{
|
|
104
|
+
// 直接用 MT 服务器内部的引擎实例验证:Mongo 与 SQL 走同一个 Database
|
|
105
|
+
const eng = await srv.mysql._getEngine('shared');
|
|
106
|
+
const sameAsPg = (await srv.pg._getEngine('shared')) === eng;
|
|
107
|
+
const sameAsMongo = (await srv.mongo._getEngine('shared')) === eng;
|
|
108
|
+
ok('MySQL/PG/Mongo 取到同一个引擎', sameAsPg && sameAsMongo);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
await new Promise((r) => srv.close(r));
|
|
112
|
+
|
|
113
|
+
console.log('\n--- idleProtocol 兜底 ---');
|
|
114
|
+
{
|
|
115
|
+
const redisIdle = createMultiServer({ port: 0, host: '127.0.0.1', idleProtocol: 'redis' });
|
|
116
|
+
await new Promise((r) => redisIdle.listen(r));
|
|
117
|
+
const s = await connect(redisIdle.address.port);
|
|
118
|
+
// 不主动发包:按 idleProtocol 走 Redis,随后发 PING 必须能回
|
|
119
|
+
await new Promise((r) => setTimeout(r, 350));
|
|
120
|
+
s.write(Buffer.from('PING\r\n'));
|
|
121
|
+
const buf = await readSome(s, 800);
|
|
122
|
+
s.destroy();
|
|
123
|
+
ok('idleProtocol=redis 时空闲连接仍可用', buf.toString('utf8').startsWith('+PONG'), buf.toString('utf8'));
|
|
124
|
+
await new Promise((r) => redisIdle.close(r));
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
console.log('\n--- CLI serve 入口 ---');
|
|
128
|
+
{
|
|
129
|
+
const bin = path.join(__dirname, '..', 'bin', 'jsql');
|
|
130
|
+
const child = spawn(process.execPath, [bin, 'serve', '-p', '0'], {
|
|
131
|
+
env: { ...process.env, JSQL_NO_STAR_NAG: '1' },
|
|
132
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
133
|
+
});
|
|
134
|
+
let out = '';
|
|
135
|
+
child.stdout.on('data', (c) => { out += c; });
|
|
136
|
+
child.stderr.on('data', (c) => { out += c; });
|
|
137
|
+
const started = await new Promise((resolve) => {
|
|
138
|
+
const t = setTimeout(() => resolve(false), 5000);
|
|
139
|
+
const check = () => {
|
|
140
|
+
if (/multi-protocol server on/.test(out) || /serve failed/.test(out)) {
|
|
141
|
+
clearTimeout(t); resolve(/multi-protocol server on/.test(out));
|
|
142
|
+
} else setTimeout(check, 50);
|
|
143
|
+
};
|
|
144
|
+
check();
|
|
145
|
+
});
|
|
146
|
+
ok('jsql serve 默认启动多协议服务器', started, out.trim());
|
|
147
|
+
child.kill('SIGTERM');
|
|
148
|
+
await new Promise((r) => { child.on('exit', r); setTimeout(r, 1000); });
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
console.log(`\nRESULT: ${pass} passed, ${fail} failed`);
|
|
152
|
+
if (fail > 0) process.exit(1);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
main().catch((e) => { console.error(e); process.exit(1); });
|