jsql-neo 4.4.0 → 4.4.2
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 +210 -150
- package/lib/migrate.js +1 -1
- package/lib/redis_server.js +5 -2
- package/lib/sql.js +2 -2
- package/package.json +10 -1
- package/test/coverage.js +187 -0
package/README.md
CHANGED
|
@@ -1,229 +1,289 @@
|
|
|
1
|
-
# JSQL-NEO
|
|
1
|
+
# JSQL-NEO
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
|
|
6
|
+

|
|
7
|
+

|
|
8
|
+

|
|
9
|
+

|
|
10
|
+

|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Why JSQL-NEO?
|
|
6
15
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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 |
|
|
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**.
|
|
12
19
|
|
|
13
|
-
|
|
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
|
|
28
|
+
|
|
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
|
+
```
|
|
14
43
|
|
|
15
|
-
##
|
|
44
|
+
## 30-second Quick Start
|
|
45
|
+
|
|
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
|
-
//
|
|
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
|
-
|
|
63
|
+
Need a **MySQL server** instead?
|
|
34
64
|
|
|
35
|
-
|
|
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
|
+
```
|
|
36
69
|
|
|
37
|
-
|
|
70
|
+
Need a **Redis server**?
|
|
38
71
|
|
|
39
|
-
```
|
|
40
|
-
|
|
41
|
-
|
|
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
|
|
46
|
-
});
|
|
47
|
-
await db.start();
|
|
72
|
+
```bash
|
|
73
|
+
jsql redis -p 6379 --data-dir ./redis-data
|
|
74
|
+
redis-cli SET hello world
|
|
48
75
|
```
|
|
49
76
|
|
|
50
|
-
|
|
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
|
|
77
|
+
Need a **web console**?
|
|
53
78
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
```js
|
|
57
|
-
// Pure JS engine (same options)
|
|
58
|
-
const db2 = new jsql.Database({ path: '/var/lib/jsql', mode: 'hybrid' });
|
|
79
|
+
```bash
|
|
80
|
+
jsql ui -p 8080 --data-dir ./data # open http://localhost:8080
|
|
59
81
|
```
|
|
60
82
|
|
|
61
|
-
|
|
83
|
+
One package. One install. Five doors in.
|
|
62
84
|
|
|
63
|
-
|
|
85
|
+
---
|
|
64
86
|
|
|
65
|
-
|
|
66
|
-
const { NativeJSQL } = require('jsql-neo');
|
|
67
|
-
const db = new NativeJSQL();
|
|
68
|
-
await db.start();
|
|
87
|
+
## Engines
|
|
69
88
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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 |
|
|
94
|
+
|
|
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)
|
|
79
99
|
|
|
80
|
-
|
|
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 |
|
|
81
105
|
|
|
82
106
|
```js
|
|
83
|
-
const
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
age: { type: 'integer' }
|
|
107
|
+
const db = new jsql.NativeJSQL({
|
|
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)
|
|
89
112
|
});
|
|
90
|
-
const ids = users.insertMany([{ name: 'Alice', age: 30 }, { name: 'Bob', age: 25 }]);
|
|
91
|
-
users.updateById(ids[0], { age: 31 });
|
|
92
|
-
db.save();
|
|
93
113
|
```
|
|
94
114
|
|
|
95
|
-
|
|
115
|
+
Atomic writes (tmp + rename), per-table files, and WAL + snapshot crash recovery on the server engine.
|
|
96
116
|
|
|
97
|
-
|
|
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 |
|
|
104
|
-
| `updateById(table, id, data)` | ✅ | ✅ | ✅ | O(1) PK update |
|
|
105
|
-
| `removeById(table, id)` | ✅ | ✅ | ✅ | O(1) PK delete |
|
|
106
|
-
| `dropTable(name)` | ✅ | ✅ | ✅ | Remove table |
|
|
107
|
-
| `executeSQL(db, sql, params?)` | ✅ | ✅ | ✅ | Run SQL statements |
|
|
117
|
+
---
|
|
108
118
|
|
|
109
|
-
##
|
|
119
|
+
## The SQL Engine
|
|
110
120
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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
|
|
120
131
|
```
|
|
121
132
|
|
|
122
|
-
|
|
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
|
|
123
140
|
|
|
124
|
-
The MySQL-
|
|
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:
|
|
125
143
|
|
|
126
144
|
| ORM | Version | Results |
|
|
127
145
|
|-----|---------|---------|
|
|
128
|
-
| [Sequelize](https://sequelize.org) | v6 | ✅ 10/10 — connect, authenticate, sync
|
|
129
|
-
| [Knex](https://knexjs.org) | v3 | ✅ 9/9 — schema builder, insert, select, where + orderBy, count, update, delete, raw
|
|
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 |
|
|
130
148
|
| [TypeORM](https://typeorm.io) | v0.3 | ✅ 8/8 — initialize, synchronize, save, findOne, find, count, update, delete |
|
|
131
149
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
host: '127.0.0.1', port: 33306, dialect: 'mysql',
|
|
137
|
-
});
|
|
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`).
|
|
138
154
|
|
|
139
|
-
|
|
140
|
-
const
|
|
141
|
-
|
|
142
|
-
connection: { host: '127.0.0.1', port: 33306, user: 'root', database: 'default' },
|
|
143
|
-
});
|
|
144
|
-
|
|
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
|
-
});
|
|
155
|
+
```js
|
|
156
|
+
const { createMysqlServer } = require('jsql-neo');
|
|
157
|
+
createMysqlServer({ port: 3306, dataDir: './data', noAuth: true }).listen();
|
|
151
158
|
```
|
|
152
159
|
|
|
153
|
-
|
|
160
|
+
---
|
|
154
161
|
|
|
155
|
-
## Redis
|
|
162
|
+
## Speak Redis
|
|
163
|
+
|
|
164
|
+
A RESP2 server that plays perfectly with `ioredis`, `node-redis`, and `redis-cli`:
|
|
156
165
|
|
|
157
|
-
```
|
|
158
|
-
|
|
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
|
|
159
173
|
```
|
|
160
174
|
|
|
161
|
-
|
|
175
|
+
Snapshot persistence to `data.rdb.json` — debounced writes (500ms) plus a guaranteed flush on shutdown.
|
|
162
176
|
|
|
163
177
|
```js
|
|
164
178
|
const { createRedisServer } = require('jsql-neo');
|
|
165
179
|
createRedisServer({ port: 6379, dataDir: './redis-data' }).listen();
|
|
166
180
|
```
|
|
167
181
|
|
|
168
|
-
|
|
182
|
+
---
|
|
169
183
|
|
|
170
|
-
##
|
|
184
|
+
## Toolbox — everything included
|
|
171
185
|
|
|
172
|
-
|
|
173
|
-
jsql ui -p 8080 --data-dir ./data # then open http://localhost:8080
|
|
174
|
-
```
|
|
186
|
+
### CLI (`jsql`)
|
|
175
187
|
|
|
176
|
-
|
|
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 |
|
|
177
199
|
|
|
178
|
-
|
|
200
|
+
### Web UI (`WebUI`)
|
|
179
201
|
|
|
180
|
-
|
|
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.
|
|
181
204
|
|
|
182
|
-
|
|
205
|
+
### Migration tools (`migrate`)
|
|
183
206
|
|
|
184
|
-
|
|
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):
|
|
185
217
|
|
|
186
218
|
```bash
|
|
187
219
|
cd examples/playground && npm install && npm run dev
|
|
188
220
|
```
|
|
189
221
|
|
|
190
|
-
|
|
222
|
+
### TypeScript
|
|
191
223
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
npm run test:orms # ORM compatibility suites (start examples/orms/start-server.js first)
|
|
195
|
-
```
|
|
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.
|
|
196
226
|
|
|
197
|
-
|
|
227
|
+
---
|
|
198
228
|
|
|
199
229
|
## Benchmark
|
|
200
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
|
+
|
|
201
242
|
```bash
|
|
202
243
|
cd bench && npm install && npm run bench
|
|
203
244
|
```
|
|
204
245
|
|
|
205
|
-
|
|
246
|
+
---
|
|
206
247
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
|
210
|
-
|
|
211
|
-
|
|
|
212
|
-
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
248
|
+
## API at a glance
|
|
249
|
+
|
|
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)` | ✅ | ✅ | ✅ | — |
|
|
257
|
+
| `updateById(table, id, data)` | ✅ | ✅ | ✅ | O(1) PK update |
|
|
258
|
+
| `removeById(table, id)` | ✅ | ✅ | ✅ | O(1) PK delete |
|
|
259
|
+
| `executeSQL(db, sql, params)` | ✅ | ✅ | ✅ | Full SQL engine, prepared statements |
|
|
260
|
+
|
|
261
|
+
```js
|
|
262
|
+
{
|
|
263
|
+
type: 'string' | 'integer' | 'float' | 'boolean',
|
|
264
|
+
primaryKey: true, // auto-indexed
|
|
265
|
+
autoIncrement: true, // integer PK generation
|
|
266
|
+
length: 32, // string max length
|
|
267
|
+
default: 'value',
|
|
268
|
+
nullable: true
|
|
269
|
+
}
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
---
|
|
273
|
+
|
|
274
|
+
## Testing
|
|
275
|
+
|
|
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
|
+
```
|
|
280
|
+
|
|
281
|
+
CI (`.github/workflows/ci.yml`): engine smoke tests on Node 18/20/22 + a full ORM compatibility job.
|
|
282
|
+
|
|
283
|
+
---
|
|
284
|
+
|
|
285
|
+
## License
|
|
286
|
+
|
|
287
|
+
MIT — use it, ship it, love it.
|
|
288
|
+
|
|
289
|
+
*JSQL-NEO: Rust-powered. Protocol-native. One package.*
|
package/lib/migrate.js
CHANGED
|
@@ -130,7 +130,7 @@ async function importFromJSON(engine, data) {
|
|
|
130
130
|
await engine.createTable(name, normalizeSchema(t.schema));
|
|
131
131
|
created.push(name);
|
|
132
132
|
if (Array.isArray(t.rows) && t.rows.length > 0) {
|
|
133
|
-
const ids = await engine.insert(name, t.rows.map(r => ({ ...r
|
|
133
|
+
const ids = await engine.insert(name, t.rows.map(r => ({ ...r })));
|
|
134
134
|
inserted += Array.isArray(ids) ? ids.length : t.rows.length;
|
|
135
135
|
}
|
|
136
136
|
}
|
package/lib/redis_server.js
CHANGED
|
@@ -250,8 +250,11 @@ class RedisServer {
|
|
|
250
250
|
case 'LPUSH': case 'RPUSH': {
|
|
251
251
|
const v = this._get(args[0]);
|
|
252
252
|
const list = (v && v.type === 'list') ? [...v.val] : [];
|
|
253
|
-
if (cmd === 'LPUSH')
|
|
254
|
-
|
|
253
|
+
if (cmd === 'LPUSH') {
|
|
254
|
+
for (const val of args.slice(1)) list.unshift(val);
|
|
255
|
+
} else {
|
|
256
|
+
list.push(...args.slice(1));
|
|
257
|
+
}
|
|
255
258
|
this.db.set(args[0], { type: 'list', val: list, ttl: v ? v.ttl : null });
|
|
256
259
|
this._scheduleSnapshot();
|
|
257
260
|
return list.length;
|
package/lib/sql.js
CHANGED
|
@@ -1994,11 +1994,11 @@ function splitStatements(sql) {
|
|
|
1994
1994
|
}
|
|
1995
1995
|
if (c === "'" || c === '"') { inStr = c; current += c; i++; continue; }
|
|
1996
1996
|
if (c === '-' && sql[i + 1] === '-') {
|
|
1997
|
-
while (i < sql.length && sql[i] !== '\n')
|
|
1997
|
+
while (i < sql.length && sql[i] !== '\n') i++;
|
|
1998
1998
|
continue;
|
|
1999
1999
|
}
|
|
2000
2000
|
if (c === '#') {
|
|
2001
|
-
while (i < sql.length && sql[i] !== '\n')
|
|
2001
|
+
while (i < sql.length && sql[i] !== '\n') i++;
|
|
2002
2002
|
continue;
|
|
2003
2003
|
}
|
|
2004
2004
|
if (c === '/' && sql[i + 1] === '*') {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jsql-neo",
|
|
3
|
-
"version": "4.4.
|
|
3
|
+
"version": "4.4.2",
|
|
4
4
|
"description": "JSQL-NEO — Rust-powered embedded database with WASM, REST API, B-Tree indexes, WAL, crash recovery",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"types": "index.d.ts",
|
|
@@ -8,12 +8,17 @@
|
|
|
8
8
|
".": {
|
|
9
9
|
"types": "./index.d.ts",
|
|
10
10
|
"browser": "./wasm/browser.mjs",
|
|
11
|
+
"import": "./index.js",
|
|
12
|
+
"require": "./index.js",
|
|
11
13
|
"default": "./index.js"
|
|
12
14
|
},
|
|
15
|
+
"./cli": "./bin/jsql",
|
|
13
16
|
"./wasm/browser.mjs": {
|
|
14
17
|
"types": "./wasm/browser.d.ts",
|
|
15
18
|
"default": "./wasm/browser.mjs"
|
|
16
19
|
},
|
|
20
|
+
"./wasm/browser_bg.mjs": "./wasm/browser_bg.mjs",
|
|
21
|
+
"./wasm/browser.d.ts": "./wasm/browser.d.ts",
|
|
17
22
|
"./wasm/*": "./wasm/*",
|
|
18
23
|
"./lib/*": "./lib/*",
|
|
19
24
|
"./package.json": "./package.json"
|
|
@@ -38,6 +43,7 @@
|
|
|
38
43
|
"scripts": {
|
|
39
44
|
"postinstall": "node postinstall.js",
|
|
40
45
|
"test": "node test/smoke.js",
|
|
46
|
+
"test:coverage": "c8 --all --include='lib/**' --exclude='lib/mysql_server.js' --exclude='lib/native_client.js' --exclude='lib/wasm_client.js' --exclude='lib/mysql_compat.js' --exclude='lib/nedb_compat.js' --exclude='lib/plugin.js' --reporter=text --reporter=lcov node test/coverage.js",
|
|
41
47
|
"test:orms": "node examples/orms/run-all.js"
|
|
42
48
|
},
|
|
43
49
|
"keywords": [
|
|
@@ -66,5 +72,8 @@
|
|
|
66
72
|
},
|
|
67
73
|
"dependencies": {
|
|
68
74
|
"@vexify-org/yaggs": "^8.1.0"
|
|
75
|
+
},
|
|
76
|
+
"devDependencies": {
|
|
77
|
+
"c8": "^10.1.2"
|
|
69
78
|
}
|
|
70
79
|
}
|
package/test/coverage.js
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Integration coverage test — exercises Redis server, Web UI, migration tools
|
|
3
|
+
* and CLI logic in-process (zero dependencies) so c8 can measure real usage.
|
|
4
|
+
*
|
|
5
|
+
* npm run coverage (c8 node test/coverage.js)
|
|
6
|
+
*/
|
|
7
|
+
const path = require('path');
|
|
8
|
+
const fs = require('fs');
|
|
9
|
+
const os = require('os');
|
|
10
|
+
|
|
11
|
+
const ROOT = path.join(__dirname, '..');
|
|
12
|
+
let passed = 0, failed = 0;
|
|
13
|
+
|
|
14
|
+
function ok(name, cond) {
|
|
15
|
+
if (cond) { passed++; console.log('[OK]', name); }
|
|
16
|
+
else { failed++; console.log('[FAIL]', name); }
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function tmp(name) {
|
|
20
|
+
const p = path.join(fs.mkdtempSync(path.join(os.tmpdir(), 'jsql-cov-')), name);
|
|
21
|
+
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
22
|
+
return p;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
(async () => {
|
|
26
|
+
/* ---------- Redis server ---------- */
|
|
27
|
+
const { RedisServer } = require(path.join(ROOT, 'lib/redis_server'));
|
|
28
|
+
const Database = require(path.join(ROOT, 'lib/database'));
|
|
29
|
+
const { executeSQL } = require(path.join(ROOT, 'lib/sql'));
|
|
30
|
+
const { WebUI } = require(path.join(ROOT, 'lib/web_ui'));
|
|
31
|
+
const migrate = require(path.join(ROOT, 'lib/migrate'));
|
|
32
|
+
|
|
33
|
+
{
|
|
34
|
+
const rs = new RedisServer({ dataDir: tmp('redis') });
|
|
35
|
+
ok('redis PING', rs.execute('PING', []) === 'PONG');
|
|
36
|
+
ok('redis ECHO', rs.execute('ECHO', ['hi']) === 'hi');
|
|
37
|
+
rs.execute('SET', ['k', 'v']);
|
|
38
|
+
ok('redis GET', rs.execute('GET', ['k']) === 'v');
|
|
39
|
+
ok('redis SETNX existing', rs.execute('SETNX', ['k', 'x']) === 0);
|
|
40
|
+
ok('redis SETNX new', rs.execute('SETNX', ['n', 'x']) === 1);
|
|
41
|
+
rs.execute('INCR', ['cnt']);
|
|
42
|
+
rs.execute('INCR', ['cnt']);
|
|
43
|
+
ok('redis INCR', rs.execute('GET', ['cnt']) === '2');
|
|
44
|
+
ok('redis DECR', rs.execute('DECR', ['cnt']) === 1);
|
|
45
|
+
ok('redis INCRBY', rs.execute('INCRBY', ['cnt', '5']) === 6);
|
|
46
|
+
rs.execute('APPEND', ['a', 'hello']);
|
|
47
|
+
ok('redis APPEND/STRLEN', rs.execute('STRLEN', ['a']) === 5);
|
|
48
|
+
rs.execute('HSET', ['h', 'f1', 'v1', 'f2', 'v2']);
|
|
49
|
+
ok('redis HGET', rs.execute('HGET', ['h', 'f1']) === 'v1');
|
|
50
|
+
const hg = rs.execute('HGETALL', ['h']);
|
|
51
|
+
ok('redis HGETALL', Array.isArray(hg) && hg.length === 4);
|
|
52
|
+
ok('redis HDEL', rs.execute('HDEL', ['h', 'f1']) === 1);
|
|
53
|
+
ok('redis HLEN', rs.execute('HLEN', ['h']) === 1);
|
|
54
|
+
ok('redis HKEYS', JSON.stringify(rs.execute('HKEYS', ['h'])) === '["f2"]');
|
|
55
|
+
rs.execute('LPUSH', ['l', 'a', 'b']);
|
|
56
|
+
ok('redis LLEN', rs.execute('LLEN', ['l']) === 2);
|
|
57
|
+
const lr = rs.execute('LRANGE', ['l', '0', '-1']);
|
|
58
|
+
ok('redis LRANGE', JSON.stringify(lr) === '["b","a"]');
|
|
59
|
+
ok('redis LINDEX', rs.execute('LINDEX', ['l', '0']) === 'b');
|
|
60
|
+
rs.execute('RPUSH', ['l', 'c']);
|
|
61
|
+
ok('redis LPOP', rs.execute('LPOP', ['l']) === 'b');
|
|
62
|
+
ok('redis LREM', rs.execute('LREM', ['l', '0', 'a']) === 1);
|
|
63
|
+
rs.execute('SADD', ['s', 'm1', 'm2', 'm3']);
|
|
64
|
+
ok('redis SCARD', rs.execute('SCARD', ['s']) === 3);
|
|
65
|
+
ok('redis SISMEMBER', rs.execute('SISMEMBER', ['s', 'm1']) === 1);
|
|
66
|
+
ok('redis SMEMBERS', rs.execute('SMEMBERS', ['s']).length === 3);
|
|
67
|
+
ok('redis SREM', rs.execute('SREM', ['s', 'm1']) === 1);
|
|
68
|
+
ok('redis KEYS', rs.execute('KEYS', ['*']).length >= 5);
|
|
69
|
+
ok('redis EXISTS', rs.execute('EXISTS', ['k', 'zzz']) === 1);
|
|
70
|
+
ok('redis TYPE', rs.execute('TYPE', ['h']) === 'hash');
|
|
71
|
+
ok('redis TTL missing', rs.execute('TTL', ['zzz']) === -2);
|
|
72
|
+
rs.execute('EXPIRE', ['k', '100']);
|
|
73
|
+
ok('redis TTL set', rs.execute('TTL', ['k']) > 0);
|
|
74
|
+
ok('redis PERSIST', rs.execute('PERSIST', ['k']) === 1);
|
|
75
|
+
ok('redis DBSIZE', rs.execute('DBSIZE', []) >= 5);
|
|
76
|
+
ok('redis FLUSHALL', rs.execute('FLUSHALL', []) === 'OK');
|
|
77
|
+
ok('redis DBSIZE after flush', rs.execute('DBSIZE', []) === 0);
|
|
78
|
+
ok('redis INFO', rs.execute('INFO', []).includes('redis_version'));
|
|
79
|
+
ok('redis SELECT', rs.execute('SELECT', ['3']) === 'OK');
|
|
80
|
+
rs.stop();
|
|
81
|
+
const persisted = JSON.parse(fs.readFileSync(rs.snapshotPath(), 'utf8'));
|
|
82
|
+
ok('redis snapshot file written', Object.keys(persisted).length >= 0);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/* ---------- Web UI ---------- */
|
|
86
|
+
{
|
|
87
|
+
const dbFile = tmp('webui/db.json');
|
|
88
|
+
fs.mkdirSync(path.dirname(dbFile), { recursive: true });
|
|
89
|
+
fs.writeFileSync(dbFile, JSON.stringify({
|
|
90
|
+
__schema__: { users: { id: { type: 'integer', primaryKey: true, autoIncrement: true }, name: { type: 'string' } } },
|
|
91
|
+
__meta__: { version: 1 },
|
|
92
|
+
users: [{ id: 1, name: 'Alice' }, { id: 2, name: 'Bob' }],
|
|
93
|
+
}));
|
|
94
|
+
const ui = new WebUI({ port: 0, dataDir: path.dirname(dbFile) });
|
|
95
|
+
const port = await ui.start();
|
|
96
|
+
const dbs = await (await fetch(`http://127.0.0.1:${port}/api/databases`)).json();
|
|
97
|
+
ok('webui list databases', dbs.length === 1 && dbs[0].name === 'db');
|
|
98
|
+
const tbls = await (await fetch(`http://127.0.0.1:${port}/api/tables?db=db`)).json();
|
|
99
|
+
ok('webui list tables', tbls.tables.length === 1 && tbls.tables[0].name === 'users' && tbls.tables[0].count === 2);
|
|
100
|
+
const q = await (await fetch(`http://127.0.0.1:${port}/api/query`, {
|
|
101
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
102
|
+
body: JSON.stringify({ db: 'db', sql: 'SELECT name FROM users WHERE id = 1' }),
|
|
103
|
+
})).json();
|
|
104
|
+
ok('webui query', q.ok && JSON.stringify(q.rows) === '[[\"Alice\"]]');
|
|
105
|
+
const ins = await (await fetch(`http://127.0.0.1:${port}/api/query`, {
|
|
106
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
107
|
+
body: JSON.stringify({ db: 'db', sql: "INSERT INTO users (name) VALUES ('Carol')" }),
|
|
108
|
+
})).json();
|
|
109
|
+
ok('webui insert', ins.ok && ins.affected === 1);
|
|
110
|
+
const bad = await (await fetch(`http://127.0.0.1:${port}/api/query`, {
|
|
111
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
112
|
+
body: JSON.stringify({ db: 'db', sql: 'SELECT * FROM nope' }),
|
|
113
|
+
})).json();
|
|
114
|
+
ok('webui error path', !bad.ok && bad.error.includes('nope'));
|
|
115
|
+
const html = await (await fetch(`http://127.0.0.1:${port}/`)).text();
|
|
116
|
+
ok('webui page', html.includes('JSQL-NEO'));
|
|
117
|
+
ok('webui missing db', (await (await fetch(`http://127.0.0.1:${port}/api/tables?db=x`)).json()).error !== undefined);
|
|
118
|
+
await ui.stop();
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/* ---------- Migration tools + SQL engine ---------- */
|
|
122
|
+
{
|
|
123
|
+
const db = new Database(':memory:', { autoSave: false });
|
|
124
|
+
const dump = `CREATE TABLE products (
|
|
125
|
+
id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
|
126
|
+
name VARCHAR(100) COLLATE utf8mb4_bin DEFAULT NULL,
|
|
127
|
+
price DECIMAL(10,2) DEFAULT '0.00',
|
|
128
|
+
note TEXT
|
|
129
|
+
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
|
130
|
+
-- comment line
|
|
131
|
+
INSERT INTO products (name, price, note) VALUES
|
|
132
|
+
('Laptop', 999.99, 'It\\'s a \\"pro\\" model\\nwith newline'),
|
|
133
|
+
('Mouse', 19.50, NULL),
|
|
134
|
+
('Cable', 9.90, 'USB\\tC');
|
|
135
|
+
`;
|
|
136
|
+
const r = await migrate.importDump(db, dump);
|
|
137
|
+
ok('migrate import dump', r.created.includes('products') && r.inserted === 3);
|
|
138
|
+
const csv = await migrate.exportTableToCSV(db, 'products');
|
|
139
|
+
ok('migrate export csv', csv.split('\n').length >= 4 && csv.includes('Laptop'));
|
|
140
|
+
const rows = await db.find('products', {}, { limit: 10 });
|
|
141
|
+
ok('migrate escaped round-trip', rows[0].name === 'Laptop' && rows[0].note.includes('with newline'));
|
|
142
|
+
const json = await migrate.exportAllToJSON(db);
|
|
143
|
+
ok('migrate export json', json.products && json.products.schema && json.products.rows.length === 3);
|
|
144
|
+
const db2 = new Database(':memory:', { autoSave: false });
|
|
145
|
+
await migrate.importFromJSON(db2, json);
|
|
146
|
+
ok('migrate import json', (await db2.count('products')) === 3);
|
|
147
|
+
const r2 = await migrate.importFromCSV(db2, 'products2', 'a,b\n1,hello\n2,"quo,ted"\n', { schema: { a: { type: 'string' }, b: { type: 'string' } } });
|
|
148
|
+
ok('migrate csv import', r2.inserted === 2);
|
|
149
|
+
|
|
150
|
+
await executeSQL(db, 'CREATE TABLE users (id INTEGER PRIMARY KEY AUTO_INCREMENT, name STRING, age INTEGER)');
|
|
151
|
+
await executeSQL(db, "INSERT INTO users (name, age) VALUES ('Alice', 30), ('Bob', 25)");
|
|
152
|
+
const sel = await executeSQL(db, 'SELECT name, age FROM users WHERE age > 26 ORDER BY age DESC');
|
|
153
|
+
ok('sql select where order', JSON.stringify(sel.rows) === '[["Alice",30]]');
|
|
154
|
+
const agg = await executeSQL(db, 'SELECT COUNT(*) AS n, AVG(age) AS a FROM users');
|
|
155
|
+
ok('sql aggregate', agg.rows[0][0] === 2);
|
|
156
|
+
const fn = await executeSQL(db, 'SELECT VERSION(), CONCAT("x","y")');
|
|
157
|
+
ok('sql func column names', fn.columns[0] === 'VERSION()');
|
|
158
|
+
await executeSQL(db, 'UPDATE users SET age = 31 WHERE name = \'Bob\'');
|
|
159
|
+
await executeSQL(db, 'DELETE FROM users WHERE id = 1');
|
|
160
|
+
ok('sql count after delete', (await db.count('users')) === 1);
|
|
161
|
+
await executeSQL(db, 'BEGIN');
|
|
162
|
+
await executeSQL(db, "INSERT INTO users (name, age) VALUES ('Temp', 99)");
|
|
163
|
+
await executeSQL(db, 'ROLLBACK');
|
|
164
|
+
ok('sql rollback', (await db.count('users')) === 1);
|
|
165
|
+
await executeSQL(db, 'START TRANSACTION');
|
|
166
|
+
await executeSQL(db, "INSERT INTO users (name, age) VALUES ('X', 1)");
|
|
167
|
+
await executeSQL(db, 'TRUNCATE TABLE users');
|
|
168
|
+
ok('sql truncate', (await db.count('users')) === 0);
|
|
169
|
+
const sqlApi = require(path.join(ROOT, 'lib/sql'));
|
|
170
|
+
const parsed = sqlApi.parseSQL('SELECT * FROM t WHERE id = 5');
|
|
171
|
+
ok('parseSQL', parsed.type === 'select');
|
|
172
|
+
ok('applyParams', sqlApi.applyParams('SELECT * FROM t WHERE id = ?', [7]) === 'SELECT * FROM t WHERE id = 7');
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/* ---------- CLI module load ---------- */
|
|
176
|
+
{
|
|
177
|
+
try {
|
|
178
|
+
require(path.join(ROOT, 'bin/jsql'));
|
|
179
|
+
ok('cli loads', true);
|
|
180
|
+
} catch (e) {
|
|
181
|
+
ok('cli loads', !/Cannot find module/.test(e.message));
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
console.log(`\n${passed} passed, ${failed} failed`);
|
|
186
|
+
process.exit(failed ? 1 : 0);
|
|
187
|
+
})().catch(e => { console.error('[FATAL]', e.stack.split('\n').slice(0, 3).join('\n')); process.exit(1); });
|