jsql-neo 5.5.0 → 6.0.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.
Files changed (81) hide show
  1. package/LICENSE +202 -202
  2. package/README.md +8155 -8097
  3. package/bin/jsql +320 -320
  4. package/bin/jsql-server +90 -90
  5. package/bin/jsql-server-control.js +177 -177
  6. package/index.d.ts +412 -385
  7. package/index.js +128 -124
  8. package/lib/ast.js +214 -214
  9. package/lib/btree.js +478 -413
  10. package/lib/cache.js +328 -328
  11. package/lib/client.js +125 -125
  12. package/lib/database.js +1741 -1744
  13. package/lib/date-types.js +182 -182
  14. package/lib/errors.js +89 -89
  15. package/lib/jsql_format.js +497 -497
  16. package/lib/migrate.js +254 -254
  17. package/lib/mod.js +292 -292
  18. package/lib/mongo_server.js +607 -607
  19. package/lib/multiserver.js +140 -140
  20. package/lib/mysql_compat.js +413 -413
  21. package/lib/mysql_server.js +1237 -1237
  22. package/lib/native_client.js +722 -732
  23. package/lib/nedb_compat.js +506 -506
  24. package/lib/pg_server.js +864 -864
  25. package/lib/plugin.js +259 -35
  26. package/lib/plugins/audit.js +37 -0
  27. package/lib/plugins/index.js +72 -0
  28. package/lib/plugins/logger.js +35 -0
  29. package/lib/plugins/metrics.js +38 -0
  30. package/lib/plugins/timestamps.js +57 -0
  31. package/lib/plugins/validation.js +128 -0
  32. package/lib/prepared.js +37 -0
  33. package/lib/query.js +834 -815
  34. package/lib/redis_server.js +571 -571
  35. package/lib/sql.js +4053 -3460
  36. package/lib/sqlite_compat.js +323 -323
  37. package/lib/sqlite_worker.js +264 -264
  38. package/lib/table.js +1351 -1304
  39. package/lib/tui.js +502 -502
  40. package/lib/wasm.js +1 -1
  41. package/lib/wasm_client.js +715 -718
  42. package/lib/web_ui.js +257 -257
  43. package/native/jsql-neo-native.node +0 -0
  44. package/nativesrc/jsql-neo-core/Cargo.lock +462 -455
  45. package/nativesrc/jsql-neo-core/Cargo.toml +26 -24
  46. package/nativesrc/jsql-neo-core/src/engine/hybrid.rs +492 -455
  47. package/nativesrc/jsql-neo-core/src/engine/memory.rs +191 -168
  48. package/nativesrc/jsql-neo-core/src/engine/mod.rs +74 -62
  49. package/nativesrc/jsql-neo-core/src/engine/table.rs +996 -700
  50. package/nativesrc/jsql-neo-core/src/lib.rs +2 -2
  51. package/nativesrc/jsql-neo-core/src/storage/mod.rs +2 -2
  52. package/nativesrc/jsql-neo-core/src/storage/persistent.rs +1 -1
  53. package/nativesrc/jsql-neo-core/src/storage/wal.rs +298 -85
  54. package/nativesrc/jsql-neo-core/src/types.rs +93 -93
  55. package/nativesrc/jsql-neo-native/Cargo.lock +613 -606
  56. package/nativesrc/jsql-neo-native/Cargo.toml +24 -16
  57. package/nativesrc/jsql-neo-native/build.rs +3 -3
  58. package/nativesrc/jsql-neo-native/package.json +7 -7
  59. package/nativesrc/jsql-neo-native/src/lib.rs +281 -269
  60. package/nativesrc/jsql-neo-wasm/Cargo.lock +472 -465
  61. package/nativesrc/jsql-neo-wasm/Cargo.toml +22 -15
  62. package/nativesrc/jsql-neo-wasm/src/lib.rs +463 -447
  63. package/package.json +87 -87
  64. package/postinstall.js +158 -158
  65. package/test/btree.test.js +144 -128
  66. package/test/coverage.js +187 -187
  67. package/test/join.test.js +110 -110
  68. package/test/native.test.js +158 -158
  69. package/test/query-opt.test.js +109 -0
  70. package/test/regress-5.1.0.js +271 -271
  71. package/test/smoke.js +58 -58
  72. package/test/sql-parser.test.js +450 -273
  73. package/test/wasm.test.js +110 -110
  74. package/wasm/browser.d.ts +89 -89
  75. package/wasm/browser.mjs +591 -569
  76. package/wasm/browser_bg.mjs +618 -597
  77. package/wasm/jsql_neo_wasm.d.ts +44 -38
  78. package/wasm/jsql_neo_wasm.js +641 -520
  79. package/wasm/jsql_neo_wasm_bg.wasm +0 -0
  80. package/wasm/jsql_neo_wasm_bg.wasm.d.ts +26 -26
  81. package/wasm/package.json +11 -11
