jsql-neo 6.0.2 → 6.1.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 +33 -35
- package/lib/native_client.js +69 -8
- package/lib/sql.js +274 -32
- 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 +86 -14
package/README.md
CHANGED
|
@@ -1104,22 +1104,22 @@ ORDER BY avg_sal DESC;
|
|
|
1104
1104
|
|
|
1105
1105
|
> **运算符方言说明。** `^` 与 `~` 在 MySQL(位运算)和 PostgreSQL(`^` 为幂、
|
|
1106
1106
|
> `~` 为正则匹配)中含义冲突,本实现统一采用 **MySQL 语义**:`^` = 按位异或、
|
|
1107
|
-
> `~` = 按位取反。需要幂运算请用 `POWER(x, n)`,需要正则请用 `REGEXP
|
|
1108
|
-
> 因此 PG 风格的
|
|
1109
|
-
> (`-> ->> #> #>> @> <@`)**暂不支持**。
|
|
1107
|
+
> `~` = 按位取反。需要幂运算请用 `POWER(x, n)`,需要正则请用 `REGEXP`。
|
|
1108
|
+
> 因此 PG 风格的 `^`(幂)与 `~ ~* !~ !~*`(正则)**暂不支持**。
|
|
1110
1109
|
|
|
1111
|
-
**JSON (PG style)** —
|
|
1112
|
-
|
|
1110
|
+
**JSON (PG style)** — `->`(返回 JSON)、`->>`(返回文本),可链式
|
|
1111
|
+
(`meta->'addr'->>'city'`)。`#>` `#>>`(路径)、`@>` `<@`(包含)、
|
|
1112
|
+
`?` `?|` `?&`(键存在)**暂不支持**。
|
|
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
|
|
@@ -1162,7 +1162,7 @@ INSERT INTO users (id, meta) VALUES
|
|
|
1162
1162
|
(1, '{"name":"Alice","tags":["admin","ops"],"addr":{"city":"SH"}}');
|
|
1163
1163
|
|
|
1164
1164
|
SELECT meta->>'name' AS name, meta->'addr'->>'city' AS city
|
|
1165
|
-
FROM users
|
|
1165
|
+
FROM users;
|
|
1166
1166
|
```
|
|
1167
1167
|
|
|
1168
1168
|
JSON columns are fully interoperable with Mongo document views.
|
|
@@ -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
|
#### 参数绑定:两种方式
|
|
@@ -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,10 @@ INTERVAL 单位:`DAY` `HOUR` `MINUTE` `SECOND` `WEEK` `MONTH` `YEAR` 及其组
|
|
|
3445
3445
|
|---|---|---|
|
|
3446
3446
|
| `->` | 取 JSON 字段(返回 JSON) | `meta->'name'` |
|
|
3447
3447
|
| `->>` | 取 JSON 字段(返回文本) | `meta->>'name'` |
|
|
3448
|
-
|
|
3449
|
-
|
|
3450
|
-
|
|
3451
|
-
|
|
3448
|
+
|
|
3449
|
+
可链式取值:`meta->'addr'->>'city'`。
|
|
3450
|
+
|
|
3451
|
+
> `#>` `#>>`(路径访问)、`@>` `<@`(包含)、`?` `?|` `?&`(键存在)**暂不支持** —— 使用时会抛出明确错误,不会静默退化成别的运算。
|
|
3452
3452
|
|
|
3453
3453
|
### LIKE / ILIKE / 正则
|
|
3454
3454
|
|
|
@@ -3466,14 +3466,12 @@ INTERVAL 单位:`DAY` `HOUR` `MINUTE` `SECOND` `WEEK` `MONTH` `YEAR` 及其组
|
|
|
3466
3466
|
|
|
3467
3467
|
- 基于 JS RegExp 引擎
|
|
3468
3468
|
- 支持 flags:`i`(忽略大小写)、`m`(多行)、`s`(点匹配换行)
|
|
3469
|
-
- PG 风格的 `~` / `~*` / `!~` / `!~*`
|
|
3470
|
-
|
|
3471
|
-
- `name ~* '^a'`(不敏感)
|
|
3472
|
-
- `name !~ '^X'`(不匹配)
|
|
3469
|
+
- PG 风格的 `~` / `~*` / `!~` / `!~*` 运算符**暂不支持**(`~` 已被用作按位取反)。
|
|
3470
|
+
正则匹配请用 `REGEXP` / `RLIKE`。
|
|
3473
3471
|
|
|
3474
3472
|
```sql
|
|
3475
3473
|
SELECT * FROM users
|
|
3476
|
-
WHERE email
|
|
3474
|
+
WHERE email REGEXP '^[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,}$';
|
|
3477
3475
|
```
|
|
3478
3476
|
|
|
3479
3477
|
### 事务 Transactions
|
|
@@ -3572,7 +3570,7 @@ INSERT INTO users (id, meta) VALUES
|
|
|
3572
3570
|
-- 查询路径
|
|
3573
3571
|
SELECT meta->>'name' AS name,
|
|
3574
3572
|
meta->'addr'->>'city' AS city
|
|
3575
|
-
FROM users
|
|
3573
|
+
FROM users;
|
|
3576
3574
|
```
|
|
3577
3575
|
|
|
3578
3576
|
JSON 函数见 [系统与杂项函数](#系统与杂项函数)。Mongo 客户端视角下,JSON 列与 BSON 文档完全互通。
|
|
@@ -5800,7 +5798,6 @@ expr := literal
|
|
|
5800
5798
|
| expr [NOT] LIKE pattern [ESCAPE char]
|
|
5801
5799
|
| expr [NOT] ILIKE pattern
|
|
5802
5800
|
| expr [NOT] RLIKE pattern | expr [NOT] REGEXP pattern
|
|
5803
|
-
| expr [NOT] ~ pattern | expr [NOT] ~* pattern | expr !~ pattern
|
|
5804
5801
|
| expr IS [NOT] NULL | expr IS [NOT] TRUE | expr IS [NOT] FALSE
|
|
5805
5802
|
| expr [NOT] IN ( ... )
|
|
5806
5803
|
| expr [NOT] ANY '(' select_statement ')'
|
|
@@ -5846,16 +5843,17 @@ function_call := func_name '(' [DISTINCT] args ')' -- 普通/聚合
|
|
|
5846
5843
|
### 运算符优先级(从高到低)
|
|
5847
5844
|
|
|
5848
5845
|
```
|
|
5849
|
-
1. () . [] -> ->>
|
|
5850
|
-
2.
|
|
5846
|
+
1. () . [] -> ->> ::
|
|
5847
|
+
2. CAST 一元 + - ~
|
|
5851
5848
|
3. ^ * / % DIV MOD
|
|
5852
5849
|
4. + -
|
|
5853
5850
|
5. << >> & | ^(位)
|
|
5854
|
-
6. = <> != < <= > >= <=> BETWEEN IN LIKE ILIKE RLIKE REGEXP
|
|
5851
|
+
6. = <> != < <= > >= <=> BETWEEN IN LIKE ILIKE RLIKE REGEXP
|
|
5855
5852
|
7. NOT
|
|
5856
|
-
8. AND
|
|
5857
|
-
9.
|
|
5858
|
-
10.
|
|
5853
|
+
8. AND &&
|
|
5854
|
+
9. XOR
|
|
5855
|
+
10. OR ||
|
|
5856
|
+
11. 三元(IF / CASE 解析为函数)
|
|
5859
5857
|
```
|
|
5860
5858
|
|
|
5861
5859
|
### 关键词语法表
|
|
@@ -7447,12 +7445,12 @@ jsql import ./data app.sql
|
|
|
7447
7445
|
| 冲突处理 | `ON DUPLICATE KEY UPDATE` | `ON CONFLICT ... DO ...` | 两者都支持 |
|
|
7448
7446
|
| 占位符 | `?` | `$1, $2` | 两者(分协议) |
|
|
7449
7447
|
| 模糊匹配 | `LIKE`(大小写敏感) | `LIKE` + `ILIKE` | 两者 |
|
|
7450
|
-
| 正则 | `REGEXP` |
|
|
7448
|
+
| 正则 | `REGEXP` | `REGEXP` | 两者 + `RLIKE` |
|
|
7451
7449
|
| 标识符引用 | 反引号 `` ` `` | 双引号 `"` | 两者 |
|
|
7452
7450
|
| 字符串字面量 | 单引号 | 单引号 | 单引号(双引号按标识符,PG 语义) |
|
|
7453
7451
|
| 布尔 | `TRUE/FALSE`(1/0) | `t/f`(三值逻辑) | 兼容两者 |
|
|
7454
7452
|
| 分页 | `LIMIT off, n` | `LIMIT n OFFSET off` | 两者 |
|
|
7455
|
-
| JSON 访问 | `JSON_EXTRACT` | `->` `->>` | 两者
|
|
7453
|
+
| JSON 访问 | `JSON_EXTRACT` | `->` `->>` | 两者 |
|
|
7456
7454
|
| 类型转换 | `CAST`/`CONVERT` | `::` | 三者 |
|
|
7457
7455
|
| 条件分支 | `IF()` | `CASE` | `IF` + `CASE` + `IIF` |
|
|
7458
7456
|
| 空值回退 | `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) {
|
package/lib/sql.js
CHANGED
|
@@ -79,6 +79,11 @@ function tokenize(sql) {
|
|
|
79
79
|
while (i < n && sql[i] !== '\n') i++;
|
|
80
80
|
continue;
|
|
81
81
|
}
|
|
82
|
+
// PG 的 JSON 路径操作符 #> / #>> 尚未实现。
|
|
83
|
+
// 若不在这里拦截,'#' 会被当成行注释吃掉,导致 SQL 后半段被静默丢弃。
|
|
84
|
+
if (c === '#' && sql[i + 1] === '>') {
|
|
85
|
+
throw new Error("JSON 路径操作符 '#>' / '#>>' 暂不支持,请改用 '->' / '->>'");
|
|
86
|
+
}
|
|
82
87
|
if (c === '#' || (c === '/' && sql[i + 1] === '*')) {
|
|
83
88
|
if (c === '#') { while (i < n && sql[i] !== '\n') i++; continue; }
|
|
84
89
|
i += 2;
|
|
@@ -95,7 +100,10 @@ function tokenize(sql) {
|
|
|
95
100
|
if (sql[j] === '\\' && j + 1 < n) {
|
|
96
101
|
const esc = sql[j + 1];
|
|
97
102
|
const map = { n: '\n', t: '\t', r: '\r', '0': '\0', "'": "'", '"': '"', '\\': '\\', b: '\b', Z: '\x1a', a: '\a' };
|
|
98
|
-
|
|
103
|
+
// MySQL 语义:\% 与 \_ 保留反斜杠(供 LIKE 匹配字面量通配符),其余未知转义忽略反斜杠
|
|
104
|
+
if (map[esc] !== undefined) str += map[esc];
|
|
105
|
+
else if (esc === '%' || esc === '_') str += '\\' + esc;
|
|
106
|
+
else str += esc;
|
|
99
107
|
j += 2;
|
|
100
108
|
} else if (sql[j] === quote) {
|
|
101
109
|
break;
|
|
@@ -150,6 +158,12 @@ function tokenize(sql) {
|
|
|
150
158
|
continue;
|
|
151
159
|
}
|
|
152
160
|
|
|
161
|
+
// PG 的 JSON 包含操作符 @> 尚未实现。
|
|
162
|
+
// 若不拦截,'@' 会被 sysvar 分支静默吞掉、'>' 退化成普通大于号,产生错误结果。
|
|
163
|
+
if (c === '@' && sql[i + 1] === '>') {
|
|
164
|
+
throw new Error("JSON 包含操作符 '@>' 暂不支持");
|
|
165
|
+
}
|
|
166
|
+
|
|
153
167
|
if (c === '@') {
|
|
154
168
|
let j = i;
|
|
155
169
|
while (j < n && sql[j] === '@') j++;
|
|
@@ -177,9 +191,20 @@ function tokenize(sql) {
|
|
|
177
191
|
continue;
|
|
178
192
|
}
|
|
179
193
|
|
|
194
|
+
const three = sql.slice(i, i + 3);
|
|
195
|
+
if (sql.slice(i, i + 2) === '<@') {
|
|
196
|
+
throw new Error("JSON 包含操作符 '<@' 暂不支持");
|
|
197
|
+
}
|
|
198
|
+
if (three === '->>' || three === '<=>') {
|
|
199
|
+
tokens.push(new SQLToken('op', three, i));
|
|
200
|
+
i += 3;
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
|
|
180
204
|
const two = sql.slice(i, i + 2);
|
|
181
205
|
if (two === '<=' || two === '>=' || two === '!=' || two === '<>' || two === '==' ||
|
|
182
|
-
two === '<<' || two === '>>'
|
|
206
|
+
two === '<<' || two === '>>' || two === '->' || two === '::' ||
|
|
207
|
+
two === '&&' || two === '||') {
|
|
183
208
|
tokens.push(new SQLToken('op', two, i));
|
|
184
209
|
i += 2;
|
|
185
210
|
continue;
|
|
@@ -523,19 +548,27 @@ class Parser {
|
|
|
523
548
|
parseColumnDef() {
|
|
524
549
|
const def = {};
|
|
525
550
|
const typeTok = this.next();
|
|
526
|
-
|
|
527
|
-
|
|
551
|
+
const typeRaw = String(typeTok.value);
|
|
552
|
+
// 这些类型不在 KEYWORDS 里(避免影响同名标识符),按上下文识别
|
|
553
|
+
const IDENT_TYPES = /^(BIG)?SERIAL$|^JSONB$|^TIMESTAMPTZ$|^BYTEA$|^UUID$/i;
|
|
554
|
+
const isIdentType = IDENT_TYPES.test(typeRaw);
|
|
555
|
+
if (typeTok.type !== 'keyword' && !isIdentType) throw new Error(`Expected column type, got '${typeTok.value}'`);
|
|
556
|
+
const type = typeRaw.toLowerCase();
|
|
528
557
|
const typeMap = {
|
|
529
558
|
integer: 'integer', int: 'integer', bigint: 'integer', tinyint: 'integer', smallint: 'integer',
|
|
530
559
|
string: 'string', text: 'string', varchar: 'string', char: 'string',
|
|
531
560
|
float: 'number', double: 'number', real: 'number', numeric: 'number', decimal: 'number',
|
|
532
561
|
boolean: 'boolean', bool: 'boolean',
|
|
533
|
-
date: 'date', datetime: 'datetime', timestamp: 'timestamp',
|
|
534
|
-
any: 'any', object: 'object', json: 'object', array: 'array'
|
|
562
|
+
date: 'date', datetime: 'datetime', timestamp: 'timestamp', timestamptz: 'timestamp',
|
|
563
|
+
any: 'any', object: 'object', json: 'object', jsonb: 'object', array: 'array',
|
|
564
|
+
bytea: 'string', uuid: 'string',
|
|
565
|
+
// PG 自增序列类型:等价于整型 + 自增 + NOT NULL
|
|
566
|
+
serial: 'integer', bigserial: 'integer'
|
|
535
567
|
};
|
|
536
568
|
const mapped = typeMap[type];
|
|
537
569
|
if (!mapped) throw new Error(`Unsupported column type: ${typeTok.value}`);
|
|
538
570
|
def.type = mapped;
|
|
571
|
+
if (/^(BIG)?SERIAL$/i.test(typeRaw)) { def.autoIncrement = true; def.required = true; }
|
|
539
572
|
|
|
540
573
|
// 长度限制: TEXT(255) / VARCHAR(100) / INTEGER(11) ...
|
|
541
574
|
if (this.peek().type === 'op' && this.peek().value === '(') {
|
|
@@ -598,6 +631,11 @@ class Parser {
|
|
|
598
631
|
def.defaultExpr = true;
|
|
599
632
|
} else {
|
|
600
633
|
def.default = this.parseValue();
|
|
634
|
+
// 允许字面量后带 :: 类型转换:DEFAULT '{}'::jsonb
|
|
635
|
+
if (this.peek().type === 'op' && this.peek().value === '::') {
|
|
636
|
+
this.next();
|
|
637
|
+
if (this.peek().type === 'keyword' || this.peek().type === 'ident') this.next();
|
|
638
|
+
}
|
|
601
639
|
}
|
|
602
640
|
break;
|
|
603
641
|
case 'COLLATE':
|
|
@@ -772,7 +810,7 @@ class Parser {
|
|
|
772
810
|
for (;;) {
|
|
773
811
|
const col = this.parseColumnRef();
|
|
774
812
|
this.expect('op', '=');
|
|
775
|
-
const val = this.
|
|
813
|
+
const val = this.parseConflictValue();
|
|
776
814
|
sets.push([col, val]);
|
|
777
815
|
if (this.peek().value === ',') { this.next(); continue; }
|
|
778
816
|
break;
|
|
@@ -852,6 +890,21 @@ class Parser {
|
|
|
852
890
|
throw new Error(`Expected value, got '${t.value}'`);
|
|
853
891
|
}
|
|
854
892
|
|
|
893
|
+
/**
|
|
894
|
+
* ON CONFLICT DO UPDATE 赋值右侧:
|
|
895
|
+
* EXCLUDED.col —— PG:引用"本次本应插入的值"
|
|
896
|
+
* VALUES(col) —— MySQL 同义(在 parseValue 中处理)
|
|
897
|
+
* 其他字面量
|
|
898
|
+
*/
|
|
899
|
+
parseConflictValue() {
|
|
900
|
+
if (this.isWord('EXCLUDED') && this.peek(1).type === 'op' && this.peek(1).value === '.') {
|
|
901
|
+
this.next(); this.next();
|
|
902
|
+
const col = this.parseColumnRef();
|
|
903
|
+
return { _excludedOf: col };
|
|
904
|
+
}
|
|
905
|
+
return this.parseValue();
|
|
906
|
+
}
|
|
907
|
+
|
|
855
908
|
/**
|
|
856
909
|
* WITH [RECURSIVE] name [(col, ...)] AS (SELECT ...), ... <statement>
|
|
857
910
|
* CTE 在语法层解析为 { type:'with', ctes:[{name, columns, select}], statement }。
|
|
@@ -1628,18 +1681,33 @@ class Parser {
|
|
|
1628
1681
|
}
|
|
1629
1682
|
|
|
1630
1683
|
parseOr() {
|
|
1684
|
+
let left = this.parseXor();
|
|
1685
|
+
for (;;) {
|
|
1686
|
+
const isOr = this.isKeyword('OR') || (this.peek().type === 'op' && this.peek().value === '||');
|
|
1687
|
+
if (!isOr) break;
|
|
1688
|
+
this.next();
|
|
1689
|
+
const right = this.parseXor();
|
|
1690
|
+
left = { type: 'or', left, right };
|
|
1691
|
+
}
|
|
1692
|
+
return left;
|
|
1693
|
+
}
|
|
1694
|
+
|
|
1695
|
+
// 逻辑异或 XOR(MySQL 优先级:OR < XOR < AND)
|
|
1696
|
+
parseXor() {
|
|
1631
1697
|
let left = this.parseAnd();
|
|
1632
|
-
while (this.
|
|
1698
|
+
while (this.isWord('XOR')) {
|
|
1633
1699
|
this.next();
|
|
1634
1700
|
const right = this.parseAnd();
|
|
1635
|
-
left = { type: '
|
|
1701
|
+
left = { type: 'xor', left, right };
|
|
1636
1702
|
}
|
|
1637
1703
|
return left;
|
|
1638
1704
|
}
|
|
1639
1705
|
|
|
1640
1706
|
parseAnd() {
|
|
1641
1707
|
let left = this.parseNot();
|
|
1642
|
-
|
|
1708
|
+
for (;;) {
|
|
1709
|
+
const isAnd = this.isKeyword('AND') || (this.peek().type === 'op' && this.peek().value === '&&');
|
|
1710
|
+
if (!isAnd) break;
|
|
1643
1711
|
this.next();
|
|
1644
1712
|
const right = this.parseNot();
|
|
1645
1713
|
left = { type: 'and', left, right };
|
|
@@ -1981,15 +2049,58 @@ class Parser {
|
|
|
1981
2049
|
}
|
|
1982
2050
|
|
|
1983
2051
|
parseTerm() {
|
|
1984
|
-
let node = this.parseOperand();
|
|
2052
|
+
let node = this.parsePostfix(this.parseOperand());
|
|
1985
2053
|
for (;;) {
|
|
1986
2054
|
const t = this.peek();
|
|
1987
2055
|
if (t.type === 'op' && (t.value === '*' || t.value === '/' || t.value === '%')) {
|
|
1988
2056
|
this.next();
|
|
1989
|
-
const right = this.parseOperand();
|
|
2057
|
+
const right = this.parsePostfix(this.parseOperand());
|
|
1990
2058
|
node = { type: 'arith', op: t.value, left: node, right };
|
|
1991
2059
|
continue;
|
|
1992
2060
|
}
|
|
2061
|
+
// MySQL 整数除法:7 DIV 2 → 3
|
|
2062
|
+
if (this.isWord('DIV')) {
|
|
2063
|
+
this.next();
|
|
2064
|
+
const right = this.parsePostfix(this.parseOperand());
|
|
2065
|
+
node = { type: 'arith', op: 'DIV', left: node, right };
|
|
2066
|
+
continue;
|
|
2067
|
+
}
|
|
2068
|
+
break;
|
|
2069
|
+
}
|
|
2070
|
+
return node;
|
|
2071
|
+
}
|
|
2072
|
+
|
|
2073
|
+
/**
|
|
2074
|
+
* 后缀运算符(绑定最紧):PG 风格 `::` 类型转换、`->` / `->>` JSON 取值。
|
|
2075
|
+
* 例:'5'::INT、meta->>'name'、meta->'addr'->>'city'。
|
|
2076
|
+
*/
|
|
2077
|
+
parsePostfix(node) {
|
|
2078
|
+
for (;;) {
|
|
2079
|
+
const t = this.peek();
|
|
2080
|
+
if (t.type === 'op' && t.value === '::') {
|
|
2081
|
+
this.next();
|
|
2082
|
+
const typeTok = this.next();
|
|
2083
|
+
let dataType = String(typeTok.value).toUpperCase();
|
|
2084
|
+
if (this.peek().type === 'op' && this.peek().value === '(') {
|
|
2085
|
+
this.next();
|
|
2086
|
+
let depth = 1, spec = '';
|
|
2087
|
+
while (depth > 0 && this.peek().type !== 'eof') {
|
|
2088
|
+
const tk = this.next();
|
|
2089
|
+
if (tk.type === 'op' && tk.value === '(') depth++;
|
|
2090
|
+
else if (tk.type === 'op' && tk.value === ')') { depth--; if (depth === 0) break; }
|
|
2091
|
+
spec += tk.value;
|
|
2092
|
+
}
|
|
2093
|
+
dataType += '(' + spec + ')';
|
|
2094
|
+
}
|
|
2095
|
+
node = { type: 'cast', expr: node, dataType };
|
|
2096
|
+
continue;
|
|
2097
|
+
}
|
|
2098
|
+
if (t.type === 'op' && (t.value === '->' || t.value === '->>')) {
|
|
2099
|
+
this.next();
|
|
2100
|
+
const key = this.parseValue();
|
|
2101
|
+
node = { type: 'jsonAccess', operand: node, key, asText: t.value === '->>' };
|
|
2102
|
+
continue;
|
|
2103
|
+
}
|
|
1993
2104
|
break;
|
|
1994
2105
|
}
|
|
1995
2106
|
return node;
|
|
@@ -2085,7 +2196,10 @@ class Parser {
|
|
|
2085
2196
|
const isILike = !(t2.type === 'keyword' && t2.value === 'LIKE');
|
|
2086
2197
|
this.next();
|
|
2087
2198
|
const pattern = this.parseValue();
|
|
2088
|
-
|
|
2199
|
+
// 可选 ESCAPE 子句:LIKE '...' ESCAPE '!'
|
|
2200
|
+
let escape = null;
|
|
2201
|
+
if (this.isWord('ESCAPE')) { this.next(); escape = this.parseValue(); }
|
|
2202
|
+
return { type: 'like', operand: left, pattern, not, ci: isILike, escape };
|
|
2089
2203
|
}
|
|
2090
2204
|
|
|
2091
2205
|
// MySQL 中 RLIKE 是 REGEXP 的同义词
|
|
@@ -2095,7 +2209,7 @@ class Parser {
|
|
|
2095
2209
|
return { type: 'regexp', operand: left, pattern, not };
|
|
2096
2210
|
}
|
|
2097
2211
|
|
|
2098
|
-
if (t2.type === 'op' && ['=', '!=', '<>', '<', '<=', '>', '>='].includes(t2.value)) {
|
|
2212
|
+
if (t2.type === 'op' && ['=', '!=', '<>', '<', '<=', '>', '>=', '<=>'].includes(t2.value)) {
|
|
2099
2213
|
this.next();
|
|
2100
2214
|
// = ANY (SELECT ...) / > ALL (SELECT ...)
|
|
2101
2215
|
const anyAll = this.isWord('ANY') || this.isWord('ALL') || this.isWord('SOME');
|
|
@@ -2209,6 +2323,8 @@ function resolveOperand(operand, row, ctx) {
|
|
|
2209
2323
|
case '*': return l * r;
|
|
2210
2324
|
case '/': return r === 0 ? null : l / r;
|
|
2211
2325
|
case '%': return r === 0 ? null : l % r;
|
|
2326
|
+
// MySQL 整数除法 DIV(结果向零取整)
|
|
2327
|
+
case 'DIV': return r === 0 ? null : Math.trunc(Number(l) / Number(r));
|
|
2212
2328
|
// 位运算:先按整数归一,与 MySQL 的整数位运算语义一致
|
|
2213
2329
|
case '&': return (Number(l) | 0) & (Number(r) | 0);
|
|
2214
2330
|
case '|': return (Number(l) | 0) | (Number(r) | 0);
|
|
@@ -2229,6 +2345,7 @@ function resolveOperand(operand, row, ctx) {
|
|
|
2229
2345
|
case 'compare':
|
|
2230
2346
|
case 'and':
|
|
2231
2347
|
case 'or':
|
|
2348
|
+
case 'xor':
|
|
2232
2349
|
case 'not':
|
|
2233
2350
|
case 'in':
|
|
2234
2351
|
case 'isNull':
|
|
@@ -2253,6 +2370,30 @@ function resolveOperand(operand, row, ctx) {
|
|
|
2253
2370
|
const v = resolveOperand(operand.expr, row, ctx);
|
|
2254
2371
|
return applyCast(v, operand.dataType);
|
|
2255
2372
|
}
|
|
2373
|
+
case 'jsonAccess': {
|
|
2374
|
+
let base = resolveOperand(operand.operand, row, ctx);
|
|
2375
|
+
if (base === null || base === undefined) return null;
|
|
2376
|
+
// JSON 列可能以字符串形式存储,先解析
|
|
2377
|
+
if (typeof base === 'string') {
|
|
2378
|
+
try { base = JSON.parse(base); } catch (e) { return null; }
|
|
2379
|
+
}
|
|
2380
|
+
const key = operand.key;
|
|
2381
|
+
let v;
|
|
2382
|
+
if (Array.isArray(base)) {
|
|
2383
|
+
const idx = Number(key);
|
|
2384
|
+
v = Number.isInteger(idx) ? (idx < 0 ? base[base.length + idx] : base[idx]) : undefined;
|
|
2385
|
+
} else if (base && typeof base === 'object') {
|
|
2386
|
+
v = base[key];
|
|
2387
|
+
} else {
|
|
2388
|
+
return null;
|
|
2389
|
+
}
|
|
2390
|
+
if (v === undefined) return null;
|
|
2391
|
+
if (operand.asText) {
|
|
2392
|
+
if (v === null) return null;
|
|
2393
|
+
return typeof v === 'object' ? JSON.stringify(v) : String(v);
|
|
2394
|
+
}
|
|
2395
|
+
return v;
|
|
2396
|
+
}
|
|
2256
2397
|
case 'quantified': {
|
|
2257
2398
|
// = ANY / > ALL (SELECT ...) —— 需先由执行层物化成 operand._list
|
|
2258
2399
|
const v = resolveOperand(operand.operand, row, ctx);
|
|
@@ -2471,13 +2612,25 @@ function seedRand(seed) {
|
|
|
2471
2612
|
}
|
|
2472
2613
|
|
|
2473
2614
|
// ci 默认 true:保持 LIKE 既有的大小写不敏感行为不变;ILIKE 同为不敏感。
|
|
2474
|
-
|
|
2615
|
+
// escape:可选转义符(ESCAPE 子句),默认反斜杠;转义符后跟 % _ 或转义符本身按字面量匹配。
|
|
2616
|
+
function likeMatch(value, pattern, ci = true, escape = '\\') {
|
|
2475
2617
|
if (typeof value !== 'string') return false;
|
|
2476
|
-
const
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
|
|
2618
|
+
const p = String(pattern);
|
|
2619
|
+
const e = (escape === null || escape === undefined || escape === '') ? '\\' : String(escape);
|
|
2620
|
+
const reEsc = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
2621
|
+
let out = '';
|
|
2622
|
+
for (let i = 0; i < p.length; i++) {
|
|
2623
|
+
const ch = p[i];
|
|
2624
|
+
if (ch === e && i + 1 < p.length && (p[i + 1] === '%' || p[i + 1] === '_' || p[i + 1] === e)) {
|
|
2625
|
+
out += reEsc(p[i + 1]);
|
|
2626
|
+
i++;
|
|
2627
|
+
continue;
|
|
2628
|
+
}
|
|
2629
|
+
if (ch === '%') out += '.*';
|
|
2630
|
+
else if (ch === '_') out += '.';
|
|
2631
|
+
else out += reEsc(ch);
|
|
2632
|
+
}
|
|
2633
|
+
return new RegExp('^' + out + '$', ci ? 'i' : '').test(value);
|
|
2481
2634
|
}
|
|
2482
2635
|
|
|
2483
2636
|
function extractEqualPushdown(expr, schema) {
|
|
@@ -2529,10 +2682,18 @@ function evaluateExpr(expr, row, ctx) {
|
|
|
2529
2682
|
switch (expr.type) {
|
|
2530
2683
|
case 'and': return evaluateExpr(expr.left, row, ctx) && evaluateExpr(expr.right, row, ctx);
|
|
2531
2684
|
case 'or': return evaluateExpr(expr.left, row, ctx) || evaluateExpr(expr.right, row, ctx);
|
|
2685
|
+
case 'xor': return Boolean(evaluateExpr(expr.left, row, ctx)) !== Boolean(evaluateExpr(expr.right, row, ctx));
|
|
2532
2686
|
case 'not': return !evaluateExpr(expr.expr, row, ctx);
|
|
2533
2687
|
case 'compare': {
|
|
2534
2688
|
const l = resolveOperand(expr.left, row, ctx);
|
|
2535
2689
|
const r = resolveOperand(expr.right, row, ctx);
|
|
2690
|
+
// MySQL NULL 安全相等:NULL <=> NULL 为真;NULL <=> 值 为假
|
|
2691
|
+
if (expr.op === '<=>') {
|
|
2692
|
+
const ln = l === null || l === undefined;
|
|
2693
|
+
const rn = r === null || r === undefined;
|
|
2694
|
+
if (ln || rn) return ln && rn;
|
|
2695
|
+
return l === r || String(l) === String(r);
|
|
2696
|
+
}
|
|
2536
2697
|
// SQL 标准:任何与 NULL 的比较结果为 UNKNOWN(在 WHERE/ON/HAVING 中视为 false)
|
|
2537
2698
|
if (l === null || r === null) return false;
|
|
2538
2699
|
// 布尔与数值互通(MySQL: 1 = TRUE、0 = FALSE)
|
|
@@ -2555,7 +2716,7 @@ function evaluateExpr(expr, row, ctx) {
|
|
|
2555
2716
|
}
|
|
2556
2717
|
case 'like': {
|
|
2557
2718
|
const v = resolveOperand(expr.operand, row, ctx);
|
|
2558
|
-
const r = likeMatch(v, expr.pattern, !!expr.ci);
|
|
2719
|
+
const r = likeMatch(v, expr.pattern, !!expr.ci, expr.escape);
|
|
2559
2720
|
return expr.not ? !r : r;
|
|
2560
2721
|
}
|
|
2561
2722
|
case 'quantified':
|
|
@@ -2858,7 +3019,23 @@ class SQLExecutor {
|
|
|
2858
3019
|
if (pkCols.length > 0) {
|
|
2859
3020
|
const keyOf = (row) => pkCols.map(c => (row[c] !== undefined && row[c] !== null ? String(row[c]) : '')).join('|');
|
|
2860
3021
|
const hasExplicitPk = (row) => pkCols.some(c => row[c] !== undefined && row[c] !== null);
|
|
2861
|
-
|
|
3022
|
+
// 冲突判定的额外列组:
|
|
3023
|
+
// ON CONFLICT (col) DO UPDATE → 指定列(PG)
|
|
3024
|
+
// ON DUPLICATE KEY UPDATE → 各唯一列(MySQL:任一唯一键冲突都触发)
|
|
3025
|
+
// 此前只认主键,导致这两者在唯一列冲突时会插入重复行而不是更新。
|
|
3026
|
+
const ocTarget = (statement.onConflict && Array.isArray(statement.onConflict.target))
|
|
3027
|
+
? statement.onConflict.target.map(c => (c && c.name !== undefined ? c.name : c)).filter(Boolean)
|
|
3028
|
+
: null;
|
|
3029
|
+
const extraIdx = [];
|
|
3030
|
+
if (ocTarget && ocTarget.length) extraIdx.push(ocTarget);
|
|
3031
|
+
else if (statement.onDuplicate && schema) {
|
|
3032
|
+
for (const [c, def] of Object.entries(schema)) {
|
|
3033
|
+
if (def && def.unique && !def.primaryKey) extraIdx.push([c]);
|
|
3034
|
+
}
|
|
3035
|
+
}
|
|
3036
|
+
const hasAllOf = (cols) => (row) => cols.every(c => row[c] !== undefined && row[c] !== null);
|
|
3037
|
+
|
|
3038
|
+
const explicit = dataRows.filter((row) => hasExplicitPk(row) || extraIdx.some((cols) => hasAllOf(cols)(row)));
|
|
2862
3039
|
if (explicit.length > 0) {
|
|
2863
3040
|
const all = (await this.engine.find(statement.name, {}, { limit: 1e9, offset: 0 })).map(r => normalizeRow(r, schema));
|
|
2864
3041
|
const pkMap = new Map();
|
|
@@ -2866,15 +3043,31 @@ class SQLExecutor {
|
|
|
2866
3043
|
const keys = pkCols.map(c => row[c]).filter(v => v !== undefined && v !== null);
|
|
2867
3044
|
if (keys.length === pkCols.length) pkMap.set(keyOf(row), keys);
|
|
2868
3045
|
}
|
|
3046
|
+
const pkValsOf = (row) => pkCols.map(c => row[c]).filter(v => v !== undefined && v !== null);
|
|
3047
|
+
const extraMaps = extraIdx.map((cols) => {
|
|
3048
|
+
const m = new Map();
|
|
3049
|
+
const pick = hasAllOf(cols);
|
|
3050
|
+
const key = (row) => cols.map(c => String(row[c])).join('|');
|
|
3051
|
+
for (const row of all) {
|
|
3052
|
+
if (!pick(row)) continue;
|
|
3053
|
+
m.set(key(row), pkValsOf(row));
|
|
3054
|
+
}
|
|
3055
|
+
return { pick, key, m };
|
|
3056
|
+
});
|
|
2869
3057
|
const conflicts = [];
|
|
2870
3058
|
const fresh = [];
|
|
2871
3059
|
for (const d of dataRows) {
|
|
2872
|
-
|
|
2873
|
-
if (hasExplicitPk(d) && pkMap.has(
|
|
2874
|
-
|
|
2875
|
-
|
|
2876
|
-
|
|
3060
|
+
let existingId = null;
|
|
3061
|
+
if (hasExplicitPk(d) && pkMap.has(keyOf(d))) existingId = pkMap.get(keyOf(d));
|
|
3062
|
+
if (!existingId) {
|
|
3063
|
+
for (const { pick, key, m } of extraMaps) {
|
|
3064
|
+
if (!pick(d)) continue;
|
|
3065
|
+
const k = key(d);
|
|
3066
|
+
if (m.has(k)) { existingId = m.get(k); break; }
|
|
3067
|
+
}
|
|
2877
3068
|
}
|
|
3069
|
+
if (existingId) conflicts.push({ d, existingId });
|
|
3070
|
+
else fresh.push(d);
|
|
2878
3071
|
}
|
|
2879
3072
|
const conflictAction = this._conflictAction(statement);
|
|
2880
3073
|
const conflictSets = this._conflictSets(statement);
|
|
@@ -2902,10 +3095,14 @@ class SQLExecutor {
|
|
|
2902
3095
|
const data = {};
|
|
2903
3096
|
if (conflictSets) {
|
|
2904
3097
|
for (const [col, val] of conflictSets) {
|
|
2905
|
-
// VALUES(col)
|
|
2906
|
-
|
|
2907
|
-
|
|
2908
|
-
|
|
3098
|
+
// VALUES(col) 引用本次本应插入的值;EXCLUDED.col 为 PG 同义写法
|
|
3099
|
+
if (val && typeof val === 'object' && val._valuesOf !== undefined) {
|
|
3100
|
+
data[col] = d[val._valuesOf] !== undefined ? d[val._valuesOf] : null;
|
|
3101
|
+
} else if (val && typeof val === 'object' && val._excludedOf !== undefined) {
|
|
3102
|
+
data[col] = d[val._excludedOf] !== undefined ? d[val._excludedOf] : null;
|
|
3103
|
+
} else {
|
|
3104
|
+
data[col] = val;
|
|
3105
|
+
}
|
|
2909
3106
|
}
|
|
2910
3107
|
} else {
|
|
2911
3108
|
for (const k of Object.keys(d)) if (!pkCols.includes(k)) data[k] = d[k];
|
|
@@ -4678,6 +4875,44 @@ function applyParams(sql, values) {
|
|
|
4678
4875
|
return out;
|
|
4679
4876
|
}
|
|
4680
4877
|
|
|
4878
|
+
// 把执行器结果补齐为 README 承诺的结果信封
|
|
4879
|
+
// (columnTypes / rowCount / message / command / durationMs / warnings)
|
|
4880
|
+
function buildResultEnvelope(stmt, r, durationMs) {
|
|
4881
|
+
if (r === null || typeof r !== 'object' || Array.isArray(r)) return r;
|
|
4882
|
+
const out = { ...r };
|
|
4883
|
+
const t = stmt && stmt.type;
|
|
4884
|
+
if (out.command === undefined) out.command = String(t || '').toUpperCase();
|
|
4885
|
+
if (out.affectedRows === undefined) out.affectedRows = 0;
|
|
4886
|
+
if (out.rowCount === undefined) {
|
|
4887
|
+
out.rowCount = Array.isArray(out.rows) ? out.rows.length
|
|
4888
|
+
: (typeof out.affectedRows === 'number' ? out.affectedRows : 0);
|
|
4889
|
+
}
|
|
4890
|
+
if (out.warnings === undefined) out.warnings = [];
|
|
4891
|
+
if (out.durationMs === undefined) out.durationMs = durationMs;
|
|
4892
|
+
if (out.message === undefined) {
|
|
4893
|
+
out.message = t === 'select'
|
|
4894
|
+
? `${out.rowCount} row${out.rowCount === 1 ? '' : 's'} selected`
|
|
4895
|
+
: `${out.affectedRows} row${out.affectedRows === 1 ? '' : 's'} affected`;
|
|
4896
|
+
}
|
|
4897
|
+
return out;
|
|
4898
|
+
}
|
|
4899
|
+
|
|
4900
|
+
// SELECT 结果的列类型:单表查询按 schema 推断;表达式/多表查询该列为 null
|
|
4901
|
+
async function inferColumnTypes(engine, stmt, r) {
|
|
4902
|
+
if (!r || !Array.isArray(r.columns)) return undefined;
|
|
4903
|
+
let schema = null;
|
|
4904
|
+
try {
|
|
4905
|
+
const t = stmt && stmt.type === 'select' && stmt.from &&
|
|
4906
|
+
Array.isArray(stmt.from.tables) && stmt.from.tables.length === 1
|
|
4907
|
+
? stmt.from.tables[0].table : null;
|
|
4908
|
+
if (t && engine && typeof engine.getTableSchema === 'function') schema = await engine.getTableSchema(t);
|
|
4909
|
+
} catch (e) { schema = null; }
|
|
4910
|
+
return r.columns.map((name) => {
|
|
4911
|
+
const def = schema && schema[name];
|
|
4912
|
+
return def && def.type ? sqlTypeName(def.type).toUpperCase() : null;
|
|
4913
|
+
});
|
|
4914
|
+
}
|
|
4915
|
+
|
|
4681
4916
|
async function executeSQL(engine, sql, paramsOrOpts, opts = {}) {
|
|
4682
4917
|
// 参数守卫:engine 必须是引擎对象,sql 必须是字符串。误用时报清晰错误,
|
|
4683
4918
|
// 而不是在后续像 `reading 'length'` 那样抛出令人费解的 TypeError。
|
|
@@ -4727,7 +4962,14 @@ async function executeSQL(engine, sql, paramsOrOpts, opts = {}) {
|
|
|
4727
4962
|
const results = [];
|
|
4728
4963
|
for (const stmtSql of statements) {
|
|
4729
4964
|
const stmt = parseSQL(stmtSql);
|
|
4730
|
-
|
|
4965
|
+
const t0 = process.hrtime.bigint();
|
|
4966
|
+
let r = await executor.execute(stmt);
|
|
4967
|
+
const durationMs = Number(process.hrtime.bigint() - t0) / 1e6;
|
|
4968
|
+
r = buildResultEnvelope(stmt, r, Math.round(durationMs * 1000) / 1000);
|
|
4969
|
+
if (r && r.columns && r.columnTypes === undefined) {
|
|
4970
|
+
r.columnTypes = await inferColumnTypes(engine, stmt, r);
|
|
4971
|
+
}
|
|
4972
|
+
results.push(r);
|
|
4731
4973
|
}
|
|
4732
4974
|
return results.length === 1 ? results[0] : results;
|
|
4733
4975
|
}
|
|
Binary file
|
|
@@ -205,7 +205,7 @@ dependencies = [
|
|
|
205
205
|
|
|
206
206
|
[[package]]
|
|
207
207
|
name = "jsql-neo-core"
|
|
208
|
-
version = "
|
|
208
|
+
version = "6.0.0"
|
|
209
209
|
dependencies = [
|
|
210
210
|
"bincode",
|
|
211
211
|
"chrono",
|
|
@@ -220,7 +220,7 @@ dependencies = [
|
|
|
220
220
|
|
|
221
221
|
[[package]]
|
|
222
222
|
name = "jsql-neo-native"
|
|
223
|
-
version = "
|
|
223
|
+
version = "6.0.0"
|
|
224
224
|
dependencies = [
|
|
225
225
|
"jsql-neo-core",
|
|
226
226
|
"napi",
|
package/package.json
CHANGED
|
@@ -139,6 +139,51 @@ async function main() {
|
|
|
139
139
|
JSON.stringify(await rowsOf('SELECT name FROM (SELECT name, sal FROM emp WHERE sal > 150)')) === '[["b"],["c"],["d"]]');
|
|
140
140
|
}
|
|
141
141
|
|
|
142
|
+
console.log('\n--- 方言特性:DIV / XOR / && / || / <=> / :: / -> ->> / LIKE ESCAPE ---');
|
|
143
|
+
{
|
|
144
|
+
const data = [
|
|
145
|
+
{ id: 1, n: 10, flag: 1, s: '100%', meta: '{"name":"Alice","addr":{"city":"SH"},"tags":["x","y"]}' },
|
|
146
|
+
{ id: 2, n: 20, flag: 0, s: '1000', meta: '{"name":"Bob","addr":{"city":"BJ"},"tags":[]}' },
|
|
147
|
+
{ id: 3, n: 30, flag: 1, s: '100', meta: null },
|
|
148
|
+
];
|
|
149
|
+
const engine = {
|
|
150
|
+
hasTable: () => true, truncate: async () => {}, flush: async () => {},
|
|
151
|
+
find: async () => data.map(r => ({ ...r })),
|
|
152
|
+
getTableSchema: async () => ({
|
|
153
|
+
id: { type: 'integer', primaryKey: true }, n: { type: 'integer' }, flag: { type: 'integer' },
|
|
154
|
+
s: { type: 'string' }, meta: { type: 'json' },
|
|
155
|
+
}),
|
|
156
|
+
};
|
|
157
|
+
const rowsOf = async (sql) => {
|
|
158
|
+
const r = await executeSQL(engine, sql, OPTS);
|
|
159
|
+
return (Array.isArray(r) ? r[0] : r).rows;
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
ok('DIV 整数除法', JSON.stringify(await rowsOf('SELECT 7 DIV 2 AS r')) === '[[3]]');
|
|
163
|
+
ok('DIV 负数向零取整', JSON.stringify(await rowsOf('SELECT -7 DIV 2 AS r')) === '[[-3]]');
|
|
164
|
+
ok("'5'::INT 类型转换", JSON.stringify(await rowsOf("SELECT '5'::INT AS r")) === '[[5]]');
|
|
165
|
+
ok('XOR 逻辑异或(WHERE)',
|
|
166
|
+
JSON.stringify(await rowsOf('SELECT id FROM tbl WHERE flag = 1 XOR n = 20')) === '[[1],[2],[3]]');
|
|
167
|
+
ok('&& 是 AND 别名',
|
|
168
|
+
JSON.stringify(await rowsOf('SELECT id FROM tbl WHERE flag = 1 && n > 10')) === '[[3]]');
|
|
169
|
+
ok('|| 是 OR 别名',
|
|
170
|
+
JSON.stringify(await rowsOf('SELECT id FROM tbl WHERE flag = 0 || n > 20')) === '[[2],[3]]');
|
|
171
|
+
ok('<=> 非空相等', JSON.stringify(await rowsOf('SELECT id FROM tbl WHERE n <=> 20')) === '[[2]]');
|
|
172
|
+
ok('<=> NULL 安全(NULL<=>NULL 为真)',
|
|
173
|
+
JSON.stringify(await rowsOf('SELECT id FROM tbl WHERE meta <=> NULL')) === '[[3]]');
|
|
174
|
+
ok("->>'name'", JSON.stringify(await rowsOf("SELECT meta->>'name' AS x FROM tbl WHERE id = 1")) === '[["Alice"]]');
|
|
175
|
+
ok("->'addr'->>'city' 链式",
|
|
176
|
+
JSON.stringify(await rowsOf("SELECT meta->'addr'->>'city' AS x FROM tbl WHERE id = 1")) === '[["SH"]]');
|
|
177
|
+
ok("->'tags'->>0 数组下标",
|
|
178
|
+
JSON.stringify(await rowsOf("SELECT meta->'tags'->>0 AS x FROM tbl WHERE id = 1")) === '[["x"]]');
|
|
179
|
+
ok("LIKE ... ESCAPE '!' 匹配字面量 %",
|
|
180
|
+
JSON.stringify(await rowsOf("SELECT id FROM tbl WHERE s LIKE '100!%' ESCAPE '!'")) === '[[1]]');
|
|
181
|
+
ok('LIKE 反斜杠转义匹配字面量 %',
|
|
182
|
+
JSON.stringify(await rowsOf("SELECT id FROM tbl WHERE s LIKE '100\\%'")) === '[[1]]');
|
|
183
|
+
ok('LIKE 普通 % 仍为通配',
|
|
184
|
+
JSON.stringify(await rowsOf("SELECT id FROM tbl WHERE s LIKE '100%'")) === '[[1],[2],[3]]');
|
|
185
|
+
}
|
|
186
|
+
|
|
142
187
|
console.log('\n--- DDL / 解析器节点覆盖 ---');
|
|
143
188
|
{
|
|
144
189
|
ok('CREATE INDEX 可解析', parseSQL('CREATE INDEX i ON t (a)').type === 'createIndex');
|
|
@@ -156,6 +201,30 @@ async function main() {
|
|
|
156
201
|
const st = parseSQL('CREATE TABLE t (id INT PRIMARY KEY, c DATETIME DEFAULT CURRENT_TIMESTAMP)');
|
|
157
202
|
return st.schema.c.default === 'CURRENT_TIMESTAMP';
|
|
158
203
|
})());
|
|
204
|
+
ok('SERIAL 折叠为整型+自增', (() => {
|
|
205
|
+
const st = parseSQL('CREATE TABLE t (id SERIAL PRIMARY KEY, n TEXT)');
|
|
206
|
+
return st.schema.id.type === 'integer' && st.schema.id.autoIncrement === true;
|
|
207
|
+
})());
|
|
208
|
+
ok('BIGSERIAL 折叠为整型+自增', (() => {
|
|
209
|
+
const st = parseSQL('CREATE TABLE t (id BIGSERIAL PRIMARY KEY)');
|
|
210
|
+
return st.schema.id.type === 'integer' && st.schema.id.autoIncrement === true;
|
|
211
|
+
})());
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
console.log('\n--- executeSQL 结果信封(README 承诺的字段)---');
|
|
215
|
+
{
|
|
216
|
+
const engine = {
|
|
217
|
+
hasTable: () => true, truncate: async () => {}, flush: async () => {},
|
|
218
|
+
find: async () => [{ id: 1, name: 'a' }, { id: 2, name: 'b' }],
|
|
219
|
+
getTableSchema: async () => ({ id: { type: 'integer', primaryKey: true }, name: { type: 'string' } }),
|
|
220
|
+
};
|
|
221
|
+
const r = await executeSQL(engine, 'SELECT * FROM t', OPTS);
|
|
222
|
+
for (const f of ['columns', 'columnTypes', 'rows', 'rowCount', 'affectedRows', 'message', 'command', 'durationMs', 'warnings']) {
|
|
223
|
+
ok(`信封含 ${f}`, r[f] !== undefined, Object.keys(r));
|
|
224
|
+
}
|
|
225
|
+
ok('rowCount = 2', r.rowCount === 2);
|
|
226
|
+
ok('command = SELECT', r.command === 'SELECT');
|
|
227
|
+
ok('columnTypes 可推断(id → INT)', Array.isArray(r.columnTypes) && r.columnTypes[0] === 'INT');
|
|
159
228
|
}
|
|
160
229
|
|
|
161
230
|
console.log('\n--- 顶层工厂导出不得为 undefined ---');
|
|
@@ -214,29 +283,32 @@ async function main() {
|
|
|
214
283
|
|
|
215
284
|
console.log('\n--- 并发 insert 不得重复写(曾行数暴涨 / 丢失)---');
|
|
216
285
|
{
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
286
|
+
// 两个引擎都要验收:曾只测 wasm,导致 native(默认引擎)的同款竞态长期漏网。
|
|
287
|
+
for (const engineName of ['wasm_client', 'native_client']) {
|
|
288
|
+
let JSQL, db;
|
|
289
|
+
try {
|
|
290
|
+
({ JSQL } = require('../lib/' + engineName + '.js'));
|
|
291
|
+
db = new JSQL();
|
|
292
|
+
await db.start();
|
|
293
|
+
} catch (e) {
|
|
294
|
+
console.log(`[SKIP] ${engineName} 不可用:` + e.message.slice(0, 60));
|
|
295
|
+
continue;
|
|
296
|
+
}
|
|
297
|
+
const tag = `[${engineName}]`;
|
|
226
298
|
let id = 0;
|
|
227
299
|
await db.createTable('conc', { id: { type: 'INT', primaryKey: true }, b: { type: 'INT' } });
|
|
228
300
|
await Promise.all(Array.from({ length: 200 }, () => db.insert('conc', { id: ++id, b: 5 })));
|
|
229
301
|
const cnt = await db.count('conc');
|
|
230
|
-
ok(
|
|
302
|
+
ok(`${tag} 并发 insert 200 行 → count 恰为 200(实际 ${cnt})`, cnt === 200);
|
|
231
303
|
|
|
232
304
|
// flush 失败不得把坏行留在 buffer 里二次爆炸
|
|
233
|
-
ok(
|
|
305
|
+
ok(`${tag} insert(null) 抛 TypeError`, await (async () => {
|
|
234
306
|
try { await db.insert('conc', null); return false; } catch (e) { return e instanceof TypeError; }
|
|
235
307
|
})());
|
|
236
|
-
ok(
|
|
308
|
+
ok(`${tag} insert(undefined) 抛 TypeError`, await (async () => {
|
|
237
309
|
try { await db.insert('conc', undefined); return false; } catch (e) { return e instanceof TypeError; }
|
|
238
310
|
})());
|
|
239
|
-
ok(
|
|
311
|
+
ok(`${tag} insert(非对象) 抛 TypeError`, await (async () => {
|
|
240
312
|
try { await db.insert('conc', 42); return false; } catch (e) { return e instanceof TypeError; }
|
|
241
313
|
})());
|
|
242
314
|
|
|
@@ -246,7 +318,7 @@ async function main() {
|
|
|
246
318
|
cyc.self = cyc;
|
|
247
319
|
let cycErr = null;
|
|
248
320
|
try { await db.insert('cyc', { id: 1, meta: cyc }); } catch (e) { cycErr = e; }
|
|
249
|
-
ok(
|
|
321
|
+
ok(`${tag} 环形对象给出带表/列名的错误`,
|
|
250
322
|
!!cycErr && /Cannot serialize column 'meta'/.test(cycErr.message),
|
|
251
323
|
cycErr && cycErr.message);
|
|
252
324
|
await db.stop();
|