jsql-neo 6.0.2 → 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/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
@@ -1102,24 +1102,24 @@ ORDER BY avg_sal DESC;
1102
1102
 
1103
1103
  **Bitwise** — `& | ^ ~ << >>`(`^` 是按位异或,`~` 是按位取反)。
1104
1104
 
1105
- > **运算符方言说明。** `^` 与 `~` 在 MySQL(位运算)和 PostgreSQL(`^` 为幂、
1106
- > `~` 为正则匹配)中含义冲突,本实现统一采用 **MySQL 语义**:`^` = 按位异或、
1107
- > `~` = 按位取反。需要幂运算请用 `POWER(x, n)`,需要正则请用 `REGEXP` / `~*`。
1108
- > 因此 PG 风格的 `^`(幂)、`~ ~* !~ !~*`(正则)与 JSON 操作符
1109
- > (`-> ->> #> #>> @> <@`)**暂不支持**。
1105
+ > **运算符方言说明。** `^` 与 `~` 在 MySQL 与 PostgreSQL 中含义冲突,本实现按上下文区分:
1106
+ > - `^` 统一为**按位异或**(MySQL 语义);幂运算请用 `POWER(x, n)`。
1107
+ > - `~` 在**单目**位置是按位取反(MySQL),在**双目**位置是正则匹配(PG),
1108
+ > 另有 `~*`(不敏感)、`!~`、`!~*`。
1109
+ > - 需要正则时也可用 `REGEXP` / `RLIKE`;PG 的 `^`(幂)**暂不支持**。
1110
1110
 
1111
- **JSON (PG style)** — `->` (JSON result), `->>` (text), `#>`, `#>>` (paths), `@>` `<@`
1112
- (containment), `?` `?|` `?&` (key existence).
1111
+ **JSON (PG style)** — `->` / `->>`(取字段)、`#>` / `#>>`(路径取值)、`@>` / `<@`(包含)、
1112
+ `?` / `?|` / `?&`(键存在),可链式(`meta->'addr'->>'city'`)。
1113
1113
 
1114
1114
  ### LIKE / ILIKE / regex
1115
1115
 
1116
- - `%` any length, `_` one char, `\` escape (`ESCAPE` clause)
1117
- - `ILIKE` = LIKE, case-insensitive
1118
- - `REGEXP`/`RLIKE`/`~` use the JS RegExp engine; flags `i/m/s`
1116
+ - `%` any length, `_` one char;`ESCAPE '!'` 可自定义转义符(默认 `\`)
1117
+ - `ILIKE` = LIKE,大小写不敏感
1118
+ - `REGEXP` / `RLIKE` 使用 JS RegExp 引擎;flags `i/m/s`
1119
1119
 
1120
1120
  ```sql
1121
1121
  SELECT * FROM users
1122
- WHERE email ~* '^[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,}$';
1122
+ WHERE email REGEXP '^[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,}$';
1123
1123
  ```
1124
1124
 
1125
1125
  ### Indexes & constraints
@@ -1270,16 +1270,17 @@ const result = await executeSQL(db, sql, params);
1270
1270
  ```js
1271
1271
  {
1272
1272
  columns: ['id', 'name', 'age'],
1273
- columnTypes: ['INTEGER', 'VARCHAR', 'INTEGER'],
1273
+ columnTypes: ['INT', 'VARCHAR(255)', 'INT'],
1274
1274
  rows: [[1, 'Alice', 30]],
1275
- rowCount: 2, affectedRows: 0, insertId: 1,
1276
- message: '2 rows selected', command: 'SELECT',
1275
+ rowCount: 1, affectedRows: 0,
1276
+ message: '1 row selected', command: 'SELECT',
1277
1277
  durationMs: 0.42, warnings: [],
1278
1278
  }