package/bin/jsql CHANGED
@@ -1,320 +1,320 @@
1
- #!/usr/bin/env node
2
- const path = require('path');
3
- const fs = require('fs');
4
- const yaggs = require('@vexify-org/yaggs');
5
- const { ModuleManager } = require('../lib/mod');
6
- const Database = require('../lib/database');
7
- const migrate = require('../lib/migrate');
8
-
9
- function printModules(list) {
10
- if (list.length === 0) {
11
- console.log('No modules registered.');
12
- return;
13
- }
14
- const rows = list.map(m =>
15
- `${m.enabled ? 'ON ' : 'OFF'} ${m.name.padEnd(24)} ${m.path}`
16
- );
17
- console.log(rows.join('\n'));
18
- }
19
-
20
- const manager = new ModuleManager();
21
-
22
- const RAW_ARGV = process.argv.slice(2);
23
-
24
- const cli = yaggs({ pkg: require('../package.json') })
25
- .usage('jsql <command> [options]')
26
- .option('help', { alias: ['h'], type: 'boolean', description: 'Show help' })
27
- .command('mod', 'Module management', (sub) => {
28
- sub.option('address', {
29
- alias: ['a'],
30
- type: 'string',
31
- description: 'Path to module file'
32
- });
33
- sub.option('json', {
34
- type: 'boolean',
35
- description: 'Output as JSON'
36
- });
37
- }, (argv) => {
38
- const action = argv._[0];
39
- const name = argv._[1];
40
- const useJson = !!argv.json;
41
-
42
- const fail = (err) => {
43
- console.error(`Error: ${err.message}`);
44
- process.exitCode = 1;
45
- };
46
- const out = (obj) => {
47
- if (obj === undefined) return;
48
- if (useJson) {
49
- console.log(JSON.stringify(obj, null, 2));
50
- } else {
51
- console.log(obj);
52
- }
53
- };
54
-
55
- try {
56
- switch (action) {
57
- case 'add': {
58
- if (!argv.address) return fail(new Error("'mod add' requires --address <path>"));
59
- const ext = path.extname(argv.address).toLowerCase();
60
- if (ext === '.js' || ext === '.cjs' || ext === '.mjs') {
61
- console.warn("Warning: JS modules run without sandbox isolation (trust model). Use .json files for data-only plugins.");
62
- }
63
- const entry = manager.add(argv.address);
64
- out(useJson ? entry : `Module '${entry.name}' added (${entry.path}). Use 'jsql mod enable ${entry.name}' to enable.`);
65
- break;
66
- }
67
- case 'enable': {
68
- if (!name) return fail(new Error("'mod enable' requires a module name"));
69
- const r = manager.enable(name);
70
- out(useJson ? r : `Module '${name}' enabled.`);
71
- break;
72
- }
73
- case 'disable': {
74
- if (!name) return fail(new Error("'mod disable' requires a module name"));
75
- const r = manager.disable(name);
76
- out(useJson ? r : `Module '${name}' disabled.`);
77
- break;
78
- }
79
- case 'remove': {
80
- if (!name) return fail(new Error("'mod remove' requires a module name"));
81
- const r = manager.remove(name);
82
- out(useJson ? r : `Module '${name}' removed.`);
83
- break;
84
- }
85
- case 'list':
86
- case 'ls': {
87
- const list = manager.list();
88
- out(useJson ? list : printModules(list));
89
- break;
90
- }
91
- case undefined:
92
- return fail(new Error("Missing action. Usage: jsql mod <add|enable|disable|remove|list>"));
93
- default:
94
- return fail(new Error(`Unknown mod action: ${action}`));
95
- }
96
- } catch (e) {
97
- fail(e);
98
- }
99
- })
100
- .command('export', 'Export a table to JSON or CSV', (sub) => {
101
- sub.option('db', { type: 'string', description: 'Database file path (default ./db.json)' });
102
- sub.option('json', { type: 'boolean', description: 'Output metadata as JSON' });
103
- }, async (argv) => {
104
- const table = argv._[0];
105
- const outFile = argv._[1];
106
- const fail = (e) => { console.error(`Error: ${e.message}`); process.exitCode = 1; };
107
- try {
108
- if (!table || !outFile) throw new Error('Usage: jsql export <table> <file.json|csv> [--db <path>]');
109
- const db = new Database(argv.db || './db.json', { autoSave: false });
110
- await db.loadDatabase ? db.loadDatabase() : null;
111
- const bytes = await migrate.exportToFile(db, table, outFile);
112
- if (argv.json) console.log(JSON.stringify({ ok: true, table, file: outFile, bytes }));
113
- else console.log(`Exported '${table}' (${bytes} bytes) to ${outFile}`);
114
- } catch (e) { fail(e); }
115
- })
116
- .command('import', 'Import a mysqldump (.sql), JSON (.json) or CSV (.csv) file', (sub) => {
117
- sub.option('db', { type: 'string', description: 'Database file path (default ./db.json)' });
118
- sub.option('table', { type: 'string', description: 'Target table for CSV import' });
119
- sub.option('strict', { type: 'boolean', description: 'Abort on first import error' });
120
- sub.option('json', { type: 'boolean', description: 'Output metadata as JSON' });
121
- }, async (argv) => {
122
- const file = argv._[0];
123
- const fail = (e) => { console.error(`Error: ${e.message}`); process.exitCode = 1; };
124
- try {
125
- if (!file) throw new Error('Usage: jsql import <file.sql|.json|.csv> [--db <path>] [--table <name>]');
126
- const db = new Database(argv.db || './db.json', { autoSave: false });
127
- await db.loadDatabase ? db.loadDatabase() : null;
128
- const ext = path.extname(file).toLowerCase();
129
- let result;
130
- if (ext === '.sql') {
131
- result = await migrate.importDumpFile(db, file, { strict: !!argv.strict });
132
- } else if (ext === '.json') {
133
- const data = JSON.parse(fs.readFileSync(file, 'utf8'));
134
- result = await migrate.importFromJSON(db, data);
135
- } else if (ext === '.csv') {
136
- if (!argv.table) throw new Error('CSV import requires --table <name>');
137
- const schemaFile = file.replace(/\.csv$/i, '.schema.json');
138
- const schema = fs.existsSync(schemaFile) ? JSON.parse(fs.readFileSync(schemaFile, 'utf8')) : null;
139
- result = await migrate.importFromCSV(db, argv.table, fs.readFileSync(file, 'utf8'), { schema });
140
- } else {
141
- throw new Error('Unsupported file type: ' + ext);
142
- }
143
- if (argv.json) {
144
- console.log(JSON.stringify({ ok: true, ...result }));
145
- } else {
146
- const errs = result.errors && result.errors.length;
147
- console.log(`Imported: ${result.created ? result.created.length + ' table(s), ' : ''}${result.inserted || 0} row(s)` + (errs ? `, ${errs} error(s)` : ''));
148
- }
149
- } catch (e) { fail(e); }
150
- })
151
- .command('serve', 'Run the MySQL-compatible server in the foreground', (sub) => {
152
- sub.option('port', { alias: ['p'], type: 'number', description: 'Listen port (default 3306)' });
153
- sub.option('host', { type: 'string', description: 'Listen host (default 127.0.0.1)' });
154
- sub.option('data-dir', { type: 'string', description: 'Directory to store databases' });
155
- sub.option('no-auth', { type: 'boolean', description: 'Allow connections without authentication' });
156
- sub.option('log', { type: 'boolean', description: 'Log queries to stdout' });
157
- }, async (argv) => {
158
- const { createMysqlServer } = require('../lib/mysql_server');
159
- const options = {
160
- port: argv.port != null ? argv.port : 3306,
161
- host: argv.host || '127.0.0.1',
162
- dataDir: argv['data-dir'],
163
- noAuth: argv['no-auth'] === true,
164
- };
165
- if (argv.log) options.onQuery = (q) => console.log('[sql]', q);
166
- const srv = createMysqlServer(options);
167
- srv.listen();
168
- console.log(`jsql-neo MySQL server on ${options.host}:${options.port}${options.dataDir ? ' (data: ' + options.dataDir + ')' : ' (in-memory)'}`);
169
- setInterval(() => {}, 1 << 30);
170
- })
171
- .command('bench', 'Insert + query benchmark against a data directory', (sub) => {
172
- sub.option('n', { type: 'number', description: 'Number of rows (default 10000)' });
173
- sub.option('data-dir', { type: 'string', description: 'Data directory (default :memory:)' });
174
- sub.option('json', { type: 'boolean', description: 'Output as JSON' });
175
- }, async (argv) => {
176
- const N = argv.n || 10000;
177
- const dataDir = argv['data-dir'] || ':memory:';
178
- try {
179
- const db = new Database(dataDir === ':memory:' ? ':memory:' : path.join(dataDir, 'bench.json'), { autoSave: false });
180
- await db.createTable('bench', { id: { type: 'integer', primaryKey: true, autoIncrement: true }, name: { type: 'string' }, score: { type: 'float' } });
181
- let t0 = Date.now();
182
- const batch = [];
183
- for (let i = 0; i < N; i++) batch.push({ name: 'user_' + i, score: Math.random() * 1000 });
184
- for (let i = 0; i < batch.length; i += 1000) await db.insert('bench', batch.slice(i, i + 1000));
185
- const insertMs = Date.now() - t0;
186
- t0 = Date.now();
187
- let hits = 0;
188
- for (let i = 0; i < 100; i++) hits += (await db.find('bench', { score: { $gt: 500 } }, { limit: 10 })).length;
189
- const queryMs = Date.now() - t0;
190
- const total = await db.count('bench');
191
- const out = { ok: true, rows: N, inserted: total, insertMs, insertPerSec: Math.round(N / (insertMs / 1000)), queryMs, queryCount: 100 };
192
- if (argv.json) console.log(JSON.stringify(out));
193
- else console.log(`Inserted ${total} rows in ${insertMs}ms (${out.insertPerSec}/s), 100 queries in ${queryMs}ms`);
194
- } catch (e) { console.error(`Error: ${e.message}`); process.exitCode = 1; }
195
- })
196
- .command('ui', 'Serve the built-in web management console', (sub) => {
197
- sub.option('port', { alias: ['p'], type: 'number', description: 'HTTP port (default 8080)' });
198
- sub.option('host', { type: 'string', description: 'Listen host (default 127.0.0.1)' });
199
- sub.option('data-dir', { type: 'string', description: 'Directory containing *.json databases (default .)' });
200
- sub.option('auth-token', { type: 'string', description: 'Require this Bearer token for /api/*' });
201
- sub.option('readonly', { type: 'boolean', description: 'Never write back to disk' });
202
- }, async (argv) => {
203
- const { WebUI } = require('../lib/web_ui');
204
- const ui = new WebUI({
205
- port: argv.port != null ? argv.port : 8080,
206
- host: argv.host,
207
- dataDir: argv['data-dir'] || '.',
208
- authToken: argv['auth-token'],
209
- readonly: argv.readonly === true,
210
- });
211
- try {
212
- const port = await ui.start();
213
- const host = argv.host || ui.host;
214
- console.log(`JSQL-NEO web UI on http://${host}:${port} (data: ${ui.dataDir})${ui.authToken ? ' [auth]' : ''}`);
215
- setInterval(() => {}, 1 << 30);
216
- } catch (e) { console.error(`Error: ${e.message}`); process.exitCode = 1; }
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
- })
231
- .command('redis', 'Run the Redis-compatible server', (sub) => {
232
- sub.option('port', { alias: ['p'], type: 'number', description: 'Listen port (default 6379)' });
233
- sub.option('host', { type: 'string', description: 'Listen host (default 127.0.0.1)' });
234
- sub.option('data-dir', { type: 'string', description: 'Directory for data.rdb.json snapshot' });
235
- sub.option('auth', { type: 'string', description: 'Require this password for AUTH' });
236
- sub.option('log', { type: 'boolean', description: 'Log commands to stdout' });
237
- }, (argv) => {
238
- const { createRedisServer } = require('../lib/redis_server');
239
- const srv = createRedisServer({
240
- port: argv.port != null ? argv.port : 6379,
241
- host: argv.host || '127.0.0.1',
242
- dataDir: argv['data-dir'],
243
- password: argv.auth || null,
244
- onQuery: argv.log ? (c) => console.log('[redis]', c.join(' ')) : null,
245
- });
246
- srv.listen();
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
- setInterval(() => {}, 1 << 30);
249
- })
250
- .command('version', 'Show version', null, () => {
251
- console.log(require('../package.json').version);
252
- })
253
- .command('server', 'Run the MySQL-compatible server in background', (sub) => {
254
- sub.option('port', { alias: ['p'], type: 'number', description: 'Listen port (default 3306)' });
255
- sub.option('host', { type: 'string', description: 'Listen host (default 127.0.0.1)' });
256
- sub.option('data-dir', { type: 'string', description: 'Directory to store databases (default in-memory)' });
257
- sub.option('auth', { alias: ['a'], type: 'string', description: 'User credential: user:password[:db1,db2] (repeatable)' });
258
- sub.option('config', { type: 'string', description: 'Path to config file (JSON)' });
259
- sub.option('no-auth', { type: 'boolean', description: 'Allow connections without authentication (local dev only)' });
260
- }, (argv) => {
261
- const { serverControl } = require('./jsql-server-control');
262
- const action = argv._[0];
263
- const fail = (err) => {
264
- console.error(`Error: ${err.message}`);
265
- process.exitCode = 1;
266
- };
267
- const options = {};
268
- if (argv.config) options.config = argv.config;
269
- if (argv.port != null) options.port = argv.port;
270
- if (argv.host != null) options.host = argv.host;
271
- if (argv['data-dir'] != null) options.dataDir = argv['data-dir'];
272
- if (argv['no-auth'] === true) options.noAuth = true;
273
- const authSpecs = [];
274
- for (let i = 0; i < RAW_ARGV.length; i++) {
275
- const a = RAW_ARGV[i];
276
- if (a === '--auth' || a === '-a') {
277
- if (RAW_ARGV[i + 1]) authSpecs.push(RAW_ARGV[++i]);
278
- } else if (a.startsWith('--auth=')) {
279
- authSpecs.push(a.slice(7));
280
- }
281
- }
282
- if (authSpecs.length > 0) options.auth = authSpecs;
283
-
284
- try {
285
- switch (action) {
286
- case 'start': {
287
- serverControl.start(options).then(
288
- (msg) => console.log(msg),
289
- (err) => fail(err)
290
- );
291
- break;
292
- }
293
- case 'stop': {
294
- serverControl.stop(options).then(
295
- (msg) => console.log(msg),
296
- (err) => fail(err)
297
- );
298
- break;
299
- }
300
- case 'status': {
301
- serverControl.status(options).then(
302
- (msg) => console.log(msg),
303
- (err) => fail(err)
304
- );
305
- break;
306
- }
307
- case undefined:
308
- return fail(new Error("Missing action. Usage: jsql server <start|stop|status>"));
309
- default:
310
- return fail(new Error(`Unknown server action: ${action}`));
311
- }
312
- } catch (e) {
313
- fail(e);
314
- }
315
- });
316
-
317
- cli.run(process.argv.slice(2)).catch(e => {
318
- console.error(`Error: ${e.message}`);
319
- process.exitCode = 1;
320
- });
1
+ #!/usr/bin/env node
2
+ const path = require('path');
3
+ const fs = require('fs');
4
+ const yaggs = require('@vexify-org/yaggs');
5
+ const { ModuleManager } = require('../lib/mod');
6
+ const Database = require('../lib/database');
7
+ const migrate = require('../lib/migrate');
8
+
9
+ function printModules(list) {
10
+ if (list.length === 0) {
11
+ console.log('No modules registered.');
12
+ return;
13
+ }
14
+ const rows = list.map(m =>
15
+ `${m.enabled ? 'ON ' : 'OFF'} ${m.name.padEnd(24)} ${m.path}`
16
+ );
17
+ console.log(rows.join('\n'));
18
+ }
19
+
20
+ const manager = new ModuleManager();
21
+
22
+ const RAW_ARGV = process.argv.slice(2);
23
+
24
+ const cli = yaggs({ pkg: require('../package.json') })
25
+ .usage('jsql <command> [options]')
26
+ .option('help', { alias: ['h'], type: 'boolean', description: 'Show help' })
27
+ .command('mod', 'Module management', (sub) => {
28
+ sub.option('address', {
29
+ alias: ['a'],
30
+ type: 'string',
31
+ description: 'Path to module file'
32
+ });
33
+ sub.option('json', {
34
+ type: 'boolean',
35
+ description: 'Output as JSON'
36
+ });
37
+ }, (argv) => {
38
+ const action = argv._[0];
39
+ const name = argv._[1];
40
+ const useJson = !!argv.json;
41
+
42
+ const fail = (err) => {
43
+ console.error(`Error: ${err.message}`);
44
+ process.exitCode = 1;
45
+ };
46
+ const out = (obj) => {
47
+ if (obj === undefined) return;
48
+ if (useJson) {
49
+ console.log(JSON.stringify(obj, null, 2));
50
+ } else {
51
+ console.log(obj);
52
+ }
53
+ };
54
+
55
+ try {
56
+ switch (action) {
57
+ case 'add': {
58
+ if (!argv.address) return fail(new Error("'mod add' requires --address <path>"));
59
+ const ext = path.extname(argv.address).toLowerCase();
60
+ if (ext === '.js' || ext === '.cjs' || ext === '.mjs') {
61
+ console.warn("Warning: JS modules run without sandbox isolation (trust model). Use .json files for data-only plugins.");
62
+ }
63
+ const entry = manager.add(argv.address);
64
+ out(useJson ? entry : `Module '${entry.name}' added (${entry.path}). Use 'jsql mod enable ${entry.name}' to enable.`);
65
+ break;
66
+ }
67
+ case 'enable': {
68
+ if (!name) return fail(new Error("'mod enable' requires a module name"));
69
+ const r = manager.enable(name);
70
+ out(useJson ? r : `Module '${name}' enabled.`);
71
+ break;
72
+ }
73
+ case 'disable': {
74
+ if (!name) return fail(new Error("'mod disable' requires a module name"));
75
+ const r = manager.disable(name);
76
+ out(useJson ? r : `Module '${name}' disabled.`);
77
+ break;
78
+ }
79
+ case 'remove': {
80
+ if (!name) return fail(new Error("'mod remove' requires a module name"));
81
+ const r = manager.remove(name);
82
+ out(useJson ? r : `Module '${name}' removed.`);
83
+ break;
84
+ }
85
+ case 'list':
86
+ case 'ls': {
87
+ const list = manager.list();
88
+ out(useJson ? list : printModules(list));
89
+ break;
90
+ }
91
+ case undefined:
92
+ return fail(new Error("Missing action. Usage: jsql mod <add|enable|disable|remove|list>"));
93
+ default:
94
+ return fail(new Error(`Unknown mod action: ${action}`));
95
+ }
96
+ } catch (e) {
97
+ fail(e);
98
+ }
99
+ })
100
+ .command('export', 'Export a table to JSON or CSV', (sub) => {
101
+ sub.option('db', { type: 'string', description: 'Database file path (default ./db.json)' });
102
+ sub.option('json', { type: 'boolean', description: 'Output metadata as JSON' });
103
+ }, async (argv) => {
104
+ const table = argv._[0];
105
+ const outFile = argv._[1];
106
+ const fail = (e) => { console.error(`Error: ${e.message}`); process.exitCode = 1; };
107
+ try {
108
+ if (!table || !outFile) throw new Error('Usage: jsql export <table> <file.json|csv> [--db <path>]');
109
+ const db = new Database(argv.db || './db.json', { autoSave: false });
110
+ await db.loadDatabase ? db.loadDatabase() : null;
111
+ const bytes = await migrate.exportToFile(db, table, outFile);
112
+ if (argv.json) console.log(JSON.stringify({ ok: true, table, file: outFile, bytes }));
113
+ else console.log(`Exported '${table}' (${bytes} bytes) to ${outFile}`);
114
+ } catch (e) { fail(e); }
115
+ })
116
+ .command('import', 'Import a mysqldump (.sql), JSON (.json) or CSV (.csv) file', (sub) => {
117
+ sub.option('db', { type: 'string', description: 'Database file path (default ./db.json)' });
118
+ sub.option('table', { type: 'string', description: 'Target table for CSV import' });
119
+ sub.option('strict', { type: 'boolean', description: 'Abort on first import error' });
120
+ sub.option('json', { type: 'boolean', description: 'Output metadata as JSON' });
121
+ }, async (argv) => {
122
+ const file = argv._[0];
123
+ const fail = (e) => { console.error(`Error: ${e.message}`); process.exitCode = 1; };
124
+ try {
125
+ if (!file) throw new Error('Usage: jsql import <file.sql|.json|.csv> [--db <path>] [--table <name>]');
126
+ const db = new Database(argv.db || './db.json', { autoSave: false });
127
+ await db.loadDatabase ? db.loadDatabase() : null;
128
+ const ext = path.extname(file).toLowerCase();
129
+ let result;
130
+ if (ext === '.sql') {
131
+ result = await migrate.importDumpFile(db, file, { strict: !!argv.strict });
132
+ } else if (ext === '.json') {
133
+ const data = JSON.parse(fs.readFileSync(file, 'utf8'));
134
+ result = await migrate.importFromJSON(db, data);
135
+ } else if (ext === '.csv') {
136
+ if (!argv.table) throw new Error('CSV import requires --table <name>');
137
+ const schemaFile = file.replace(/\.csv$/i, '.schema.json');
138
+ const schema = fs.existsSync(schemaFile) ? JSON.parse(fs.readFileSync(schemaFile, 'utf8')) : null;
139
+ result = await migrate.importFromCSV(db, argv.table, fs.readFileSync(file, 'utf8'), { schema });
140
+ } else {
141
+ throw new Error('Unsupported file type: ' + ext);
142
+ }
143
+ if (argv.json) {
144
+ console.log(JSON.stringify({ ok: true, ...result }));
145
+ } else {
146
+ const errs = result.errors && result.errors.length;
147
+ console.log(`Imported: ${result.created ? result.created.length + ' table(s), ' : ''}${result.inserted || 0} row(s)` + (errs ? `, ${errs} error(s)` : ''));
148
+ }
149
+ } catch (e) { fail(e); }
150
+ })
151
+ .command('serve', 'Run the MySQL-compatible server in the foreground', (sub) => {
152
+ sub.option('port', { alias: ['p'], type: 'number', description: 'Listen port (default 3306)' });
153
+ sub.option('host', { type: 'string', description: 'Listen host (default 127.0.0.1)' });
154
+ sub.option('data-dir', { type: 'string', description: 'Directory to store databases' });
155
+ sub.option('no-auth', { type: 'boolean', description: 'Allow connections without authentication' });
156
+ sub.option('log', { type: 'boolean', description: 'Log queries to stdout' });
157
+ }, async (argv) => {
158
+ const { createMysqlServer } = require('../lib/mysql_server');
159
+ const options = {
160
+ port: argv.port != null ? argv.port : 3306,
161
+ host: argv.host || '127.0.0.1',
162
+ dataDir: argv['data-dir'],
163
+ noAuth: argv['no-auth'] === true,
164
+ };
165
+ if (argv.log) options.onQuery = (q) => console.log('[sql]', q);
166
+ const srv = createMysqlServer(options);
167
+ srv.listen();
168
+ console.log(`jsql-neo MySQL server on ${options.host}:${options.port}${options.dataDir ? ' (data: ' + options.dataDir + ')' : ' (in-memory)'}`);
169
+ setInterval(() => {}, 1 << 30);
170
+ })
171
+ .command('bench', 'Insert + query benchmark against a data directory', (sub) => {
172
+ sub.option('n', { type: 'number', description: 'Number of rows (default 10000)' });
173
+ sub.option('data-dir', { type: 'string', description: 'Data directory (default :memory:)' });
174
+ sub.option('json', { type: 'boolean', description: 'Output as JSON' });
175
+ }, async (argv) => {
176
+ const N = argv.n || 10000;
177
+ const dataDir = argv['data-dir'] || ':memory:';
178
+ try {
179
+ const db = new Database(dataDir === ':memory:' ? ':memory:' : path.join(dataDir, 'bench.json'), { autoSave: false });
180
+ await db.createTable('bench', { id: { type: 'integer', primaryKey: true, autoIncrement: true }, name: { type: 'string' }, score: { type: 'float' } });
181
+ let t0 = Date.now();
182
+ const batch = [];
183
+ for (let i = 0; i < N; i++) batch.push({ name: 'user_' + i, score: Math.random() * 1000 });
184
+ for (let i = 0; i < batch.length; i += 1000) await db.insert('bench', batch.slice(i, i + 1000));
185
+ const insertMs = Date.now() - t0;
186
+ t0 = Date.now();
187
+ let hits = 0;
188
+ for (let i = 0; i < 100; i++) hits += (await db.find('bench', { score: { $gt: 500 } }, { limit: 10 })).length;
189
+ const queryMs = Date.now() - t0;
190
+ const total = await db.count('bench');
191
+ const out = { ok: true, rows: N, inserted: total, insertMs, insertPerSec: Math.round(N / (insertMs / 1000)), queryMs, queryCount: 100 };
192
+ if (argv.json) console.log(JSON.stringify(out));
193
+ else console.log(`Inserted ${total} rows in ${insertMs}ms (${out.insertPerSec}/s), 100 queries in ${queryMs}ms`);
194
+ } catch (e) { console.error(`Error: ${e.message}`); process.exitCode = 1; }
195
+ })
196
+ .command('ui', 'Serve the built-in web management console', (sub) => {
197
+ sub.option('port', { alias: ['p'], type: 'number', description: 'HTTP port (default 8080)' });
198
+ sub.option('host', { type: 'string', description: 'Listen host (default 127.0.0.1)' });
199
+ sub.option('data-dir', { type: 'string', description: 'Directory containing *.json databases (default .)' });
200
+ sub.option('auth-token', { type: 'string', description: 'Require this Bearer token for /api/*' });
201
+ sub.option('readonly', { type: 'boolean', description: 'Never write back to disk' });
202
+ }, async (argv) => {
203
+ const { WebUI } = require('../lib/web_ui');
204
+ const ui = new WebUI({
205
+ port: argv.port != null ? argv.port : 8080,
206
+ host: argv.host,
207
+ dataDir: argv['data-dir'] || '.',
208
+ authToken: argv['auth-token'],
209
+ readonly: argv.readonly === true,
210
+ });
211
+ try {
212
+ const port = await ui.start();
213
+ const host = argv.host || ui.host;
214
+ console.log(`JSQL-NEO web UI on http://${host}:${port} (data: ${ui.dataDir})${ui.authToken ? ' [auth]' : ''}`);
215
+ setInterval(() => {}, 1 << 30);
216
+ } catch (e) { console.error(`Error: ${e.message}`); process.exitCode = 1; }
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
+ })
231
+ .command('redis', 'Run the Redis-compatible server', (sub) => {
232
+ sub.option('port', { alias: ['p'], type: 'number', description: 'Listen port (default 6379)' });
233
+ sub.option('host', { type: 'string', description: 'Listen host (default 127.0.0.1)' });
234
+ sub.option('data-dir', { type: 'string', description: 'Directory for data.rdb.json snapshot' });
235
+ sub.option('auth', { type: 'string', description: 'Require this password for AUTH' });
236
+ sub.option('log', { type: 'boolean', description: 'Log commands to stdout' });
237
+ }, (argv) => {
238
+ const { createRedisServer } = require('../lib/redis_server');
239
+ const srv = createRedisServer({
240
+ port: argv.port != null ? argv.port : 6379,
241
+ host: argv.host || '127.0.0.1',
242
+ dataDir: argv['data-dir'],
243
+ password: argv.auth || null,
244
+ onQuery: argv.log ? (c) => console.log('[redis]', c.join(' ')) : null,
245
+ });
246
+ srv.listen();
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
+ setInterval(() => {}, 1 << 30);
249
+ })
250
+ .command('version', 'Show version', null, () => {
251
+ console.log(require('../package.json').version);
252
+ })
253
+ .command('server', 'Run the MySQL-compatible server in background', (sub) => {
254
+ sub.option('port', { alias: ['p'], type: 'number', description: 'Listen port (default 3306)' });
255
+ sub.option('host', { type: 'string', description: 'Listen host (default 127.0.0.1)' });
256
+ sub.option('data-dir', { type: 'string', description: 'Directory to store databases (default in-memory)' });
257
+ sub.option('auth', { alias: ['a'], type: 'string', description: 'User credential: user:password[:db1,db2] (repeatable)' });
258
+ sub.option('config', { type: 'string', description: 'Path to config file (JSON)' });
259
+ sub.option('no-auth', { type: 'boolean', description: 'Allow connections without authentication (local dev only)' });
260
+ }, (argv) => {
261
+ const { serverControl } = require('./jsql-server-control');
262
+ const action = argv._[0];
263
+ const fail = (err) => {
264
+ console.error(`Error: ${err.message}`);
265
+ process.exitCode = 1;
266
+ };
267
+ const options = {};
268
+ if (argv.config) options.config = argv.config;
269
+ if (argv.port != null) options.port = argv.port;
270
+ if (argv.host != null) options.host = argv.host;
271
+ if (argv['data-dir'] != null) options.dataDir = argv['data-dir'];
272
+ if (argv['no-auth'] === true) options.noAuth = true;
273
+ const authSpecs = [];
274
+ for (let i = 0; i < RAW_ARGV.length; i++) {
275
+ const a = RAW_ARGV[i];
276
+ if (a === '--auth' || a === '-a') {
277
+ if (RAW_ARGV[i + 1]) authSpecs.push(RAW_ARGV[++i]);
278
+ } else if (a.startsWith('--auth=')) {
279
+ authSpecs.push(a.slice(7));
280
+ }
281
+ }
282
+ if (authSpecs.length > 0) options.auth = authSpecs;
283
+
284
+ try {
285
+ switch (action) {
286
+ case 'start': {
287
+ serverControl.start(options).then(
288
+ (msg) => console.log(msg),
289
+ (err) => fail(err)
290
+ );
291
+ break;
292
+ }
293
+ case 'stop': {
294
+ serverControl.stop(options).then(
295
+ (msg) => console.log(msg),
296
+ (err) => fail(err)
297
+ );
298
+ break;
299
+ }
300
+ case 'status': {
301
+ serverControl.status(options).then(
302
+ (msg) => console.log(msg),
303
+ (err) => fail(err)
304
+ );
305
+ break;
306
+ }
307
+ case undefined:
308
+ return fail(new Error("Missing action. Usage: jsql server <start|stop|status>"));
309
+ default:
310
+ return fail(new Error(`Unknown server action: ${action}`));
311
+ }
312
+ } catch (e) {
313
+ fail(e);
314
+ }
315
+ });
316
+
317
+ cli.run(process.argv.slice(2)).catch(e => {
318
+ console.error(`Error: ${e.message}`);
319
+ process.exitCode = 1;
320
+ });