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 +40 -35
- package/lib/native_client.js +69 -8
- package/lib/sql.js +523 -65
- package/native/jsql-neo-native.node +0 -0
- package/nativesrc/jsql-neo-native/Cargo.lock +2 -2
- package/package.json +1 -1
- package/test/readme-audit.test.js +153 -14
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
|
|
@@ -1102,24 +1102,24 @@ ORDER BY avg_sal DESC;
|
|
|
1102
1102
|
|
|
1103
1103
|
**Bitwise** — `& | ^ ~ << >>`(`^` 是按位异或,`~` 是按位取反)。
|
|
1104
1104
|
|
|
1105
|
-
> **运算符方言说明。** `^` 与 `~` 在 MySQL
|
|
1106
|
-
>
|
|
1107
|
-
> `~`
|
|
1108
|
-
>
|
|
1109
|
-
>
|
|
1105
|
+
> **运算符方言说明。** `^` 与 `~` 在 MySQL 与 PostgreSQL 中含义冲突,本实现按上下文区分:
|
|
1106
|
+
> - `^` 统一为**按位异或**(MySQL 语义);幂运算请用 `POWER(x, n)`。
|
|
1107
|
+
> - `~` 在**单目**位置是按位取反(MySQL),在**双目**位置是正则匹配(PG),
|
|
1108
|
+
> 另有 `~*`(不敏感)、`!~`、`!~*`。
|
|
1109
|
+
> - 需要正则时也可用 `REGEXP` / `RLIKE`;PG 的 `^`(幂)**暂不支持**。
|
|
1110
1110
|
|
|
1111
|
-
**JSON (PG style)** — `->`
|
|
1112
|
-
|
|
1111
|
+
**JSON (PG style)** — `->` / `->>`(取字段)、`#>` / `#>>`(路径取值)、`@>` / `<@`(包含)、
|
|
1112
|
+
`?` / `?|` / `?&`(键存在),可链式(`meta->'addr'->>'city'`)。
|
|
1113
1113
|
|
|
1114
1114
|
### LIKE / ILIKE / regex
|
|
1115
1115
|
|
|
1116
|
-
- `%` any length, `_` one char
|
|
1117
|
-
- `ILIKE` = LIKE
|
|
1118
|
-
- `REGEXP
|
|
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
|
|
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: ['
|
|
1273
|
+
columnTypes: ['INT', 'VARCHAR(255)', 'INT'],
|
|
1274
1274
|
rows: [[1, 'Alice', 30]],
|
|
1275
|
-
rowCount:
|
|
1276
|
-
message: '
|
|
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
|
|
1282
|
-
`
|
|
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
|
-
```
|
|
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
|
-
|
|
|
3448
|
+
| `#>` | 路径取值(返回 JSON) | `meta#>'{addr,city}'` |
|
|
3449
|
+
| `#>>` | 路径取值(返回文本) | `meta#>>'{addr,city}'` |
|
|
3449
3450
|
| `@>` | 包含 | `meta @> '{"plan":"pro"}'` |
|
|
3450
|
-
| `<@` | 被包含 | `'{"a":1}'
|
|
3451
|
-
| `?`
|
|
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
|
-
|
|
3471
|
-
- `name ~* '^a'`(不敏感)
|
|
3472
|
-
- `name !~ '^X'`(不匹配)
|
|
3474
|
+
- PG 风格的 `~`(大小写敏感)/ `~*`(不敏感)/ `!~` / `!~*` 双目运算符也支持;
|
|
3475
|
+
单目位置的 `~` 仍是按位取反。
|
|
3473
3476
|
|
|
3474
3477
|
```sql
|
|
3475
3478
|
SELECT * FROM users
|
|
3476
|
-
WHERE email
|
|
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
|
|
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.
|
|
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.
|
|
5858
|
-
10.
|
|
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/native_client.js
CHANGED
|
@@ -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
|
-
|
|
403
|
-
|
|
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
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
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) {
|