1279
1279
  ```
1280
1280
 
1281
- Result matrix: SELECT → `rows`; INSERT → `affectedRows` + `insertId`; UPDATE/DELETE →
1282
- `affectedRows`; DDL/txn → `message`. Params: positional `?`, named `:name`, or object maps.
1281
+ Result matrix: SELECT → `rows`(另有 `columnTypes`、`rowCount`、`command`、`message`、
1282
+ `durationMs`、`warnings`);INSERT → `affectedRows` + `insertId`(另有 `ids`);
1283
+ UPDATE/DELETE → `affectedRows`;DDL/txn → `message`。 Params: positional `?`, named `:name`, or object maps.
1283
1284
  Multi-statement supported; `SQLExecutor` class runs batched SQL from a string/stream.
1284
1285
 
1285
1286
  #### 参数绑定:两种方式
@@ -3137,7 +3138,7 @@ DELETE FROM orders WHERE status = 'cancelled' LIMIT 100;
3137
3138
 
3138
3139
  #### 查询语法 SELECT
3139
3140
 
3140
- ```sql
3141
+ ```text
3141
3142
  SELECT [DISTINCT] select_list
3142
3143
  FROM table_reference
3143
3144
  [JOIN table_reference ON condition]
@@ -3403,7 +3404,6 @@ INTERVAL 单位:`DAY` `HOUR` `MINUTE` `SECOND` `WEEK` `MONTH` `YEAR` 及其组
3403
3404
  | `+` `-` `*` `/` | 四则 | `(a + b) * 2` |
3404
3405
  | `%` / `MOD` | 取模 | `a % 3` |
3405
3406
  | `DIV` | 整数除法(MySQL) | `7 DIV 2` → 3 |
3406
- | `^` | 幂(PG) | `2 ^ 10` → 1024 |
3407
3407
 
3408
3408
  #### 比较运算符
3409
3409
 
@@ -3445,10 +3445,15 @@ INTERVAL 单位:`DAY` `HOUR` `MINUTE` `SECOND` `WEEK` `MONTH` `YEAR` 及其组
3445
3445
  |---|---|---|
3446
3446
  | `->` | 取 JSON 字段(返回 JSON) | `meta->'name'` |
3447
3447
  | `->>` | 取 JSON 字段(返回文本) | `meta->>'name'` |
3448
- | `#>`, `#>>` | 路径访问 | `meta#>>'{a,b}'` |
3448
+ | `#>` | 路径取值(返回 JSON) | `meta#>'{addr,city}'` |
3449
+ | `#>>` | 路径取值(返回文本) | `meta#>>'{addr,city}'` |
3449
3450
  | `@>` | 包含 | `meta @> '{"plan":"pro"}'` |
3450
- | `<@` | 被包含 | `'{"a":1}'::jsonb <@ meta` |
3451
- | `?` / `?|` / `?&` | 键存在 | `meta ? 'plan'` |
3451
+ | `<@` | 被包含 | `'{"a":1}' <@ meta` |
3452
+ | `?` | 含指定键 | `meta ? 'plan'` |
3453
+ | `?\|` | 含任一键 | `meta ?\| '{a,b}'` |
3454
+ | `?&` | 含全部键 | `meta ?& '{a,b}'` |
3455
+
3456
+ 可链式取值:`meta->'addr'->>'city'`。
3452
3457
 
3453
3458
  ### LIKE / ILIKE / 正则
3454
3459
 
@@ -3466,14 +3471,12 @@ INTERVAL 单位:`DAY` `HOUR` `MINUTE` `SECOND` `WEEK` `MONTH` `YEAR` 及其组
3466
3471
 
3467
3472
  - 基于 JS RegExp 引擎
3468
3473
  - 支持 flags:`i`(忽略大小写)、`m`(多行)、`s`(点匹配换行)
3469
- - PG 风格的 `~` / `~*` / `!~` / `!~*` 运算符也支持:
3470
- - `name ~ '^A'`(大小写敏感匹配)
3471
- - `name ~* '^a'`(不敏感)
3472
- - `name !~ '^X'`(不匹配)
3474
+ - PG 风格的 `~`(大小写敏感)/ `~*`(不敏感)/ `!~` / `!~*` 双目运算符也支持;
3475
+ 单目位置的 `~` 仍是按位取反。
3473
3476
 
3474
3477
  ```sql
3475
3478
  SELECT * FROM users
3476
- WHERE email ~* '^[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,}$';
3479
+ WHERE email REGEXP '^[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,}$';
3477
3480
  ```
3478
3481
 
3479
3482
  ### 事务 Transactions
@@ -5800,7 +5803,8 @@ expr := literal
5800
5803
  | expr [NOT] LIKE pattern [ESCAPE char]
5801
5804
  | expr [NOT] ILIKE pattern
