jsql-neo 5.4.2 → 5.5.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 (72) hide show
  1. package/LICENSE +202 -202
  2. package/README.md +8097 -7971
  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 +385 -357
  7. package/index.js +124 -118
  8. package/lib/ast.js +214 -0
  9. package/lib/btree.js +413 -413
  10. package/lib/cache.js +328 -328
  11. package/lib/client.js +125 -125
  12. package/lib/database.js +1744 -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 +732 -732
  23. package/lib/nedb_compat.js +506 -506
  24. package/lib/pg_server.js +864 -864
  25. package/lib/plugin.js +34 -34
  26. package/lib/query.js +815 -815
  27. package/lib/redis_server.js +571 -571
  28. package/lib/sql.js +3460 -2925
  29. package/lib/sqlite_compat.js +323 -323
  30. package/lib/sqlite_worker.js +264 -264
  31. package/lib/table.js +1304 -1304
  32. package/lib/tui.js +502 -502
  33. package/lib/wasm.js +1 -1
  34. package/lib/wasm_client.js +718 -718
  35. package/lib/web_ui.js +257 -257
  36. package/native/jsql-neo-native.node +0 -0
  37. package/nativesrc/jsql-neo-core/Cargo.lock +455 -455
  38. package/nativesrc/jsql-neo-core/Cargo.toml +24 -24
  39. package/nativesrc/jsql-neo-core/src/engine/hybrid.rs +455 -455
  40. package/nativesrc/jsql-neo-core/src/engine/memory.rs +168 -168
  41. package/nativesrc/jsql-neo-core/src/engine/mod.rs +62 -62
  42. package/nativesrc/jsql-neo-core/src/engine/table.rs +700 -700
  43. package/nativesrc/jsql-neo-core/src/lib.rs +2 -2
  44. package/nativesrc/jsql-neo-core/src/storage/mod.rs +2 -2
  45. package/nativesrc/jsql-neo-core/src/storage/persistent.rs +1 -1
  46. package/nativesrc/jsql-neo-core/src/storage/wal.rs +85 -85
  47. package/nativesrc/jsql-neo-core/src/types.rs +93 -93
  48. package/nativesrc/jsql-neo-native/Cargo.lock +606 -606
  49. package/nativesrc/jsql-neo-native/Cargo.toml +16 -16
  50. package/nativesrc/jsql-neo-native/build.rs +3 -3
  51. package/nativesrc/jsql-neo-native/package.json +7 -7
  52. package/nativesrc/jsql-neo-native/src/lib.rs +269 -269
  53. package/nativesrc/jsql-neo-wasm/Cargo.lock +465 -465
  54. package/nativesrc/jsql-neo-wasm/Cargo.toml +15 -15
  55. package/nativesrc/jsql-neo-wasm/src/lib.rs +447 -447
  56. package/package.json +87 -86
  57. package/postinstall.js +158 -158
  58. package/test/btree.test.js +128 -128
  59. package/test/coverage.js +187 -187
  60. package/test/join.test.js +110 -110
  61. package/test/native.test.js +158 -158
  62. package/test/regress-5.1.0.js +271 -271
  63. package/test/smoke.js +58 -58
  64. package/test/sql-parser.test.js +273 -0
  65. package/test/wasm.test.js +110 -110
  66. package/wasm/browser.d.ts +89 -89
  67. package/wasm/browser.mjs +569 -569
  68. package/wasm/browser_bg.mjs +597 -597
  69. package/wasm/jsql_neo_wasm.d.ts +38 -38
  70. package/wasm/jsql_neo_wasm.js +520 -520
  71. package/wasm/jsql_neo_wasm_bg.wasm.d.ts +26 -26
  72. 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
+ });