jsql-neo 6.3.6 → 6.4.0-beta.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 +91 -8231
- package/bin/jsql +90 -0
- package/index.js +1 -1
- package/lib/cache.js +53 -34
- package/lib/client.js +11 -2
- package/lib/database.js +380 -60
- package/lib/date-types.js +56 -15
- package/lib/errors.js +4 -1
- package/lib/jsql_format.js +45 -24
- package/lib/mod.js +27 -6
- package/lib/mongo_server.js +174 -40
- package/lib/mysql_compat.js +58 -11
- package/lib/mysql_server.js +110 -4
- package/lib/nedb_compat.js +11 -8
- package/lib/pg_server.js +4 -11
- package/lib/plugin.js +20 -2
- package/lib/prepared.js +10 -0
- package/lib/query.js +11 -3
- package/lib/redis_server.js +124 -27
- package/lib/sql.js +935 -85
- package/lib/sqlite_compat.js +2 -1
- package/lib/sqlite_worker.js +8 -2
- package/lib/table.js +149 -29
- package/lib/tui.js +1 -1
- package/lib/web_ui.js +24 -8
- package/native/jsql-neo-native.node +0 -0
- package/package.json +11 -4
- package/test/alter-table.test.js +289 -0
- package/test/identifier-keywords.test.js +140 -0
- package/test/index-ddl.test.js +203 -0
- package/test/mongo-group.test.js +105 -0
- package/test/null-semantics.test.js +126 -0
- package/test/readme-audit.test.js +8 -7
- package/test/recursive-cte.test.js +123 -0
- package/test/savepoint.test.js +388 -0
- package/test/sql-parser.test.js +30 -5
package/bin/jsql
CHANGED
|
@@ -247,6 +247,96 @@ const cli = yaggs({ pkg: require('../package.json') })
|
|
|
247
247
|
console.log(`jsql-neo Redis server on ${argv.host || '127.0.0.1'}:${argv.port != null ? argv.port : 6379}${argv['data-dir'] ? ' (data: ' + argv['data-dir'] + ')' : ''}`);
|
|
248
248
|
setInterval(() => {}, 1 << 30);
|
|
249
249
|
})
|
|
250
|
+
.command('plugin', 'Plugin registry management (list, search, install, uninstall, info)', (sub) => {
|
|
251
|
+
sub.option('json', { type: 'boolean', description: 'Output as JSON' });
|
|
252
|
+
sub.option('category', { type: 'string', description: 'Filter by category' });
|
|
253
|
+
sub.option('limit', { type: 'number', description: 'Max results per page' });
|
|
254
|
+
}, async (argv) => {
|
|
255
|
+
const pluginMgr = require('../src/cli/plugin');
|
|
256
|
+
const action = argv._[0];
|
|
257
|
+
const arg = argv._[1];
|
|
258
|
+
const useJson = !!argv.json;
|
|
259
|
+
|
|
260
|
+
const fail = (e) => { console.error(`Error: ${e.message}`); process.exitCode = 1; };
|
|
261
|
+
const out = (obj) => {
|
|
262
|
+
if (obj === undefined) return;
|
|
263
|
+
if (useJson) console.log(JSON.stringify(obj, null, 2));
|
|
264
|
+
else console.log(obj);
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
try {
|
|
268
|
+
switch (action) {
|
|
269
|
+
case 'list':
|
|
270
|
+
case 'ls': {
|
|
271
|
+
const opts = {};
|
|
272
|
+
if (argv.category) opts.category = argv.category;
|
|
273
|
+
if (argv.limit) opts.limit = argv.limit;
|
|
274
|
+
const plugins = await pluginMgr.listPlugins(opts);
|
|
275
|
+
out(useJson ? plugins : pluginMgr.formatPluginList(plugins));
|
|
276
|
+
break;
|
|
277
|
+
}
|
|
278
|
+
case 'search': {
|
|
279
|
+
if (!arg) throw new Error('Usage: jsql plugin search <query>');
|
|
280
|
+
const opts = {};
|
|
281
|
+
if (argv.category) opts.category = argv.category;
|
|
282
|
+
const data = await pluginMgr.searchPlugins(arg, opts);
|
|
283
|
+
// search 返回 { results: [...], query, total } 或 [...]
|
|
284
|
+
const plugins = Array.isArray(data) ? data : (data.results || []);
|
|
285
|
+
if (useJson) {
|
|
286
|
+
out(data);
|
|
287
|
+
} else {
|
|
288
|
+
if (!plugins.length) {
|
|
289
|
+
console.log(`No plugins found for '${arg}'.`);
|
|
290
|
+
} else {
|
|
291
|
+
console.log(`Found ${plugins.length} plugin(s) for '${arg}':`);
|
|
292
|
+
console.log(pluginMgr.formatPluginList(plugins.map(p => p.plugin || p)));
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
break;
|
|
296
|
+
}
|
|
297
|
+
case 'install': {
|
|
298
|
+
if (!arg) throw new Error('Usage: jsql plugin install <name>');
|
|
299
|
+
const manifest = await pluginMgr.installPlugin(arg);
|
|
300
|
+
out(useJson ? manifest : `Installed plugin '${manifest.name}' (${manifest.version})`);
|
|
301
|
+
break;
|
|
302
|
+
}
|
|
303
|
+
case 'uninstall':
|
|
304
|
+
case 'remove': {
|
|
305
|
+
if (!arg) throw new Error('Usage: jsql plugin uninstall <name>');
|
|
306
|
+
const r = pluginMgr.uninstallPlugin(arg);
|
|
307
|
+
out(useJson ? r : `Uninstalled plugin '${r.name}'.`);
|
|
308
|
+
break;
|
|
309
|
+
}
|
|
310
|
+
case 'info': {
|
|
311
|
+
if (!arg) throw new Error('Usage: jsql plugin info <name>');
|
|
312
|
+
const p = await pluginMgr.getPluginInfo(arg);
|
|
313
|
+
out(useJson ? p : pluginMgr.formatPluginInfo(p));
|
|
314
|
+
break;
|
|
315
|
+
}
|
|
316
|
+
case undefined:
|
|
317
|
+
// 无子命令 → 显示已安装插件
|
|
318
|
+
const installed = pluginMgr.listInstalled();
|
|
319
|
+
if (useJson) {
|
|
320
|
+
out(installed);
|
|
321
|
+
} else {
|
|
322
|
+
if (!installed.length) {
|
|
323
|
+
console.log('No plugins installed.');
|
|
324
|
+
console.log(`Plugin directory: ${pluginMgr.PLUGIN_DIR}`);
|
|
325
|
+
} else {
|
|
326
|
+
console.log(`Installed plugins (${installed.length}):`);
|
|
327
|
+
installed.forEach(p => {
|
|
328
|
+
console.log(` ${p.name.padEnd(24)} ${p.version || '?'}`);
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
break;
|
|
333
|
+
default:
|
|
334
|
+
throw new Error(`Unknown plugin action: ${action}`);
|
|
335
|
+
}
|
|
336
|
+
} catch (e) {
|
|
337
|
+
fail(e);
|
|
338
|
+
}
|
|
339
|
+
})
|
|
250
340
|
.command('version', 'Show version', null, () => {
|
|
251
341
|
console.log(require('../package.json').version);
|
|
252
342
|
})
|
package/index.js
CHANGED
package/lib/cache.js
CHANGED
|
@@ -36,7 +36,8 @@ class Cache {
|
|
|
36
36
|
return count;
|
|
37
37
|
}
|
|
38
38
|
keys(pattern = '*') {
|
|
39
|
-
const
|
|
39
|
+
const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '.*').replace(/\?/g, '.');
|
|
40
|
+
const regex = new RegExp('^' + escaped + '$');
|
|
40
41
|
const result = [];
|
|
41
42
|
for (const key of this._getData().keys()) { if (!this._isExpired(key) && regex.test(key)) result.push(key); }
|
|
42
43
|
return result;
|
|
@@ -47,8 +48,9 @@ class Cache {
|
|
|
47
48
|
const val = this._getData().get(k);
|
|
48
49
|
if (val === undefined) return 'none';
|
|
49
50
|
if (typeof val === 'string') return 'string';
|
|
50
|
-
if (Array.isArray(val)) return val._type === 'hash' ? 'hash' : 'list';
|
|
51
51
|
if (val instanceof Set) return 'set';
|
|
52
|
+
if (Array.isArray(val)) return val._type === 'hash' ? 'hash' : 'list';
|
|
53
|
+
if (val !== null && typeof val === 'object' && val._type === 'hash') return 'hash';
|
|
52
54
|
return 'string';
|
|
53
55
|
}
|
|
54
56
|
expire(key, seconds) {
|
|
@@ -83,7 +85,10 @@ class Cache {
|
|
|
83
85
|
hset(key, field, value) {
|
|
84
86
|
const k = String(key);
|
|
85
87
|
let hash = this._getData().get(k);
|
|
86
|
-
if (
|
|
88
|
+
if (hash !== undefined && (typeof hash !== 'object' || hash._type !== 'hash')) {
|
|
89
|
+
throw new Error('WRONGTYPE Operation against a key holding the wrong kind of value');
|
|
90
|
+
}
|
|
91
|
+
if (!hash) { hash = { _type: 'hash' }; this._getData().set(k, hash); }
|
|
87
92
|
const existed = hash[field] !== undefined;
|
|
88
93
|
hash[field] = String(value);
|
|
89
94
|
this._dirty = true;
|
|
@@ -132,7 +137,10 @@ class Cache {
|
|
|
132
137
|
lpush(key, ...values) {
|
|
133
138
|
const k = String(key);
|
|
134
139
|
let list = this._getData().get(k);
|
|
135
|
-
if (
|
|
140
|
+
if (list !== undefined && (!Array.isArray(list) || list._type === 'hash')) {
|
|
141
|
+
throw new Error('WRONGTYPE Operation against a key holding the wrong kind of value');
|
|
142
|
+
}
|
|
143
|
+
if (!list) { list = []; this._getData().set(k, list); }
|
|
136
144
|
list.unshift(...values.map(String).reverse());
|
|
137
145
|
this._dirty = true;
|
|
138
146
|
return list.length;
|
|
@@ -140,7 +148,10 @@ class Cache {
|
|
|
140
148
|
rpush(key, ...values) {
|
|
141
149
|
const k = String(key);
|
|
142
150
|
let list = this._getData().get(k);
|
|
143
|
-
if (
|
|
151
|
+
if (list !== undefined && (!Array.isArray(list) || list._type === 'hash')) {
|
|
152
|
+
throw new Error('WRONGTYPE Operation against a key holding the wrong kind of value');
|
|
153
|
+
}
|
|
154
|
+
if (!list) { list = []; this._getData().set(k, list); }
|
|
144
155
|
list.push(...values.map(String));
|
|
145
156
|
this._dirty = true;
|
|
146
157
|
return list.length;
|
|
@@ -175,13 +186,18 @@ class Cache {
|
|
|
175
186
|
if (!list || !Array.isArray(list) || list._type === 'hash') return [];
|
|
176
187
|
if (start < 0) start = Math.max(list.length + start, 0);
|
|
177
188
|
if (stop < 0) stop = list.length + stop;
|
|
189
|
+
stop = Math.min(stop, list.length - 1);
|
|
190
|
+
if (start > stop) return [];
|
|
178
191
|
return list.slice(start, stop + 1);
|
|
179
192
|
}
|
|
180
193
|
|
|
181
194
|
sadd(key, ...members) {
|
|
182
195
|
const k = String(key);
|
|
183
196
|
let set = this._getData().get(k);
|
|
184
|
-
if (
|
|
197
|
+
if (set !== undefined && !(set instanceof Set)) {
|
|
198
|
+
throw new Error('WRONGTYPE Operation against a key holding the wrong kind of value');
|
|
199
|
+
}
|
|
200
|
+
if (!set) { set = new Set(); this._getData().set(k, set); }
|
|
185
201
|
let count = 0;
|
|
186
202
|
for (const m of members) { if (!set.has(String(m))) { set.add(String(m)); count++; } }
|
|
187
203
|
if (count > 0) this._dirty = true;
|
|
@@ -219,10 +235,11 @@ class Cache {
|
|
|
219
235
|
}
|
|
220
236
|
|
|
221
237
|
flushall() {
|
|
238
|
+
const currentDb = this._db;
|
|
222
239
|
for (let i = 0; i < this._dbs.length; i++) { if (this._dbs[i]) this._dbs[i].clear(); if (this._dbsExpires[i]) this._dbsExpires[i].clear(); }
|
|
223
|
-
this._data = this._dbs[
|
|
224
|
-
this._expires = this._dbsExpires[
|
|
225
|
-
this._db =
|
|
240
|
+
this._data = this._dbs[currentDb] = new Map();
|
|
241
|
+
this._expires = this._dbsExpires[currentDb] = new Map();
|
|
242
|
+
this._db = currentDb;
|
|
226
243
|
this._dirty = true;
|
|
227
244
|
return 'OK';
|
|
228
245
|
}
|
|
@@ -244,32 +261,34 @@ class Cache {
|
|
|
244
261
|
|
|
245
262
|
save() {
|
|
246
263
|
if (!this._persistPath) return;
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
const
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
264
|
+
try {
|
|
265
|
+
const dump = { version: 1, databases: [] };
|
|
266
|
+
for (let i = 0; i < this._dbs.length; i++) {
|
|
267
|
+
const db = this._dbs[i];
|
|
268
|
+
if (!db || db.size === 0) continue;
|
|
269
|
+
const data = {};
|
|
270
|
+
for (const [key, val] of db) {
|
|
271
|
+
const exp = this._dbsExpires[i];
|
|
272
|
+
if (exp && exp.has(key) && exp.get(key) <= Date.now()) continue;
|
|
273
|
+
if (val instanceof Set) data[key] = { _t: 'set', _v: [...val] };
|
|
274
|
+
else if (Array.isArray(val)) data[key] = { _t: 'list', _v: val };
|
|
275
|
+
else if (typeof val === 'object' && val._type === 'hash') {
|
|
276
|
+
const clean = {}; for (const [f, v] of Object.entries(val)) { if (f !== '_type') clean[f] = v; }
|
|
277
|
+
data[key] = { _t: 'hash', _v: clean };
|
|
278
|
+
} else data[key] = { _t: 'string', _v: val };
|
|
279
|
+
}
|
|
280
|
+
const expires = {};
|
|
281
|
+
const expMap = this._dbsExpires[i];
|
|
282
|
+
if (expMap) { for (const [key, ttl] of expMap) { if (ttl > Date.now()) expires[key] = ttl; } }
|
|
283
|
+
if (Object.keys(data).length > 0 || Object.keys(expires).length > 0) dump.databases.push({ index: i, data, expires });
|
|
261
284
|
}
|
|
262
|
-
const
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
fs.writeFileSync(this._persistPath + '.tmp', JSON.stringify(dump), 'utf8');
|
|
270
|
-
fs.renameSync(this._persistPath + '.tmp', this._persistPath);
|
|
271
|
-
this._dirty = false;
|
|
272
|
-
return 'OK';
|
|
285
|
+
const dir = path.dirname(this._persistPath);
|
|
286
|
+
if (dir) fs.mkdirSync(dir, { recursive: true });
|
|
287
|
+
fs.writeFileSync(this._persistPath + '.tmp', JSON.stringify(dump), 'utf8');
|
|
288
|
+
fs.renameSync(this._persistPath + '.tmp', this._persistPath);
|
|
289
|
+
this._dirty = false;
|
|
290
|
+
return 'OK';
|
|
291
|
+
} catch (e) { return null; }
|
|
273
292
|
}
|
|
274
293
|
|
|
275
294
|
_load() {
|
package/lib/client.js
CHANGED
|
@@ -12,7 +12,8 @@ function binPath() {
|
|
|
12
12
|
class JSQL {
|
|
13
13
|
constructor(opts = {}) {
|
|
14
14
|
this.host = opts.host || '127.0.0.1';
|
|
15
|
-
|
|
15
|
+
// P3-5: 默认端口 6379 会与 Redis 冲突,改为 3000
|
|
16
|
+
this.port = opts.port || 3000;
|
|
16
17
|
this.dataDir = opts.dataDir || null;
|
|
17
18
|
this._process = null;
|
|
18
19
|
}
|
|
@@ -45,7 +46,8 @@ class JSQL {
|
|
|
45
46
|
async stop() {
|
|
46
47
|
if (!this._process) return;
|
|
47
48
|
this._process.kill();
|
|
48
|
-
|
|
49
|
+
// P3-6: 不再硬等 500ms,给进程一点时间退出即可
|
|
50
|
+
await new Promise(r => setTimeout(r, 100));
|
|
49
51
|
this._process = null;
|
|
50
52
|
}
|
|
51
53
|
|
|
@@ -74,6 +76,13 @@ class JSQL {
|
|
|
74
76
|
headers: body ? { 'Content-Type': 'application/json' } : {},
|
|
75
77
|
};
|
|
76
78
|
const req = http.request(opts, (res) => {
|
|
79
|
+
// P3-7: 未处理 res 错误——非 2xx 状态码应 reject
|
|
80
|
+
if (res.statusCode < 200 || res.statusCode >= 300) {
|
|
81
|
+
let errData = '';
|
|
82
|
+
res.on('data', c => errData += c);
|
|
83
|
+
res.on('end', () => reject(new Error(`HTTP ${res.statusCode}: ${errData}`)));
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
77
86
|
let data = '';
|
|
78
87
|
res.on('data', c => data += c);
|
|
79
88
|
res.on('end', () => {
|