5802
5805
  | expr [NOT] RLIKE pattern | expr [NOT] REGEXP pattern
5803
- | expr [NOT] ~ pattern | expr [NOT] ~* pattern | expr !~ pattern
5806
+ | expr ~ pattern | expr ~* pattern | expr !~ pattern | expr !~* pattern
5807
+ | expr ? key | expr ?| keys | expr ?& keys
5804
5808
  | expr IS [NOT] NULL | expr IS [NOT] TRUE | expr IS [NOT] FALSE
5805
5809
  | expr [NOT] IN ( ... )
5806
5810
  | expr [NOT] ANY '(' select_statement ')'
@@ -5846,16 +5850,17 @@ function_call := func_name '(' [DISTINCT] args ')' -- 普通/聚合
5846
5850
  ### 运算符优先级(从高到低)
5847
5851
 
5848
5852
  ```
5849
- 1. () . [] -> ->> #> #>>
5850
- 2. :: CAST 一元 + - ~
5853
+ 1. () . [] -> ->> #> #>> ::
5854
+ 2. CAST 一元 + - ~
5851
5855
  3. ^ * / % DIV MOD
5852
5856
  4. + -
5853
5857
  5. << >> & | ^(位)
5854
- 6. = <> != < <= > >= <=> BETWEEN IN LIKE ILIKE RLIKE REGEXP ~ ~* @> <@ ?
5858
+ 6. = <> != < <= > >= <=> @> <@ ? ?| ?& BETWEEN IN LIKE ILIKE RLIKE REGEXP ~ ~* !~ !~*
5855
5859
  7. NOT
5856
- 8. AND
5857
- 9. OR XOR
5858
- 10. 三元(IF / CASE 解析为函数)
5860
+ 8. AND &&
5861
+ 9. XOR
5862
+ 10. OR ||
5863
+ 11. 三元(IF / CASE 解析为函数)
5859
5864
  ```
5860
5865
 
5861
5866
  ### 关键词语法表
@@ -7452,7 +7457,7 @@ jsql import ./data app.sql
7452
7457
  | 字符串字面量 | 单引号 | 单引号 | 单引号(双引号按标识符,PG 语义) |
7453
7458
  | 布尔 | `TRUE/FALSE`(1/0) | `t/f`(三值逻辑) | 兼容两者 |
7454
7459
  | 分页 | `LIMIT off, n` | `LIMIT n OFFSET off` | 两者 |
7455
- | JSON 访问 | `JSON_EXTRACT` | `->` `->>` | 两者 + `#>>` |
7460
+ | JSON 访问 | `JSON_EXTRACT` | `->` `->>` `#>>` | 两者 |
7456
7461
  | 类型转换 | `CAST`/`CONVERT` | `::` | 三者 |
7457
7462
  | 条件分支 | `IF()` | `CASE` | `IF` + `CASE` + `IIF` |
7458
7463
  | 空值回退 | `IFNULL()` | `COALESCE` | 两者 |
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
  }
@@ -17,6 +17,33 @@ function safeParse(str, fallback) {
17
17
  try { return JSON.parse(str); } catch (e) { return fallback; }
18
18
  }
19
19
 
