jsql-neo 4.3.0 → 4.4.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 CHANGED
@@ -1,165 +1,289 @@
1
- # JSQL-NEO v4.0.1
1
+ # JSQL-NEO
2
2
 
3
- Rust-powered embedded database with **three engines + SQL + Redis-style storage** in one npm package.
3
+ > **One engine to rule them all** a Rust-powered embedded database that speaks your language:
4
+ > MySQL protocol. Redis protocol. SQL. TypeScript. The browser. **And it fits in one npm package.**
4
5
 
5
- ## Engines
6
+ ![Engines](https://img.shields.io/badge/engines-Native%20%7C%20WASM%20%7C%20Pure%20JS-7ee787)
7
+ ![MySQL](https://img.shields.io/badge/protocol-MySQL%20compatible-1f6feb)
8
+ ![Redis](https://img.shields.io/badge/protocol-Redis%20RESP2-f03c15)
9
+ ![ZERO](https://img.shields.io/badge/dependencies-ZERO-8957e5)
10
+ ![WASM](https://img.shields.io/badge/runs%20in-Browser%20%28WASM%29-79c0ff)
11
+
12
+ ---
13
+
14
+ ## Why JSQL-NEO?
15
+
16
+ Most embedded databases make you choose: *native speed*, *portable WASM*, or *a familiar file format*.
17
+ JSQL-NEO gives you **all three in one install** — plus drop-in compatibility with the **two most popular
18
+ database protocols in the world**.
19
+
20
+ - ⚡ **Rust core** — N-API native addon, ~2× faster than better-sqlite3 (see [Benchmark](#benchmark))
21
+ - 🧩 **WASM build** — the *same engine* runs in Node.js **and any browser**, zero native deps
22
+ - 🐘 **MySQL protocol** — Sequelize, Knex, TypeORM, mysql2, phpMyAdmin … **just work**, no plugin
23
+ - 🐇 **Redis protocol** — ioredis, node-redis, redis-cli — strings, hashes, lists, sets, TTL, snapshots
24
+ - 🌐 **Built-in Web UI** — a zero-dependency management console ships with the package
25
+ - 🗃️ **Three storage modes** — memory-first, hybrid (LRU + async flush), and disk
26
+ - 📦 **Zero runtime dependencies** — the whole world is your `node_modules`
27
+ - 🏷️ **Typed** — full TypeScript declarations for every API surface
6
28
 
7
- | Engine | Entry | Use Case |
8
- |--------|-------|----------|
9
- | **Native** (Rust → N-API, fastest) | `NativeJSQL` | Node.js, best performance |
10
- | **WASM** (Rust wasm-pack, zero native deps) | `JSQL` | Node.js / browsers, no native addon |
11
- | **Pure JS** (local JSON file, SQLite-like) | `Database` | Local file persistence, legacy API |
29
+ ```
30
+ ┌─────────────────────────── JSQL-NEO ───────────────────────────┐
31
+ │ │
32
+ Node.js ──┤ Native (Rust N-API) Fastest path, zero deps
33
+ Node.js ──┤ WASM (Rust wasm) Portable, no native addon │
34
+ Browser ──┤ WASM (+ IndexedDB) Full SQL engine in your browser │
35
+ Anywhere ─┤ Pure JS (JSON file) SQLite-like local persistence │
36
+ │ │
37
+ ├── speak MySQL ──────────► Sequelize / Knex / TypeORM / mysql2 │
38
+ ├── speak Redis ──────────► ioredis / node-redis / redis-cli │
39
+ ├── speak HTTP ───────────► built-in Web UI + management APIs │
40
+ └── speak SQL ────────────► CREATE / SELECT / JOIN / aggregates │
41
+
42
+ ```
12
43
 
13
- All engines share the same API (`createTable` / `insert` / `findById` / `find` / `updateById` / `removeById` / `dropTable`) plus `executeSQL()`.
44
+ ## 30-second Quick Start
14
45
 
15
- ## SQL
46
+ ```bash
47
+ npm install jsql-neo
48
+ ```
16
49
 
17
50
  ```js
18
51
  const jsql = require('jsql-neo');
19
- const db = new jsql.NativeJSQL();
52
+ const db = new jsql.NativeJSQL(); // fastest engine
20
53
  await db.start();
21
54
 
22
55
  await jsql.executeSQL(db, 'CREATE TABLE users (id INTEGER PRIMARY KEY AUTO_INCREMENT, name STRING, age INTEGER)');
23
- await jsql.executeSQL(db, "INSERT INTO users (name, age) VALUES ('Alice', 30), ('Bob', 25)");
24
- await jsql.executeSQL(db, "INSERT INTO users VALUES (5, 'Carol', 35) ON DUPLICATE KEY UPDATE age = 30");
56
+ await jsql.executeSQL(db, "INSERT INTO users (name, age) VALUES ('Alice', 30), ('Bob', 25), ('Carol', 35)");
25
57
  const r = await jsql.executeSQL(db, 'SELECT name, age FROM users WHERE age > 26 ORDER BY age DESC');
26
- // rows: [["Carol",35],["Alice",30]]
58
+ // rows: [["Carol",35],["Alice",30]]
27
59
 
28
- await jsql.executeSQL(db, 'UPDATE users SET age = 31 WHERE id = 1');
29
- await jsql.executeSQL(db, 'DELETE FROM users WHERE id = 2');
30
60
  await db.stop();
31
61
  ```
32
62
 
33
- Supported: `CREATE/DROP TABLE`, `INSERT` (multi-row, `ON DUPLICATE KEY UPDATE`), `SELECT` (`WHERE`/`ORDER BY`/`LIMIT`/`OFFSET`/`GROUP BY`/`HAVING`/aggregates), `UPDATE`, `DELETE`, prepared statements with `?` placeholders.
63
+ Need a **MySQL server** instead?
64
+
65
+ ```bash
66
+ jsql serve -p 3306 --data-dir ./data
67
+ mysql -h 127.0.0.1 -P 3306 -u root # any MySQL client, now
68
+ ```
69
+
70
+ Need a **Redis server**?
71
+
72
+ ```bash
73
+ jsql redis -p 6379 --data-dir ./redis-data
74
+ redis-cli SET hello world
75
+ ```
76
+
77
+ Need a **web console**?
78
+
79
+ ```bash
80
+ jsql ui -p 8080 --data-dir ./data # open http://localhost:8080
81
+ ```
82
+
83
+ One package. One install. Five doors in.
84
+
85
+ ---
86
+
87
+ ## Engines
34
88
 
35
- ## Redis-Style Storage Modes
89
+ | Engine | Entry point | Speed | Where it runs | Best for |
90
+ |--------|-------------|-------|---------------|----------|
91
+ | **Native** | `NativeJSQL` | ⚡ fastest (Rust N-API) | Node.js | Production, hot paths |
92
+ | **WASM** | `JSQL` | fast (Rust → wasm) | Node.js **and browsers** | Portability, edge, playgrounds |
93
+ | **Pure JS** | `Database` | solid | Node.js | Local JSON files, zero-native deploys |
36
94
 
37
- Native and Pure JS engines support three persistence modes (Redis-compatible model: memory-first, async flush, LRU eviction, lazy reload):
95
+ All three share the same API `createTable` / `insert` / `findById` / `find` / `updateById` /
96
+ `removeById` / `dropTable` — plus a common `executeSQL()` SQL engine. **Write once, run anywhere.**
97
+
98
+ ### Storage modes (Native & Pure JS)
99
+
100
+ | Mode | Behavior |
101
+ |------|----------|
102
+ | `memory` | Pure in-memory, max speed, no path needed |
103
+ | `hybrid` | Memory-first, async incremental flush, cold tables LRU-evicted under memory pressure, lazy reload |
104
+ | `disk` | Fast flush (50ms), memory as read/write cache |
38
105
 
39
106
  ```js
40
107
  const db = new jsql.NativeJSQL({
41
- path: '/var/lib/jsql', // storage directory (required for hybrid/disk)
42
- mode: 'hybrid', // 'memory' (default) | 'hybrid' | 'disk'
43
- memReserveMB: 512, // keep 512MB RAM headroom before evicting
44
- flushInterval: 200, // ms between async flushes (hybrid 200 / disk 50)
45
- evictInterval: 1000, // ms between memory-pressure checks
108
+ path: '/var/lib/jsql',
109
+ mode: 'hybrid', // 'memory' | 'hybrid' | 'disk'
110
+ memReserveMB: 512, // RAM headroom before LRU eviction
111
+ flushInterval: 200, // async flush cadence (ms)
46
112
  });
47
- await db.start();
48
113
  ```
49
114
 
50
- - **`memory`** pure in-memory (default, no path needed)
51
- - **`hybrid`** — writes go to memory first, async incremental flush to disk; cold tables are LRU-evicted when memory pressure exceeds `total - memReserveMB`, and lazily reloaded on next access
52
- - **`disk`** — fast flush (50ms), memory acts as read/write cache
115
+ Atomic writes (tmp + rename), per-table files, and WAL + snapshot crash recovery on the server engine.
53
116
 
54
- Data is stored per-table as `<dir>/<table>.jsql.json` + `meta.json`; writes are atomic (tmp + rename).
117
+ ---
55
118
 
56
- ```js
57
- // Pure JS engine (same options)
58
- const db2 = new jsql.Database({ path: '/var/lib/jsql', mode: 'hybrid' });
119
+ ## The SQL Engine
120
+
121
+ A full SQL engine with prepared statements, joins, subqueries, and MySQL-compatible column naming:
122
+
123
+ ```sql
124
+ CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50), age INTEGER)
125
+ INSERT INTO users (name, age) VALUES ('Alice', 30), ('Bob', 25) ON DUPLICATE KEY UPDATE age = 30
126
+ SELECT name, age FROM users WHERE age > 26 ORDER BY age DESC LIMIT 10 OFFSET 5
127
+ SELECT COUNT(*), AVG(age) FROM users GROUP BY dept HAVING COUNT(*) > 2
128
+ UPDATE users SET age = 31 WHERE name = 'Bob'
129
+ DELETE FROM users WHERE id = 2
130
+ BEGIN / COMMIT / ROLLBACK
59
131
  ```
60
132
 
61
- ## Quick Start
133
+ Scalar functions: `VERSION()`, `NOW()`, `CONCAT()`, `IFNULL()`, `COALESCE()`, `UPPER()`, `LOWER()`,
134
+ `LENGTH()`, `ROUND()`, `LAST_INSERT_ID()`, `ROW_COUNT()`, `FOUND_ROWS()`, `CONNECTION_ID()`, `DATABASE()` …
135
+ System variables: `@@version`, `@@sql_mode`, `SET @@sql_mode = 'STRICT_TRANS_TABLES'` …
136
+
137
+ ---
138
+
139
+ ## Speak MySQL
62
140
 
63
- ### Native (fastest)
141
+ The MySQL wire-protocol server accepts **standard MySQL clients** — no plugins, no middleware.
142
+ Verified in CI against the real drivers used by three major ORMs:
143
+
144
+ | ORM | Version | Results |
145
+ |-----|---------|---------|
146
+ | [Sequelize](https://sequelize.org) | v6 | ✅ 10/10 — connect, authenticate, sync, create, bulkCreate, find, count, update, destroy, MAX() |
147
+ | [Knex](https://knexjs.org) | v3 | ✅ 9/9 — schema builder, insert, select, where + orderBy, count, update, delete, raw SQL |
148
+ | [TypeORM](https://typeorm.io) | v0.3 | ✅ 8/8 — initialize, synchronize, save, findOne, find, count, update, delete |
149
+
150
+ Also verified with `mysql2/promise` over the wire: prepared statements (`COM_STMT_PREPARE`/`EXECUTE`),
151
+ binary protocol result sets, `SHOW COLUMNS` / `SHOW INDEX` / `SHOW CREATE TABLE` / `SHOW VARIABLES` /
152
+ `SHOW GRANTS`, `information_schema`, `START TRANSACTION`, `TRUNCATE TABLE`, `SET` statements, and
153
+ MySQL DDL forms (`int unsigned`, `auto_increment`, `ENGINE=InnoDB`, `DEFAULT CHARSET`).
64
154
 
65
155
  ```js
66
- const { NativeJSQL } = require('jsql-neo');
67
- const db = new NativeJSQL();
68
- await db.start();
156
+ const { createMysqlServer } = require('jsql-neo');
157
+ createMysqlServer({ port: 3306, dataDir: './data', noAuth: true }).listen();
158
+ ```
69
159
 
70
- await db.createTable('users', {
71
- name: { type: 'string' },
72
- age: { type: 'integer' }
73
- });
74
- const [id] = await db.insert('users', { name: 'Alice', age: 30 });
75
- const user = await db.findById('users', id);
76
- // → { id: 1, fields: { name: 'Alice', age: 30 }, created_at: '...', updated_at: '...' }
77
- await db.stop();
160
+ ---
161
+
162
+ ## Speak Redis
163
+
164
+ A RESP2 server that plays perfectly with `ioredis`, `node-redis`, and `redis-cli`:
165
+
166
+ ```
167
+ PING ECHO SET GET SETNX DEL EXISTS KEYS TYPE EXPIRE TTL PERSIST
168
+ INCR DECR INCRBY DECRBY APPEND STRLEN
169
+ HSET HGET HGETALL HDEL HEXISTS HLEN HKEYS HVALS
170
+ LPUSH RPUSH LPOP RPOP LLEN LRANGE LINDEX LREM
171
+ SADD SREM SMEMBERS SISMEMBER SCARD
172
+ DBSIZE FLUSHALL FLUSHDB SELECT INFO AUTH QUIT
78
173
  ```
79
174
 
80
- ### Pure JS (local file)
175
+ Snapshot persistence to `data.rdb.json` — debounced writes (500ms) plus a guaranteed flush on shutdown.
81
176
 
82
177
  ```js
83
- const { Database } = require('jsql-neo');
84
- const db = new Database('/tmp/mydb.json'); // or { path, mode } for hybrid/disk
85
- const users = db.createTable('users', {
86
- id: { type: 'integer', autoIncrement: true, primaryKey: true },
87
- name: { type: 'string', length: 32 },
88
- age: { type: 'integer' }
89
- });
90
- const ids = users.insertMany([{ name: 'Alice', age: 30 }, { name: 'Bob', age: 25 }]);
91
- users.updateById(ids[0], { age: 31 });
92
- db.save();
178
+ const { createRedisServer } = require('jsql-neo');
179
+ createRedisServer({ port: 6379, dataDir: './redis-data' }).listen();
180
+ ```
181
+
182
+ ---
183
+
184
+ ## Toolbox — everything included
185
+
186
+ ### CLI (`jsql`)
187
+
188
+ | Command | What it does |
189
+ |---------|--------------|
190
+ | `jsql serve` | Run the MySQL-compatible server in the foreground |
191
+ | `jsql server start\|stop\|status` | Background daemon with pid control |
192
+ | `jsql redis` | Run the Redis-compatible server |
193
+ | `jsql ui` | Serve the built-in web management console |
194
+ | `jsql import <dump.sql\|.json\|.csv>` | Load a mysqldump, JSON, or CSV file |
195
+ | `jsql export <table> <file>` | Dump a table to JSON or CSV |
196
+ | `jsql bench` | Insert + query benchmark against a data dir |
197
+ | `jsql mod` | Plugin registry (enable / disable / list) |
198
+ | `jsql version` | Print version |
199
+
200
+ ### Web UI (`WebUI`)
201
+
202
+ A zero-dependency HTTP management console: browse databases and tables, run SQL in the browser,
203
+ see results as a table. Perfect for dev tools, admin panels, and demos.
204
+
205
+ ### Migration tools (`migrate`)
206
+
207
+ ```js
208
+ const { importDumpFile, exportToFile, importFromCSV, exportAllToJSON } = require('jsql-neo');
209
+ await importDumpFile(db, './backup.sql', { strict: true }); // real mysqldump format
210
+ await exportToFile(db, 'users', './users.csv'); // CSV round-trip
211
+ ```
212
+
213
+ ### Browser playground
214
+
215
+ `examples/playground/` is a self-contained SQL sandbox — the **entire engine runs in your browser**
216
+ (WASM + IndexedDB persistence, no server):
217
+
218
+ ```bash
219
+ cd examples/playground && npm install && npm run dev
220
+ ```
221
+
222
+ ### TypeScript
223
+
224
+ Full declarations ship with the package (`index.d.ts` + `wasm/browser.d.ts`), verified with
225
+ `tsc --strict`. Autocomplete your way through every engine, server, and tool.
226
+
227
+ ---
228
+
229
+ ## Benchmark
230
+
231
+ 100,000 rows — insert / point query / range query / count / update (Linux x64, Node 24):
232
+
233
+ | Engine | Insert/s | Point query (500×) | Range query (500×) | Total |
234
+ |--------|----------|--------------------|--------------------|-------|
235
+ | **Native (Rust N-API)** | 0.66M | 930ms | 685ms | **1.77s** 🏆 |
236
+ | better-sqlite3 (WAL) | 0.40M | 3258ms | 149ms | 3.66s |
237
+ | sql.js (WASM sqlite) | 0.30M | 5852ms | 366ms | 6.57s |
238
+ | Pure JS engine | 0.38M | 11278ms | 18138ms | 29.7s |
239
+
240
+ **~2× faster than better-sqlite3. ~17× faster than a pure-JS engine.** Reproduce it yourself:
241
+
242
+ ```bash
243
+ cd bench && npm install && npm run bench
93
244
  ```
94
245
 
95
- ## API
246
+ ---
247
+
248
+ ## API at a glance
96
249
 
97
- | Method | Native | WASM | Pure JS | Description |
98
- |--------|--------|------|---------|-------------|
99
- | `createTable(name, schema)` | ✅ | ✅ | ✅ | Define table with typed fields |
100
- | `insert(table, data)` | ✅ | ✅ | ✅ | Insert row(s), returns IDs |
101
- | `findById(table, id)` | ✅ | ✅ | ✅ | O(1) PK lookup |
102
- | `find(table, filter?)` | ✅ | ✅ | ✅ | Filtered query with B-Tree index |
103
- | `count(table)` | ✅ | ✅ | ✅ | Row count |
250
+ | Method | Native | WASM | Pure JS | Notes |
251
+ |--------|:------:|:----:|:-------:|-------|
252
+ | `createTable(name, schema)` | ✅ | ✅ | ✅ | Typed fields, indexes |
253
+ | `insert(table, data)` | ✅ | ✅ | ✅ | Batch supported, returns IDs |
254
+ | `findById(table, id)` | ✅ | ✅ | ✅ | O(1) primary-key hash lookup |
255
+ | `find(table, filter, opts)` | ✅ | ✅ | ✅ | Filter + B-Tree range scan, pagination |
256
+ | `count(table)` | ✅ | ✅ | ✅ | |
104
257
  | `updateById(table, id, data)` | ✅ | ✅ | ✅ | O(1) PK update |
105
258
  | `removeById(table, id)` | ✅ | ✅ | ✅ | O(1) PK delete |
106
- | `dropTable(name)` | ✅ | ✅ | ✅ | Remove table |
107
- | `executeSQL(db, sql, params?)` | ✅ | ✅ | ✅ | Run SQL statements |
108
-
109
- ## Schema Field Options
259
+ | `executeSQL(db, sql, params)` | ✅ | ✅ | ✅ | Full SQL engine, prepared statements |
110
260
 
111
261
  ```js
112
262
  {
113
263
  type: 'string' | 'integer' | 'float' | 'boolean',
114
- primaryKey: true, // PK field (auto-indexed)
115
- autoIncrement: true, // Auto-generate integer PK
116
- length: 32, // Max string length
117
- default: 'value', // Default value
118
- nullable: true // Allow null
264
+ primaryKey: true, // auto-indexed
265
+ autoIncrement: true, // integer PK generation
266
+ length: 32, // string max length
267
+ default: 'value',
268
+ nullable: true
119
269
  }
120
270
  ```
121
271
 
122
- ## ORM Compatibility
272
+ ---
123
273
 
124
- The MySQL-compatible server mode accepts standard MySQL clients, so popular Node.js ORMs work without a plugin. Tested against a live `createMysqlServer()` instance:
274
+ ## Testing
125
275
 
126
- | ORM | Version | Results |
127
- |-----|---------|---------|
128
- | [Sequelize](https://sequelize.org) | v6 | 10/10 — connect, authenticate, sync (CREATE TABLE), create, bulkCreate, find, count, update, destroy, MAX() |
129
- | [Knex](https://knexjs.org) | v3 | ✅ 9/9 — schema builder, insert, select, where + orderBy, count, update, delete, raw `SELECT VERSION()` |
130
- | [TypeORM](https://typeorm.io) | v0.3 | ✅ 8/8 — initialize, synchronize, save, findOne, find, count, update, delete |
131
-
132
- ```js
133
- // Sequelize
134
- const { Sequelize, DataTypes } = require('sequelize');
135
- const sequelize = new Sequelize('default', 'root', '', {
136
- host: '127.0.0.1', port: 33306, dialect: 'mysql',
137
- });
276
+ ```bash
277
+ npm test # zero-dependency SQL engine smoke suite
278
+ npm run test:orms # ORM compatibility (start examples/orms/start-server.js first)
279
+ ```
138
280
 
139
- // Knex
140
- const knex = require('knex')({
141
- client: 'mysql2',
142
- connection: { host: '127.0.0.1', port: 33306, user: 'root', database: 'default' },
143
- });
281
+ CI (`.github/workflows/ci.yml`): engine smoke tests on Node 18/20/22 + a full ORM compatibility job.
144
282
 
145
- // TypeORM
146
- const ds = new DataSource({
147
- type: 'mysql',
148
- host: '127.0.0.1', port: 33306, username: 'root', database: 'default',
149
- synchronize: true, entities: [User],
150
- });
151
- ```
283
+ ---
152
284
 
153
- Supported for ORM compatibility: prepared statements (`COM_STMT_PREPARE`/`EXECUTE`), binary protocol result sets, `SHOW COLUMNS` / `SHOW INDEX` / `SHOW CREATE TABLE` / `SHOW VARIABLES` / `SHOW GRANTS`, `information_schema` queries, `START TRANSACTION`, `TRUNCATE TABLE`, `SET` statements, scalar functions (`VERSION()`, `NOW()`, `CONCAT()`, `IFNULL()`, ...), and MySQL DDL forms (`int unsigned`, `auto_increment`, `ENGINE=InnoDB`, `DEFAULT CHARSET`).
285
+ ## License
154
286
 
155
- ## Features
287
+ MIT — use it, ship it, love it.
156
288
 
157
- - Three engines: Native (N-API Rust), WASM (wasm-pack Rust), Pure JS (local JSON)
158
- - SQL engine with prepared statements
159
- - Redis-style hybrid/disk storage: memory-first + async flush + LRU eviction + lazy reload
160
- - O(1) primary key hash index (`FxHashMap` / `Map`)
161
- - B-Tree indexing for range queries
162
- - WAL + snapshot crash recovery (server engine)
163
- - Batch insert / update / delete
164
- - Cursor-based pagination
165
- - Transaction support (server engine)
289
+ *JSQL-NEO: Rust-powered. Protocol-native. One package.*
package/bin/jsql CHANGED
@@ -1,6 +1,10 @@
1
1
  #!/usr/bin/env node
2
+ const path = require('path');
3
+ const fs = require('fs');
2
4
  const yaggs = require('@vexify-org/yaggs');
3
5
  const { ModuleManager } = require('../lib/mod');
6
+ const Database = require('../lib/database');
7
+ const migrate = require('../lib/migrate');
4
8
 
5
9
  function printModules(list) {
6
10
  if (list.length === 0) {
@@ -93,6 +97,140 @@ const cli = yaggs()
93
97
  fail(e);
94
98
  }
95
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 0.0.0.0)' });
199
+ sub.option('data-dir', { type: 'string', description: 'Directory containing *.json databases (default .)' });
200
+ sub.option('readonly', { type: 'boolean', description: 'Never write back to disk' });
201
+ }, async (argv) => {
202
+ const { WebUI } = require('../lib/web_ui');
203
+ const ui = new WebUI({
204
+ port: argv.port != null ? argv.port : 8080,
205
+ host: argv.host || '0.0.0.0',
206
+ dataDir: argv['data-dir'] || '.',
207
+ readonly: argv.readonly === true,
208
+ });
209
+ try {
210
+ const port = await ui.start();
211
+ console.log(`JSQL-NEO web UI on http://${argv.host || '0.0.0.0'}:${port} (data: ${ui.dataDir})`);
212
+ setInterval(() => {}, 1 << 30);
213
+ } catch (e) { console.error(`Error: ${e.message}`); process.exitCode = 1; }
214
+ })
215
+ .command('redis', 'Run the Redis-compatible server', (sub) => {
216
+ sub.option('port', { alias: 'p', type: 'number', description: 'Listen port (default 6379)' });
217
+ sub.option('host', { type: 'string', description: 'Listen host (default 127.0.0.1)' });
218
+ sub.option('data-dir', { type: 'string', description: 'Directory for data.rdb.json snapshot' });
219
+ sub.option('auth', { type: 'string', description: 'Require this password for AUTH' });
220
+ sub.option('log', { type: 'boolean', description: 'Log commands to stdout' });
221
+ }, (argv) => {
222
+ const { createRedisServer } = require('../lib/redis_server');
223
+ const srv = createRedisServer({
224
+ port: argv.port != null ? argv.port : 6379,
225
+ host: argv.host || '127.0.0.1',
226
+ dataDir: argv['data-dir'],
227
+ password: argv.auth || null,
228
+ onQuery: argv.log ? (c) => console.log('[redis]', c.join(' ')) : null,
229
+ });
230
+ srv.listen();
231
+ 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'] + ')' : ''}`);
232
+ setInterval(() => {}, 1 << 30);
233
+ })
96
234
  .command('version', 'Show version', null, () => {
97
235
  console.log(require('../package.json').version);
98
236
  })