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.
Files changed (2) hide show
  1. package/lib/migrate.js +126 -30
  2. 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
- let field = '';
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] === '"') { field += '"'; i += 2; continue; }
96
+ if (text[i + 1] === '"') { fieldBuf.push('"'); i += 2; continue; }
67
97
  inQuotes = false;
68
98
  i++;
69
99
  continue;
70
100
  }
71
- field += c;
101
+ fieldBuf.push(c);
72
102
  i++;
73
103
  continue;
74
104
  }
75
- if (c === '"' && field === '') { inQuotes = true; i++; continue; }
76
- if (c === ',') { row.push(field); field = ''; i++; continue; }
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(field);
80
- field = '';
81
- if (row.length > 1 || row[0] !== '') rows.push(row);
82
- row = [];
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
- field += c;
124
+ fieldBuf.push(c);
87
125
  i++;
88
126
  }
89
- if (field !== '' || row.length > 0) {
90
- row.push(field);
91
- if (row.length > 1 || row[0] !== '') rows.push(row);
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 = typeof data === 'string' ? JSON.parse(data) : data;
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
- const ids = await engine.insert(name, t.rows.map(r => ({ ...r })));
146
- inserted += Array.isArray(ids) ? ids.length : t.rows.length;
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
- const rows = parseCSV(csv);
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 dataRows = [];
183
- for (let i = start; i < rows.length; i++) {
184
- const row = {};
185
- for (let j = 0; j < columns.length; j++) {
186
- const type = schema && schema[columns[j]] ? schema[columns[j]].type : 'string';
187
- row[columns[j]] = parseValue(rows[i][j], type);
188
- }
189
- if (row.id === null || row.id === undefined || row.id === '') delete row.id;
190
- dataRows.push(row);
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
- const ids = dataRows.length > 0 ? await engine.insert(table, dataRows) : [];
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
- const statements = splitStatements(sqlText);
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jsql-neo",
3
- "version": "6.0.4",
3
+ "version": "6.0.5",
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",