jsql-neo 6.0.2 → 6.0.4

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` | 两者 |
@@ -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) {