jsql-neo 4.4.1 → 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/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/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); });
|