jsql-neo 6.1.0 → 6.3.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 CHANGED
@@ -835,7 +835,7 @@ CREATE TABLE IF NOT EXISTS orders (
835
835
 
836
836
  **SELECT grammar**
837
837
 
838
- ```sql
838
+ ```text
839
839
  WITH [RECURSIVE] cte_name [(col, ...)] AS (SELECT ...)[, ...] -- CTE,见下节
840
840
  SELECT [DISTINCT] select_list
841
841
  FROM table_reference
@@ -1056,10 +1056,15 @@ Coercion rules: numeric strings compare as numbers (`'5' = 5`), `NULL = NULL`
1056
1056
  `DAYOFWEEK`, `DAYNAME`, `MONTHNAME`, `UNIX_TIMESTAMP`, `FROM_UNIXTIME`.
1057
1057
 
1058
1058
  ```sql
1059
- SELECT DATE_ADD('2026-08-12', INTERVAL 1 DAY); -- '2026-08-13 00:00:00'
1059
+ SELECT DATE_ADD('2026-08-12', INTERVAL 1 DAY); -- '2026-08-13'(纯日期进 → 纯日期出)
1060
+ SELECT DATE_ADD('2026-08-12 10:00:00', INTERVAL 1 DAY); -- '2026-08-13 10:00:00'
1060
1061
  SELECT DATE_FORMAT(NOW(), '%Y-%m-%d %H:%i'); -- '2026-08-12 10:30'
1061
1062
  SELECT EXTRACT(YEAR FROM '2026-08-12'); -- 2026
1062
1063
  SELECT AGE('2026-08-12', '2020-01-01'); -- '6 years 7 mons 11 days'
1064
+ SELECT DATEDIFF('2026-08-12', '2026-08-01'); -- 11
1065
+ SELECT TIMESTAMPDIFF(MONTH, '2026-01-01', '2026-08-01'); -- 7(end - start)
1066
+ SELECT TO_DATE('2026-08-12', 'YYYY-MM-DD'); -- '2026-08-12'(PG 格式串)
1067
+ SELECT DATE_TRUNC('month', '2026-08-12 10:30:00'); -- '2026-08-01 00:00:00'
1063
1068
  ```
1064
1069
 
1065
1070
  **Conditional / null** — `IF(cond, a, b)`, `IIF`, `IFNULL(a, b)`, `COALESCE(...)`, `NULLIF`,
@@ -1102,14 +1107,14 @@ ORDER BY avg_sal DESC;
1102
1107
 
1103
1108
  **Bitwise** — `& | ^ ~ << >>`(`^` 是按位异或,`~` 是按位取反)。
1104
1109
 
1105
- > **运算符方言说明。** `^` 与 `~` 在 MySQL(位运算)和 PostgreSQL(`^` 为幂、
1106
- > `~` 为正则匹配)中含义冲突,本实现统一采用 **MySQL 语义**:`^` = 按位异或、
1107
- > `~` = 按位取反。需要幂运算请用 `POWER(x, n)`,需要正则请用 `REGEXP`。
1108
- > 因此 PG 风格的 `^`(幂)与 `~ ~* !~ !~*`(正则)**暂不支持**。
1110
+ > **运算符方言说明。** `^` 与 `~` 在 MySQL 与 PostgreSQL 中含义冲突,本实现按上下文区分:
1111
+ > - `^` 统一为**按位异或**(MySQL 语义);幂运算请用 `POWER(x, n)`。
1112
+ > - `~` 在**单目**位置是按位取反(MySQL),在**双目**位置是正则匹配(PG),
1113
+ > 另有 `~*`(不敏感)、`!~`、`!~*`。
1114
+ > - 需要正则时也可用 `REGEXP` / `RLIKE`;PG 的 `^`(幂)**暂不支持**。
1109
1115
 
1110
- **JSON (PG style)** — `->`(返回 JSON)、`->>`(返回文本),可链式
1111
- (`meta->'addr'->>'city'`)。`#>` `#>>`(路径)、`@>` `<@`(包含)、
1112
- `?` `?|` `?&`(键存在)**暂不支持**。
1116
+ **JSON (PG style)** — `->` / `->>`(取字段)、`#>` / `#>>`(路径取值)、`@>` / `<@`(包含)、
1117
+ `?` / `?|` / `?&`(键存在),可链式(`meta->'addr'->>'city'`)。
1113
1118
 
1114
1119
  ### LIKE / ILIKE / regex
1115
1120
 
@@ -1162,7 +1167,7 @@ INSERT INTO users (id, meta) VALUES
1162
1167
  (1, '{"name":"Alice","tags":["admin","ops"],"addr":{"city":"SH"}}');
1163
1168
 
1164
1169
  SELECT meta->>'name' AS name, meta->'addr'->>'city' AS city
1165
- FROM users;
1170
+ FROM users WHERE meta @> '{"tags":["admin"]}';
1166
1171
  ```
1167
1172
 
1168
1173
  JSON columns are fully interoperable with Mongo document views.
@@ -3138,7 +3143,7 @@ DELETE FROM orders WHERE status = 'cancelled' LIMIT 100;
3138
3143
 
3139
3144
  #### 查询语法 SELECT
3140
3145
 
3141
- ```sql
3146
+ ```text
3142
3147
  SELECT [DISTINCT] select_list
3143
3148
  FROM table_reference
3144
3149
  [JOIN table_reference ON condition]
@@ -3445,11 +3450,16 @@ INTERVAL 单位:`DAY` `HOUR` `MINUTE` `SECOND` `WEEK` `MONTH` `YEAR` 及其组
3445
3450
  |---|---|---|
3446
3451
  | `->` | 取 JSON 字段(返回 JSON) | `meta->'name'` |
3447
3452
  | `->>` | 取 JSON 字段(返回文本) | `meta->>'name'` |
3453
+ | `#>` | 路径取值(返回 JSON) | `meta#>'{addr,city}'` |
3454
+ | `#>>` | 路径取值(返回文本) | `meta#>>'{addr,city}'` |
3455
+ | `@>` | 包含 | `meta @> '{"plan":"pro"}'` |
3456
+ | `<@` | 被包含 | `'{"a":1}' <@ meta` |
3457
+ | `?` | 含指定键 | `meta ? 'plan'` |
3458
+ | `?\|` | 含任一键 | `meta ?\| '{a,b}'` |
3459
+ | `?&` | 含全部键 | `meta ?& '{a,b}'` |
3448
3460
 
3449
3461
  可链式取值:`meta->'addr'->>'city'`。
3450
3462
 
3451
- > `#>` `#>>`(路径访问)、`@>` `<@`(包含)、`?` `?|` `?&`(键存在)**暂不支持** —— 使用时会抛出明确错误,不会静默退化成别的运算。
3452
-
3453
3463
  ### LIKE / ILIKE / 正则
3454
3464
 
3455
3465
  **LIKE 通配符**
@@ -3466,8 +3476,8 @@ INTERVAL 单位:`DAY` `HOUR` `MINUTE` `SECOND` `WEEK` `MONTH` `YEAR` 及其组
3466
3476
 
3467
3477
  - 基于 JS RegExp 引擎
3468
3478
  - 支持 flags:`i`(忽略大小写)、`m`(多行)、`s`(点匹配换行)
3469
- - PG 风格的 `~` / `~*` / `!~` / `!~*` 运算符**暂不支持**(`~` 已被用作按位取反)。
3470
- 正则匹配请用 `REGEXP` / `RLIKE`。
3479
+ - PG 风格的 `~`(大小写敏感)/ `~*`(不敏感)/ `!~` / `!~*` 双目运算符也支持;
3480
+ 单目位置的 `~` 仍是按位取反。
3471
3481
 
3472
3482
  ```sql
3473
3483
  SELECT * FROM users
@@ -3570,7 +3580,7 @@ INSERT INTO users (id, meta) VALUES
3570
3580
  -- 查询路径
3571
3581
  SELECT meta->>'name' AS name,
3572
3582
  meta->'addr'->>'city' AS city
3573
- FROM users;
3583
+ FROM users WHERE meta @> '{"tags":["admin"]}';
3574
3584
  ```
3575
3585
 
3576
3586
  JSON 函数见 [系统与杂项函数](#系统与杂项函数)。Mongo 客户端视角下,JSON 列与 BSON 文档完全互通。
@@ -5798,6 +5808,8 @@ expr := literal
5798
5808
  | expr [NOT] LIKE pattern [ESCAPE char]
5799
5809
  | expr [NOT] ILIKE pattern
5800
5810
  | expr [NOT] RLIKE pattern | expr [NOT] REGEXP pattern
5811
+ | expr ~ pattern | expr ~* pattern | expr !~ pattern | expr !~* pattern
5812
+ | expr ? key | expr ?| keys | expr ?& keys
5801
5813
  | expr IS [NOT] NULL | expr IS [NOT] TRUE | expr IS [NOT] FALSE
5802
5814
  | expr [NOT] IN ( ... )
5803
5815
  | expr [NOT] ANY '(' select_statement ')'
@@ -5843,12 +5855,12 @@ function_call := func_name '(' [DISTINCT] args ')' -- 普通/聚合
5843
5855
  ### 运算符优先级(从高到低)
5844
5856
 
5845
5857
  ```
5846
- 1. () . [] -> ->> ::
5858
+ 1. () . [] -> ->> #> #>> ::
5847
5859
  2. CAST 一元 + - ~
5848
5860
  3. ^ * / % DIV MOD
5849
5861
  4. + -
5850
5862
  5. << >> & | ^(位)
5851
- 6. = <> != < <= > >= <=> BETWEEN IN LIKE ILIKE RLIKE REGEXP
5863
+ 6. = <> != < <= > >= <=> @> <@ ? ?| ?& BETWEEN IN LIKE ILIKE RLIKE REGEXP ~ ~* !~ !~*
5852
5864
  7. NOT
5853
5865
  8. AND &&
5854
5866
  9. XOR
@@ -7445,12 +7457,12 @@ jsql import ./data app.sql
7445
7457
  | 冲突处理 | `ON DUPLICATE KEY UPDATE` | `ON CONFLICT ... DO ...` | 两者都支持 |
7446
7458
  | 占位符 | `?` | `$1, $2` | 两者(分协议) |
7447
7459
  | 模糊匹配 | `LIKE`(大小写敏感) | `LIKE` + `ILIKE` | 两者 |
7448
- | 正则 | `REGEXP` | `REGEXP` | 两者 + `RLIKE` |
7460
+ | 正则 | `REGEXP` | `~` `~*` | 两者 + `RLIKE` |
7449
7461
  | 标识符引用 | 反引号 `` ` `` | 双引号 `"` | 两者 |
7450
7462
  | 字符串字面量 | 单引号 | 单引号 | 单引号(双引号按标识符,PG 语义) |
7451
7463
  | 布尔 | `TRUE/FALSE`(1/0) | `t/f`(三值逻辑) | 兼容两者 |
7452
7464
  | 分页 | `LIMIT off, n` | `LIMIT n OFFSET off` | 两者 |
7453
- | JSON 访问 | `JSON_EXTRACT` | `->` `->>` | 两者 |
7465
+ | JSON 访问 | `JSON_EXTRACT` | `->` `->>` `#>>` | 两者 |
7454
7466
  | 类型转换 | `CAST`/`CONVERT` | `::` | 三者 |
7455
7467
  | 条件分支 | `IF()` | `CASE` | `IF` + `CASE` + `IIF` |
7456
7468
  | 空值回退 | `IFNULL()` | `COALESCE` | 两者 |
package/lib/date-types.js CHANGED
@@ -33,17 +33,37 @@ function validateDateType(value, type, fieldName) {
33
33
  // DATE: YYYY-MM-DD
34
34
  // ============================================================
35
35
 
36
+ function pad2(n) { return String(n).padStart(2, '0'); }
37
+
38
+ /**
39
+ * 本地时区的 'YYYY-MM-DD'。
40
+ * 不能用 toISOString().slice(0,10) —— 那是 UTC:东八区 2023-01-01 00:00 本地
41
+ * 会是 2022-12-31T16:00Z,切出来整整差一天(2024-02-29 → 2024-02-28)。
42
+ */
43
+ function toLocalDateString(d) {
44
+ return d.getFullYear() + '-' + pad2(d.getMonth() + 1) + '-' + pad2(d.getDate());
45
+ }
46
+
47
+ /** 本地时区的 'YYYY-MM-DD HH:MM:SS'(同理不能用 toISOString) */
48
+ function toLocalDateTimeString(d) {
49
+ return toLocalDateString(d) + ' ' +
50
+ pad2(d.getHours()) + ':' + pad2(d.getMinutes()) + ':' + pad2(d.getSeconds());
51
+ }
52
+
36
53
  function validateDate(value, fieldName) {
37
54
  if (value instanceof Date) {
38
- return value.toISOString().slice(0, 10);
55
+ return toLocalDateString(value);
39
56
  }
40
57
  if (typeof value === 'number') {
41
58
  // Unix timestamp → date
42
- return new Date(value * 1000).toISOString().slice(0, 10);
59
+ return toLocalDateString(new Date(value * 1000));
43
60
  }
44
61
  if (typeof value === 'string') {
45
- const d = parseDateString(value);
46
- if (d) return d.toISOString().slice(0, 10);
62
+ const s = value.trim();
63
+ // 已是 YYYY-MM-DD:直接返回,避免本地→UTC 往返导致差一天
64
+ if (/^\d{4}-\d{2}-\d{2}$/.test(s)) return s;
65
+ const d = parseDateString(s);
66
+ if (d) return toLocalDateString(d);
47
67
  }
48
68
  throw createError('ER_INVALID_DATE', fieldName, String(value));
49
69
  }
@@ -79,26 +99,23 @@ function parseDateString(str) {
79
99
 
80
100
  function validateDateTime(value, fieldName) {
81
101
  if (value instanceof Date) {
82
- return value.toISOString().replace('T', ' ').slice(0, 19);
102
+ return toLocalDateTimeString(value);
83
103
  }
84
104
  if (typeof value === 'number') {
85
- return new Date(value * 1000).toISOString().replace('T', ' ').slice(0, 19);
105
+ return toLocalDateTimeString(new Date(value * 1000));
86
106
  }
87
107
  if (typeof value === 'string') {
88
- // ISO format: 2024-01-15T10:30:00.000Z
108
+ // ISO format: 2024-01-15T10:30:00.000Z(带时区标记,按字面取日期+时间)
89
109
  const isoMatch = value.match(/^(\d{4}-\d{2}-\d{2})T(\d{2}:\d{2}:\d{2})/);
90
110
  if (isoMatch) {
91
111
  return isoMatch[1] + ' ' + isoMatch[2];
92
112
  }
93
- // MySQL format: YYYY-MM-DD HH:MM:SS
113
+ // MySQL format: YYYY-MM-DD HH:MM:SS —— 已是目标格式,原样返回
94
114
  const myMatch = value.match(/^(\d{4}-\d{2}-\d{2}) (\d{2}:\d{2}:\d{2})$/);
95
- if (myMatch) {
96
- const d = new Date(value.replace(' ', 'T') + 'Z');
97
- if (!isNaN(d.getTime())) return value;
98
- }
115
+ if (myMatch) return value;
99
116
  const d = new Date(value);
100
117
  if (!isNaN(d.getTime())) {
101
- return d.toISOString().replace('T', ' ').slice(0, 19);
118
+ return toLocalDateTimeString(d);
102
119
  }
103
120
  }
104
121
  throw createError('ER_INVALID_DATE', fieldName, String(value));
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
  }