jsql-neo 6.0.4 → 6.0.5
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 +126 -30
- package/package.json +1 -1
package/lib/migrate.js
CHANGED
|
@@ -12,6 +12,15 @@ const path = require('path');
|
|
|
12
12
|
const { splitStatements, executeSQL } = require('./sql');
|
|
13
13
|
const { parseFieldShorthand } = require('./database');
|
|
14
14
|
|
|
15
|
+
// 体积护栏(CWE-770):与 lib/sql.js 的 DEFAULT_MAX_SQL_LENGTH 同风格。
|
|
16
|
+
// 可用 opts.maxInputSize / opts.maxRows 覆盖;传 Infinity 关闭;chunkSize 控制每批 insert 行数。
|
|
17
|
+
const DEFAULT_MAX_CSV_LENGTH = 64 * 1024 * 1024; // 64 MiB
|
|
18
|
+
const DEFAULT_MAX_JSON_LENGTH = 64 * 1024 * 1024;
|
|
19
|
+
const DEFAULT_MAX_SQL_LENGTH = 64 * 1024 * 1024; // 与 lib/sql.js 保持一致
|
|
20
|
+
const DEFAULT_MAX_ROWS = 2_000_000; // 200 万行
|
|
21
|
+
const DEFAULT_MAX_STATEMENTS = 200_000; // dump 文件最多允许 20 万条语句
|
|
22
|
+
const DEFAULT_CHUNK_SIZE = 1000;
|
|
23
|
+
|
|
15
24
|
function normalizeSchema(schema) {
|
|
16
25
|
const out = {};
|
|
17
26
|
for (const [name, def] of Object.entries(schema || {})) {
|
|
@@ -52,10 +61,31 @@ function parseValue(str, type) {
|
|
|
52
61
|
return str;
|
|
53
62
|
}
|
|
54
63
|
|
|
55
|
-
function parseCSV(text) {
|
|
64
|
+
function parseCSV(text, opts = {}) {
|
|
65
|
+
if (typeof text !== 'string') {
|
|
66
|
+
throw new Error('parseCSV: csv must be a string');
|
|
67
|
+
}
|
|
68
|
+
const maxLen = opts.maxInputSize == null ? DEFAULT_MAX_CSV_LENGTH : opts.maxInputSize;
|
|
69
|
+
if (typeof maxLen === 'number' && Number.isFinite(maxLen) && text.length > maxLen) {
|
|
70
|
+
throw new Error(
|
|
71
|
+
`CSV text too large: ${text.length} chars exceeds limit of ${maxLen} ` +
|
|
72
|
+
`(raise opts.maxInputSize to allow larger input)`
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
const maxRows = opts.maxRows == null ? DEFAULT_MAX_ROWS : opts.maxRows;
|
|
76
|
+
const isFiniteMaxRows = typeof maxRows === 'number' && Number.isFinite(maxRows);
|
|
77
|
+
|
|
56
78
|
const rows = [];
|
|
79
|
+
// 把 `field += c` 改为数组收集中间 join —— 与 splitStatements 同修 O(n²)
|
|
80
|
+
let fieldBuf = [];
|
|
81
|
+
const flushField = () => { const s = fieldBuf.join(''); fieldBuf.length = 0; return s; };
|
|
57
82
|
let row = [];
|
|
58
|
-
|
|
83
|
+
const flushRow = () => {
|
|
84
|
+
const r = row;
|
|
85
|
+
row = [];
|
|
86
|
+
rows.push(r);
|
|
87
|
+
return r;
|
|
88
|
+
};
|
|
59
89
|
let inQuotes = false;
|
|
60
90
|
let i = 0;
|
|
61
91
|
const n = text.length;
|
|
@@ -63,32 +93,48 @@ function parseCSV(text) {
|
|
|
63
93
|
const c = text[i];
|
|
64
94
|
if (inQuotes) {
|
|
65
95
|
if (c === '"') {
|
|
66
|
-
if (text[i + 1] === '"') {
|
|
96
|
+
if (text[i + 1] === '"') { fieldBuf.push('"'); i += 2; continue; }
|
|
67
97
|
inQuotes = false;
|
|
68
98
|
i++;
|
|
69
99
|
continue;
|
|
70
100
|
}
|
|
71
|
-
|
|
101
|
+
fieldBuf.push(c);
|
|
72
102
|
i++;
|
|
73
103
|
continue;
|
|
74
104
|
}
|
|
75
|
-
if (c === '"' &&
|
|
76
|
-
if (c === ',') { row.push(
|
|
105
|
+
if (c === '"' && fieldBuf.length === 0) { inQuotes = true; i++; continue; }
|
|
106
|
+
if (c === ',') { row.push(flushField()); i++; continue; }
|
|
77
107
|
if (c === '\n' || c === '\r') {
|
|
78
108
|
if (c === '\r' && text[i + 1] === '\n') i++;
|
|
79
|
-
row.push(
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
109
|
+
row.push(flushField());
|
|
110
|
+
if (row.length > 1 || row[0] !== '') {
|
|
111
|
+
if (isFiniteMaxRows && rows.length + 1 > maxRows) {
|
|
112
|
+
throw new Error(
|
|
113
|
+
`CSV too many rows: ${rows.length + 1} exceeds limit of ${maxRows} ` +
|
|
114
|
+
`(raise opts.maxRows or split the file)`
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
flushRow();
|
|
118
|
+
} else {
|
|
119
|
+
row = [];
|
|
120
|
+
}
|
|
83
121
|
i++;
|
|
84
122
|
continue;
|
|
85
123
|
}
|
|
86
|
-
|
|
124
|
+
fieldBuf.push(c);
|
|
87
125
|
i++;
|
|
88
126
|
}
|
|
89
|
-
if (
|
|
90
|
-
row.push(
|
|
91
|
-
if (row.length > 1 || row[0] !== '')
|
|
127
|
+
if (fieldBuf.length > 0 || row.length > 0) {
|
|
128
|
+
row.push(flushField());
|
|
129
|
+
if (row.length > 1 || row[0] !== '') {
|
|
130
|
+
if (isFiniteMaxRows && rows.length + 1 > maxRows) {
|
|
131
|
+
throw new Error(
|
|
132
|
+
`CSV too many rows: ${rows.length + 1} exceeds limit of ${maxRows} ` +
|
|
133
|
+
`(raise opts.maxRows or split the file)`
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
flushRow();
|
|
137
|
+
}
|
|
92
138
|
}
|
|
93
139
|
return rows;
|
|
94
140
|
}
|
|
@@ -123,7 +169,23 @@ async function exportAllToJSON(engine, tables) {
|
|
|
123
169
|
|
|
124
170
|
async function importFromJSON(engine, data, opts = {}) {
|
|
125
171
|
const overwrite = !!(opts && opts.overwrite);
|
|
126
|
-
let tables
|
|
172
|
+
let tables;
|
|
173
|
+
if (typeof data === 'string') {
|
|
174
|
+
// JSON.parse 吞超长输入也会打爆堆 —— 先加体积护栏,错误带位置
|
|
175
|
+
const maxLen = opts.maxInputSize == null ? DEFAULT_MAX_JSON_LENGTH : opts.maxInputSize;
|
|
176
|
+
if (typeof maxLen === 'number' && Number.isFinite(maxLen) && data.length > maxLen) {
|
|
177
|
+
throw new Error(
|
|
178
|
+
`JSON text too large: ${data.length} chars exceeds limit of ${maxLen} ` +
|
|
179
|
+
`(raise opts.maxInputSize to allow larger input)`
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
try { tables = JSON.parse(data); }
|
|
183
|
+
catch (e) {
|
|
184
|
+
throw new Error(`importFromJSON: invalid JSON: ${e.message}`);
|
|
185
|
+
}
|
|
186
|
+
} else {
|
|
187
|
+
tables = data;
|
|
188
|
+
}
|
|
127
189
|
// 兼容单表形状:exportTableToJSON 返回 { table, schema, rows }
|
|
128
190
|
if (tables && !Array.isArray(tables) && typeof tables === 'object'
|
|
129
191
|
&& tables.table && tables.schema && !tables[tables.table]) {
|
|
@@ -142,8 +204,13 @@ async function importFromJSON(engine, data, opts = {}) {
|
|
|
142
204
|
await engine.createTable(name, normalizeSchema(t.schema));
|
|
143
205
|
created.push(name);
|
|
144
206
|
if (Array.isArray(t.rows) && t.rows.length > 0) {
|
|
145
|
-
|
|
146
|
-
|
|
207
|
+
// 分块 insert —— 避免全量 rows.map 先攒再 insert
|
|
208
|
+
const chunkSize = opts.chunkSize != null ? opts.chunkSize : DEFAULT_CHUNK_SIZE;
|
|
209
|
+
for (let i = 0; i < t.rows.length; i += chunkSize) {
|
|
210
|
+
const chunk = t.rows.slice(i, i + chunkSize).map(r => ({ ...r }));
|
|
211
|
+
const ids = await engine.insert(name, chunk);
|
|
212
|
+
inserted += Array.isArray(ids) ? ids.length : chunk.length;
|
|
213
|
+
}
|
|
147
214
|
}
|
|
148
215
|
}
|
|
149
216
|
return { created, inserted };
|
|
@@ -161,7 +228,8 @@ async function exportTableToCSV(engine, table) {
|
|
|
161
228
|
|
|
162
229
|
async function importFromCSV(engine, table, csv, opts = {}) {
|
|
163
230
|
const schema = opts.schema || await engine.getTableSchema(table);
|
|
164
|
-
|
|
231
|
+
// parseCSV 内已有体积/行数护栏,这里传 opts.maxInputSize / opts.maxRows
|
|
232
|
+
const rows = parseCSV(csv, opts);
|
|
165
233
|
if (rows.length === 0) return { inserted: 0 };
|
|
166
234
|
let columns;
|
|
167
235
|
let start = 0;
|
|
@@ -179,24 +247,43 @@ async function importFromCSV(engine, table, csv, opts = {}) {
|
|
|
179
247
|
}
|
|
180
248
|
await engine.createTable(table, normalizeSchema(schema));
|
|
181
249
|
}
|
|
182
|
-
const
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
250
|
+
const chunkSize = opts.chunkSize != null ? opts.chunkSize : DEFAULT_CHUNK_SIZE;
|
|
251
|
+
let inserted = 0;
|
|
252
|
+
// 不攒 dataRows 全量,边解析边分块写入 —— 避免 2M 行全在内存里
|
|
253
|
+
for (let i = start; i < rows.length; i += chunkSize) {
|
|
254
|
+
const chunk = rows.slice(i, i + chunkSize);
|
|
255
|
+
const dataRows = chunk.map(raw => {
|
|
256
|
+
const row = {};
|
|
257
|
+
for (let j = 0; j < columns.length; j++) {
|
|
258
|
+
const type = schema && schema[columns[j]] ? schema[columns[j]].type : 'string';
|
|
259
|
+
row[columns[j]] = parseValue(raw[j], type);
|
|
260
|
+
}
|
|
261
|
+
if (row.id === null || row.id === undefined || row.id === '') delete row.id;
|
|
262
|
+
return row;
|
|
263
|
+
});
|
|
264
|
+
const ids = await engine.insert(table, dataRows);
|
|
265
|
+
inserted += Array.isArray(ids) ? ids.length : dataRows.length;
|
|
191
266
|
}
|
|
192
|
-
|
|
193
|
-
return { inserted: dataRows.length, ids };
|
|
267
|
+
return { inserted };
|
|
194
268
|
}
|
|
195
269
|
|
|
196
270
|
/* ---------- mysqldump ---------- */
|
|
197
271
|
|
|
198
272
|
async function importDump(engine, sqlText, opts = {}) {
|
|
199
|
-
|
|
273
|
+
// splitStatements 自身有体积护栏,但这里要让用户能通过 opts.maxInputSize 覆盖,
|
|
274
|
+
// 还要额外加「语句数量」护栏 —— 64 MiB 里可能塞几百万条小 INSERT,每条都合法但合起来打爆堆。
|
|
275
|
+
const maxLen = opts.maxInputSize == null ? DEFAULT_MAX_SQL_LENGTH : opts.maxInputSize;
|
|
276
|
+
const splitOpts = { maxLength: maxLen };
|
|
277
|
+
const statements = splitStatements(sqlText, splitOpts);
|
|
278
|
+
|
|
279
|
+
const maxStmts = opts.maxStatements == null ? DEFAULT_MAX_STATEMENTS : opts.maxStatements;
|
|
280
|
+
if (typeof maxStmts === 'number' && Number.isFinite(maxStmts) && statements.length > maxStmts) {
|
|
281
|
+
throw new Error(
|
|
282
|
+
`SQL dump too many statements: ${statements.length} exceeds limit of ${maxStmts} ` +
|
|
283
|
+
`(raise opts.maxStatements or split the dump file)`
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
|
|
200
287
|
const created = [];
|
|
201
288
|
let inserted = 0;
|
|
202
289
|
const errors = [];
|
|
@@ -221,6 +308,15 @@ async function importDump(engine, sqlText, opts = {}) {
|
|
|
221
308
|
}
|
|
222
309
|
|
|
223
310
|
async function importDumpFile(engine, filePath, opts = {}) {
|
|
311
|
+
// 先看文件大小再决定是否读 —— 避免 fs.readFileSync 把超大 dump 一次性吃进堆
|
|
312
|
+
const maxLen = opts.maxInputSize == null ? DEFAULT_MAX_SQL_LENGTH : opts.maxInputSize;
|
|
313
|
+
const stat = fs.statSync(filePath);
|
|
314
|
+
if (typeof maxLen === 'number' && Number.isFinite(maxLen) && stat.size > maxLen) {
|
|
315
|
+
throw new Error(
|
|
316
|
+
`Dump file too large: ${stat.size} bytes exceeds limit of ${maxLen} ` +
|
|
317
|
+
`(raise opts.maxInputSize or split the file)`
|
|
318
|
+
);
|
|
319
|
+
}
|
|
224
320
|
const text = fs.readFileSync(filePath, 'utf8');
|
|
225
321
|
return importDump(engine, text, opts);
|
|
226
322
|
}
|