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 +32 -20
- package/lib/date-types.js +30 -13
- package/lib/migrate.js +126 -30
- package/lib/sql.js +722 -83
- package/package.json +1 -1
- package/postinstall.js +61 -17
- package/test/readme-audit.test.js +186 -4
package/README.md
CHANGED
|
@@ -835,7 +835,7 @@ CREATE TABLE IF NOT EXISTS orders (
|
|
|
835
835
|
|
|
836
836
|
**SELECT grammar**
|
|
837
837
|
|
|
838
|
-
```
|
|
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);
|
|
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
|
|
1106
|
-
>
|
|
1107
|
-
> `~`
|
|
1108
|
-
>
|
|
1110
|
+
> **运算符方言说明。** `^` 与 `~` 在 MySQL 与 PostgreSQL 中含义冲突,本实现按上下文区分:
|
|
1111
|
+
> - `^` 统一为**按位异或**(MySQL 语义);幂运算请用 `POWER(x, n)`。
|
|
1112
|
+
> - `~` 在**单目**位置是按位取反(MySQL),在**双目**位置是正则匹配(PG),
|
|
1113
|
+
> 另有 `~*`(不敏感)、`!~`、`!~*`。
|
|
1114
|
+
> - 需要正则时也可用 `REGEXP` / `RLIKE`;PG 的 `^`(幂)**暂不支持**。
|
|
1109
1115
|
|
|
1110
|
-
**JSON (PG style)** —
|
|
1111
|
-
|
|
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
|
-
```
|
|
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
|
-
|
|
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` |
|
|
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
|
|
55
|
+
return toLocalDateString(value);
|
|
39
56
|
}
|
|
40
57
|
if (typeof value === 'number') {
|
|
41
58
|
// Unix timestamp → date
|
|
42
|
-
return new Date(value * 1000)
|
|
59
|
+
return toLocalDateString(new Date(value * 1000));
|
|
43
60
|
}
|
|
44
61
|
if (typeof value === 'string') {
|
|
45
|
-
const
|
|
46
|
-
|
|
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
|
|
102
|
+
return toLocalDateTimeString(value);
|
|
83
103
|
}
|
|
84
104
|
if (typeof value === 'number') {
|
|
85
|
-
return new Date(value * 1000)
|
|
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
|
|
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
|
-
|
|
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
|
}
|