jsql-neo 4.3.0 → 4.4.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.
- package/README.md +64 -0
- package/bin/jsql +138 -0
- package/index.d.ts +351 -0
- package/index.js +18 -0
- package/lib/migrate.js +242 -0
- package/lib/mysql_server.js +51 -1
- package/lib/redis_server.js +448 -0
- package/lib/sql.js +125 -51
- package/lib/web_ui.js +226 -0
- package/package.json +66 -56
- package/test/smoke.js +58 -0
- package/wasm/browser.d.ts +88 -0
package/lib/migrate.js
ADDED
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Migration tools: mysqldump import, JSON import/export, CSV import/export.
|
|
3
|
+
*
|
|
4
|
+
* Works against any engine exposing:
|
|
5
|
+
* hasTable(name) / getTableSchema(name) / find(name, {}, {limit, offset})
|
|
6
|
+
* createTable(name, schema) / insert(name, rows) / executeSQL(sql, ...)
|
|
7
|
+
* (Database instances and the jsql-neo MySQL server engine both qualify.)
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const fs = require('fs');
|
|
11
|
+
const path = require('path');
|
|
12
|
+
const { splitStatements, executeSQL } = require('./sql');
|
|
13
|
+
|
|
14
|
+
function normalizeSchema(schema) {
|
|
15
|
+
const out = {};
|
|
16
|
+
for (const [name, def] of Object.entries(schema || {})) {
|
|
17
|
+
const d = typeof def === 'string' ? { type: def } : { ...def };
|
|
18
|
+
if (!d.type) d.type = typeof d === 'object' ? 'any' : 'string';
|
|
19
|
+
if (d.type === 'int' || d.type === 'bigint' || d.type === 'smallint' || d.type === 'tinyint') d.type = 'integer';
|
|
20
|
+
if (d.type === 'varchar' || d.type === 'text' || d.type === 'char') d.type = 'string';
|
|
21
|
+
if (d.type === 'double' || d.type === 'real' || d.type === 'decimal' || d.type === 'numeric') d.type = 'float';
|
|
22
|
+
if (d.type === 'bool') d.type = 'boolean';
|
|
23
|
+
delete d.length;
|
|
24
|
+
if (d.maxLength) { d.length = d.maxLength; delete d.maxLength; }
|
|
25
|
+
out[name] = d;
|
|
26
|
+
}
|
|
27
|
+
return out;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function serializeValue(v) {
|
|
31
|
+
if (v === null || v === undefined) return '';
|
|
32
|
+
if (v instanceof Date) return v.toISOString();
|
|
33
|
+
if (typeof v === 'object') return JSON.stringify(v);
|
|
34
|
+
return String(v);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function parseValue(str, type) {
|
|
38
|
+
if (str === '' || str === null || str === undefined) return null;
|
|
39
|
+
const t = String(type || 'string').toLowerCase();
|
|
40
|
+
if (t === 'integer') return Number.isFinite(Number(str)) ? Math.trunc(Number(str)) : str;
|
|
41
|
+
if (t === 'float' || t === 'number') return Number.isFinite(Number(str)) ? Number(str) : str;
|
|
42
|
+
if (t === 'boolean') {
|
|
43
|
+
const s = str.toLowerCase();
|
|
44
|
+
if (['1', 'true', 'yes', 'y'].includes(s)) return true;
|
|
45
|
+
if (['0', 'false', 'no', 'n'].includes(s)) return false;
|
|
46
|
+
return str;
|
|
47
|
+
}
|
|
48
|
+
if (t === 'object' || t === 'array') {
|
|
49
|
+
try { return JSON.parse(str); } catch (e) { return str; }
|
|
50
|
+
}
|
|
51
|
+
return str;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function parseCSV(text) {
|
|
55
|
+
const rows = [];
|
|
56
|
+
let row = [];
|
|
57
|
+
let field = '';
|
|
58
|
+
let inQuotes = false;
|
|
59
|
+
let i = 0;
|
|
60
|
+
const n = text.length;
|
|
61
|
+
while (i < n) {
|
|
62
|
+
const c = text[i];
|
|
63
|
+
if (inQuotes) {
|
|
64
|
+
if (c === '"') {
|
|
65
|
+
if (text[i + 1] === '"') { field += '"'; i += 2; continue; }
|
|
66
|
+
inQuotes = false;
|
|
67
|
+
i++;
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
field += c;
|
|
71
|
+
i++;
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
if (c === '"' && field === '') { inQuotes = true; i++; continue; }
|
|
75
|
+
if (c === ',') { row.push(field); field = ''; i++; continue; }
|
|
76
|
+
if (c === '\n' || c === '\r') {
|
|
77
|
+
if (c === '\r' && text[i + 1] === '\n') i++;
|
|
78
|
+
row.push(field);
|
|
79
|
+
field = '';
|
|
80
|
+
if (row.length > 1 || row[0] !== '') rows.push(row);
|
|
81
|
+
row = [];
|
|
82
|
+
i++;
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
field += c;
|
|
86
|
+
i++;
|
|
87
|
+
}
|
|
88
|
+
if (field !== '' || row.length > 0) {
|
|
89
|
+
row.push(field);
|
|
90
|
+
if (row.length > 1 || row[0] !== '') rows.push(row);
|
|
91
|
+
}
|
|
92
|
+
return rows;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function toCSV(rows, columns) {
|
|
96
|
+
const escape = (v) => {
|
|
97
|
+
const s = serializeValue(v);
|
|
98
|
+
return /[",\n\r]/.test(s) ? '"' + s.replace(/"/g, '""') + '"' : s;
|
|
99
|
+
};
|
|
100
|
+
const lines = [columns.map(escape).join(',')];
|
|
101
|
+
for (const r of rows) {
|
|
102
|
+
lines.push(columns.map(c => escape(r[c])).join(','));
|
|
103
|
+
}
|
|
104
|
+
return lines.join('\n') + '\n';
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/* ---------- JSON ---------- */
|
|
108
|
+
|
|
109
|
+
async function exportTableToJSON(engine, table) {
|
|
110
|
+
const schema = await engine.getTableSchema(table);
|
|
111
|
+
if (!schema) throw new Error(`Table '${table}' does not exist`);
|
|
112
|
+
const rows = await engine.find(table, {}, { limit: 1e9, offset: 0 });
|
|
113
|
+
return { table, schema, rows };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async function exportAllToJSON(engine, tables) {
|
|
117
|
+
const list = tables || await engine.getTables();
|
|
118
|
+
const out = {};
|
|
119
|
+
for (const t of list) out[t] = await exportTableToJSON(engine, t);
|
|
120
|
+
return out;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async function importFromJSON(engine, data) {
|
|
124
|
+
const tables = typeof data === 'string' ? JSON.parse(data) : data;
|
|
125
|
+
const created = [];
|
|
126
|
+
let inserted = 0;
|
|
127
|
+
for (const [name, t] of Object.entries(tables)) {
|
|
128
|
+
if (!t || !t.schema) continue;
|
|
129
|
+
if (engine.hasTable(name)) await engine.dropTable(name);
|
|
130
|
+
await engine.createTable(name, normalizeSchema(t.schema));
|
|
131
|
+
created.push(name);
|
|
132
|
+
if (Array.isArray(t.rows) && t.rows.length > 0) {
|
|
133
|
+
const ids = await engine.insert(name, t.rows.map(r => ({ ...r.fields, id: r.id })));
|
|
134
|
+
inserted += Array.isArray(ids) ? ids.length : t.rows.length;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return { created, inserted };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/* ---------- CSV ---------- */
|
|
141
|
+
|
|
142
|
+
async function exportTableToCSV(engine, table) {
|
|
143
|
+
const schema = await engine.getTableSchema(table);
|
|
144
|
+
if (!schema) throw new Error(`Table '${table}' does not exist`);
|
|
145
|
+
const columns = Object.keys(schema);
|
|
146
|
+
const rows = await engine.find(table, {}, { limit: 1e9, offset: 0 });
|
|
147
|
+
return toCSV(rows, columns);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
async function importFromCSV(engine, table, csv, opts = {}) {
|
|
151
|
+
const schema = opts.schema || await engine.getTableSchema(table);
|
|
152
|
+
const rows = parseCSV(csv);
|
|
153
|
+
if (rows.length === 0) return { inserted: 0 };
|
|
154
|
+
let columns;
|
|
155
|
+
let start = 0;
|
|
156
|
+
if (opts.header !== false) {
|
|
157
|
+
columns = rows[0];
|
|
158
|
+
start = 1;
|
|
159
|
+
} else if (schema) {
|
|
160
|
+
columns = Object.keys(schema);
|
|
161
|
+
} else {
|
|
162
|
+
columns = rows[0].map((_, i) => 'col' + (i + 1));
|
|
163
|
+
}
|
|
164
|
+
if (!engine.hasTable(table)) {
|
|
165
|
+
if (!schema) {
|
|
166
|
+
throw new Error(`Table '${table}' does not exist; provide opts.schema to create it`);
|
|
167
|
+
}
|
|
168
|
+
await engine.createTable(table, normalizeSchema(schema));
|
|
169
|
+
}
|
|
170
|
+
const dataRows = [];
|
|
171
|
+
for (let i = start; i < rows.length; i++) {
|
|
172
|
+
const row = {};
|
|
173
|
+
for (let j = 0; j < columns.length; j++) {
|
|
174
|
+
const type = schema && schema[columns[j]] ? schema[columns[j]].type : 'string';
|
|
175
|
+
row[columns[j]] = parseValue(rows[i][j], type);
|
|
176
|
+
}
|
|
177
|
+
if (row.id === null || row.id === undefined || row.id === '') delete row.id;
|
|
178
|
+
dataRows.push(row);
|
|
179
|
+
}
|
|
180
|
+
const ids = dataRows.length > 0 ? await engine.insert(table, dataRows) : [];
|
|
181
|
+
return { inserted: dataRows.length, ids };
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/* ---------- mysqldump ---------- */
|
|
185
|
+
|
|
186
|
+
async function importDump(engine, sqlText, opts = {}) {
|
|
187
|
+
const statements = splitStatements(sqlText);
|
|
188
|
+
const created = [];
|
|
189
|
+
let inserted = 0;
|
|
190
|
+
const errors = [];
|
|
191
|
+
for (const raw of statements) {
|
|
192
|
+
const stmt = raw.trim();
|
|
193
|
+
if (!stmt) continue;
|
|
194
|
+
if (stmt.startsWith('--') || stmt.startsWith('#')) continue;
|
|
195
|
+
const upper = stmt.toUpperCase();
|
|
196
|
+
if (upper.startsWith('LOCK ') || upper.startsWith('UNLOCK ')) continue;
|
|
197
|
+
if (upper.startsWith('/*!')) continue;
|
|
198
|
+
if (upper.startsWith('SET ') && opts.skipSet !== false) continue;
|
|
199
|
+
try {
|
|
200
|
+
const r = await executeSQL(engine, stmt, { safety: false });
|
|
201
|
+
if (r && r.type === 'createTable') created.push(r.table);
|
|
202
|
+
if (r && r.type === 'insert') inserted += (r.ids || []).length || r.affectedRows || 0;
|
|
203
|
+
} catch (e) {
|
|
204
|
+
if (opts.strict) throw e;
|
|
205
|
+
errors.push({ sql: stmt.slice(0, 120), error: e.message });
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
return { created, inserted, errors };
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
async function importDumpFile(engine, filePath, opts = {}) {
|
|
212
|
+
const text = fs.readFileSync(filePath, 'utf8');
|
|
213
|
+
return importDump(engine, text, opts);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
async function exportToFile(engine, table, filePath) {
|
|
217
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
218
|
+
let content;
|
|
219
|
+
if (ext === '.json') {
|
|
220
|
+
content = JSON.stringify(await exportTableToJSON(engine, table), null, 2);
|
|
221
|
+
} else if (ext === '.csv') {
|
|
222
|
+
content = await exportTableToCSV(engine, table);
|
|
223
|
+
} else {
|
|
224
|
+
throw new Error('Unsupported export format (use .json or .csv): ' + filePath);
|
|
225
|
+
}
|
|
226
|
+
fs.writeFileSync(filePath, content);
|
|
227
|
+
return content.length;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
module.exports = {
|
|
231
|
+
normalizeSchema,
|
|
232
|
+
parseCSV,
|
|
233
|
+
toCSV,
|
|
234
|
+
exportTableToJSON,
|
|
235
|
+
exportAllToJSON,
|
|
236
|
+
importFromJSON,
|
|
237
|
+
exportTableToCSV,
|
|
238
|
+
importFromCSV,
|
|
239
|
+
importDump,
|
|
240
|
+
importDumpFile,
|
|
241
|
+
exportToFile,
|
|
242
|
+
};
|
package/lib/mysql_server.js
CHANGED
|
@@ -31,6 +31,7 @@ const MYSQL_TYPE_TINY = 1, MYSQL_TYPE_LONG = 3, MYSQL_TYPE_LONGLONG = 8,
|
|
|
31
31
|
MYSQL_TYPE_JSON = 245, MYSQL_TYPE_NULL = 6;
|
|
32
32
|
|
|
33
33
|
function encodeLenenc(value) {
|
|
34
|
+
if (typeof value === 'bigint') value = Number(value);
|
|
34
35
|
if (value === null) return Buffer.from([0xfb]);
|
|
35
36
|
if (value < 0xfb) return Buffer.from([value]);
|
|
36
37
|
if (value <= 0xffff) {
|
|
@@ -135,7 +136,38 @@ function toMysqlErrno(e) {
|
|
|
135
136
|
if (e && typeof e.code === 'number') return e.code;
|
|
136
137
|
const m = e && e.message ? String(e.message).match(/^(ER_[A-Z_]+)/) : null;
|
|
137
138
|
if (m) {
|
|
138
|
-
const known = {
|
|
139
|
+
const known = {
|
|
140
|
+
ER_DUP_ENTRY: 1062,
|
|
141
|
+
ER_NO_SUCH_TABLE: 1146,
|
|
142
|
+
ER_TABLE_EXISTS: 1050,
|
|
143
|
+
ER_TABLE_EXISTS_ERROR: 1050,
|
|
144
|
+
ER_PARSE_ERROR: 1064,
|
|
145
|
+
ER_BAD_FIELD_ERROR: 1054,
|
|
146
|
+
ER_BAD_NULL_ERROR: 1048,
|
|
147
|
+
ER_ACCESS_DENIED_ERROR: 1045,
|
|
148
|
+
ER_DBACCESS_DENIED_ERROR: 1044,
|
|
149
|
+
ER_BAD_DB_ERROR: 1049,
|
|
150
|
+
ER_WRONG_DB_NAME: 1102,
|
|
151
|
+
ER_WRONG_TABLE_NAME: 1103,
|
|
152
|
+
ER_WRONG_COLUMN_NAME: 1166,
|
|
153
|
+
ER_DATA_TOO_LONG: 1406,
|
|
154
|
+
ER_OUT_OF_RANGE: 1264,
|
|
155
|
+
ER_CHECK_CONSTRAINT: 3819,
|
|
156
|
+
ER_NO_DEFAULT_FOR_FIELD: 1364,
|
|
157
|
+
ER_CANT_DROP_FIELD_OR_KEY: 1091,
|
|
158
|
+
ER_CANT_DROP_DATABASE: 1008,
|
|
159
|
+
ER_EMPTY_QUERY: 1065,
|
|
160
|
+
ER_UNKNOWN_TABLE: 1109,
|
|
161
|
+
ER_NON_UNIQ_ERROR: 1052,
|
|
162
|
+
ER_WRONG_FIELD_WITH_GROUP: 1055,
|
|
163
|
+
ER_WRONG_VALUE_COUNT_ON_ROW: 1136,
|
|
164
|
+
ER_MISSING_TABLE: 1052,
|
|
165
|
+
ER_SP_DOES_NOT_EXIST: 1305,
|
|
166
|
+
ER_NOT_SUPPORTED_YET: 1235,
|
|
167
|
+
ER_LOCK_DEADLOCK: 1213,
|
|
168
|
+
ER_LOCK_WAIT_TIMEOUT: 1205,
|
|
169
|
+
ER_UNKNOWN_ERROR: 1105,
|
|
170
|
+
};
|
|
139
171
|
if (known[m[1]]) return known[m[1]];
|
|
140
172
|
}
|
|
141
173
|
return 1105;
|
|
@@ -273,11 +305,13 @@ function binaryResultSetPacket(result, tableSchema, baseSeq) {
|
|
|
273
305
|
if (type === MYSQL_TYPE_LONG || type === MYSQL_TYPE_LONGLONG || type === MYSQL_TYPE_DOUBLE) {
|
|
274
306
|
const num = Number(v);
|
|
275
307
|
if (type === MYSQL_TYPE_DOUBLE) {
|
|
308
|
+
if (!Number.isFinite(num)) return encodeLenencString(String(v));
|
|
276
309
|
const b = Buffer.alloc(8); b.writeDoubleLE(num, 0); return b;
|
|
277
310
|
}
|
|
278
311
|
if (Number.isInteger(num) && num <= 2147483647 && num >= -2147483648) {
|
|
279
312
|
const b = Buffer.alloc(4); b.writeInt32LE(num, 0); return b;
|
|
280
313
|
}
|
|
314
|
+
if (!Number.isFinite(num)) return encodeLenencString(String(v));
|
|
281
315
|
const b = Buffer.alloc(8); b.writeBigInt64LE(BigInt(Math.trunc(num)), 0); return b;
|
|
282
316
|
}
|
|
283
317
|
if (type === MYSQL_TYPE_TINY) {
|
|
@@ -339,6 +373,14 @@ class MysqlConnection {
|
|
|
339
373
|
this.multiStatements = false;
|
|
340
374
|
this._stmts = new Map();
|
|
341
375
|
this._stmtSeq = 0;
|
|
376
|
+
this.session = {
|
|
377
|
+
lastInsertId: 0,
|
|
378
|
+
rowCount: 0,
|
|
379
|
+
foundRows: 0,
|
|
380
|
+
connectionId: server._connectionCounter,
|
|
381
|
+
currentDb: null,
|
|
382
|
+
sysvars: {},
|
|
383
|
+
};
|
|
342
384
|
if (server.handshakeTimeout > 0) {
|
|
343
385
|
this._authTimer = setTimeout(() => {
|
|
344
386
|
if (!this.authenticated) {
|
|
@@ -804,10 +846,12 @@ class MysqlConnection {
|
|
|
804
846
|
allowComments: this.server.allowComments,
|
|
805
847
|
safety: this.server.safety,
|
|
806
848
|
maxStatements: 1,
|
|
849
|
+
session: this.session,
|
|
807
850
|
});
|
|
808
851
|
if (r.type === 'select' || r.type === 'showTables' || r.type === 'showDatabases' || r.type === 'describe'
|
|
809
852
|
|| r.type === 'showColumns' || r.type === 'showIndex' || r.type === 'showCreateTable'
|
|
810
853
|
|| r.type === 'showVariables' || r.type === 'showStatus' || r.type === 'showGrants' || r.type === 'showWarnings') {
|
|
854
|
+
this.session.foundRows = (r.raw && r.raw.length !== undefined) ? r.raw.length : (r.rows ? r.rows.length : 0);
|
|
811
855
|
let schema = null;
|
|
812
856
|
if (r.table) {
|
|
813
857
|
schema = engine.getTableSchema
|
|
@@ -820,6 +864,12 @@ class MysqlConnection {
|
|
|
820
864
|
this.sequence = sequence;
|
|
821
865
|
this.socket.write(Buffer.concat(packets));
|
|
822
866
|
} else {
|
|
867
|
+
if (r.type === 'insert') {
|
|
868
|
+
this.session.lastInsertId = r.insertId;
|
|
869
|
+
this.session.rowCount = r.affectedRows;
|
|
870
|
+
} else if (r.type === 'update' || r.type === 'delete' || r.type === 'truncate') {
|
|
871
|
+
this.session.rowCount = r.affectedRows;
|
|
872
|
+
}
|
|
823
873
|
this._send(okPacket(r.affectedRows || 0, r.insertId || 0));
|
|
824
874
|
}
|
|
825
875
|
}
|