20
+ /**
21
+ * 序列化对象/数组字段(JSON 列)并给出可读错误。直接 JSON.stringify 遇到循环引用会抛
22
+ * "Converting circular structure to JSON",指向不到具体列,这里补上表/列信息。
23
+ * 与 lib/wasm_client.js 的 safeStringify 保持一致。
24
+ */
25
+ function safeStringify(value, table, column) {
26
+ try {
27
+ return JSON.stringify(value);
28
+ } catch (e) {
29
+ const msg = (e && e.message) ? e.message : String(e);
30
+ throw new Error(`Cannot serialize column '${column}' of table '${table}': ${msg}`);
31
+ }
32
+ }
33
+
34
+ /** 插入行必须是普通对象;null/undefined/数组/基本类型在这里就被拦下(与 lib/wasm_client.js 一致) */
35
+ function assertRowObject(row, index, isBatch) {
36
+ const ok = row !== null && row !== undefined && typeof row === 'object' &&
37
+ !Array.isArray(row) && !(row instanceof Date);
38
+ if (ok) return;
39
+ const got = row === null ? 'null'
40
+ : Array.isArray(row) ? 'array'
41
+ : row instanceof Date ? 'Date'
42
+ : typeof row;
43
+ const where = isBatch ? ` at index ${index}` : '';
44
+ throw new TypeError(`insert(): expected a row object${where}, got ${got}`);
45
+ }
46
+
20
47
  const NATIVE_TYPE_MAP = {
21
48
  text: 'string',
22
49
  varchar: 'string',
@@ -399,8 +426,17 @@ class JSQL {
399
426
  rows = rows.map(row => {
400
427
  const out = {};
401
428
  for (const [k, v] of Object.entries(row)) {
402
- if (v instanceof Date) out[k] = v.toISOString();
403
- else out[k] = v;
429
+ const def = schema[k];
430
+ let t = null;
431
+ if (typeof def === 'string') t = def.toLowerCase();
432
+ else if (def && typeof def === 'object' && def.type) t = String(def.type).toLowerCase();
433
+ if (v !== null && v !== undefined && ['json', 'array', 'object'].includes(t) && typeof v !== 'string') {
434
+ out[k] = safeStringify(v, table, k);
435
+ } else if (v instanceof Date) {
436
+ out[k] = v.toISOString();
437
+ } else {
438
+ out[k] = v;
439
+ }
404
440
  }
405
441
  return out;
406
442
  });
@@ -450,18 +486,39 @@ class JSQL {
450
486
  async _flush() {
451
487
  this._flushOpsNow();
452
488
  if (!this._runHooks('beforeFlush', [])) return null;
453
- const flushed = {};
454
- for (const [table, rows] of Object.entries(this._buffer)) {
455
- if (rows.length === 0) continue;
456
- const r = await this._insertBatch(table, rows);
457
- flushed[table] = r;
458
- }
489
+ // 先把整个 buffer 原子取出再写入。
490
+ // 旧写法是遍历 this._buffer、await 之后才清空 —— 期间其他协程继续往同一个数组
491
+ // push,同一批行会被多个 flush 重复写入(并发 insert 行数暴涨/丢行)。
492
+ // 与 lib/wasm_client.js 的修法保持一致。
493
+ const pending = this._buffer;
459
494
  this._buffer = {};
460
495
  this._bufferSize = 0;
496
+ const flushed = {};
497
+ try {
498
+ for (const [table, rows] of Object.entries(pending)) {
499
+ if (rows.length === 0) continue;
500
+ flushed[table] = await this._insertBatch(table, rows);
501
+ }
502
+ } catch (e) {
503
+ // 坏行不回写 buffer:否则 stop() 二次 flush 会再抛同一个错导致进程 exit 1
504
+ if (!this._flushErrors) this._flushErrors = [];
505
+ this._flushErrors.push({
506
+ at: Date.now(),
507
+ message: (e && e.message) ? e.message : String(e),
508
+ tables: Object.keys(pending),
509
+ rowCounts: Object.fromEntries(Object.entries(pending).map(([t, r]) => [t, r.length])),
510
+ });
511
+ throw e;
512
+ }
461
513
  this._runHooks('afterFlush', []);
462
514
  return flushed;
463
515
  }
464
516
 
517
+ /** 返回历次 flush 失败的记录(坏行不会回写 buffer,只能从这里查) */
518
+ lastFlushErrors() {
519
+ return (this._flushErrors || []).slice();
520
+ }
521
+
465
522
  _flushOpsNow() {
466
523
  if (this._opTimer) {
467
524
  clearTimeout(this._opTimer);
@@ -479,7 +536,11 @@ class JSQL {
479
536
  }
480
537
 
481
538
  async insert(table, data) {
539
+ // 入参校验:null / undefined / 非对象会在下游 Object.entries() 处炸成
540
+ // 难以定位的 "Cannot convert undefined or null to object",这里直接给明确 TypeError
541
+ // (与 lib/wasm_client.js 的 assertRowObject 保持一致)
482
542
  const arr = Array.isArray(data) ? data : [data];
543
+ arr.forEach((row, i) => assertRowObject(row, i, arr.length > 1 || Array.isArray(data)));
483
544
  var filtered = arr;
484
545
  if (!this._runHooks('beforeInsert', [table, filtered])) return [];
485
546
  if (arr.length > 1) {