jsql-neo 6.1.0 → 6.2.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 +26 -19
- package/lib/migrate.js +126 -30
- package/lib/sql.js +320 -72
- package/package.json +1 -1
- package/postinstall.js +49 -14
- package/test/readme-audit.test.js +106 -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
|
|
@@ -1102,14 +1102,14 @@ ORDER BY avg_sal DESC;
|
|
|
1102
1102
|
|
|
1103
1103
|
**Bitwise** — `& | ^ ~ << >>`(`^` 是按位异或,`~` 是按位取反)。
|
|
1104
1104
|
|
|
1105
|
-
> **运算符方言说明。** `^` 与 `~` 在 MySQL
|
|
1106
|
-
>
|
|
1107
|
-
> `~`
|
|
1108
|
-
>
|
|
1105
|
+
> **运算符方言说明。** `^` 与 `~` 在 MySQL 与 PostgreSQL 中含义冲突,本实现按上下文区分:
|
|
1106
|
+
> - `^` 统一为**按位异或**(MySQL 语义);幂运算请用 `POWER(x, n)`。
|
|
1107
|
+
> - `~` 在**单目**位置是按位取反(MySQL),在**双目**位置是正则匹配(PG),
|
|
1108
|
+
> 另有 `~*`(不敏感)、`!~`、`!~*`。
|
|
1109
|
+
> - 需要正则时也可用 `REGEXP` / `RLIKE`;PG 的 `^`(幂)**暂不支持**。
|
|
1109
1110
|
|
|
1110
|
-
**JSON (PG style)** —
|
|
1111
|
-
|
|
1112
|
-
`?` `?|` `?&`(键存在)**暂不支持**。
|
|
1111
|
+
**JSON (PG style)** — `->` / `->>`(取字段)、`#>` / `#>>`(路径取值)、`@>` / `<@`(包含)、
|
|
1112
|
+
`?` / `?|` / `?&`(键存在),可链式(`meta->'addr'->>'city'`)。
|
|
1113
1113
|
|
|
1114
1114
|
### LIKE / ILIKE / regex
|
|
1115
1115
|
|
|
@@ -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 WHERE meta @> '{"tags":["admin"]}';
|
|
1166
1166
|
```
|
|
1167
1167
|
|
|
1168
1168
|
JSON columns are fully interoperable with Mongo document views.
|
|
@@ -3138,7 +3138,7 @@ DELETE FROM orders WHERE status = 'cancelled' LIMIT 100;
|
|
|
3138
3138
|
|
|
3139
3139
|
#### 查询语法 SELECT
|
|
3140
3140
|
|
|
3141
|
-
```
|
|
3141
|
+
```text
|
|
3142
3142
|
SELECT [DISTINCT] select_list
|
|
3143
3143
|
FROM table_reference
|
|
3144
3144
|
[JOIN table_reference ON condition]
|
|
@@ -3445,11 +3445,16 @@ INTERVAL 单位:`DAY` `HOUR` `MINUTE` `SECOND` `WEEK` `MONTH` `YEAR` 及其组
|
|
|
3445
3445
|
|---|---|---|
|
|
3446
3446
|
| `->` | 取 JSON 字段(返回 JSON) | `meta->'name'` |
|
|
3447
3447
|
| `->>` | 取 JSON 字段(返回文本) | `meta->>'name'` |
|
|
3448
|
+
| `#>` | 路径取值(返回 JSON) | `meta#>'{addr,city}'` |
|
|
3449
|
+
| `#>>` | 路径取值(返回文本) | `meta#>>'{addr,city}'` |
|
|
3450
|
+
| `@>` | 包含 | `meta @> '{"plan":"pro"}'` |
|
|
3451
|
+
| `<@` | 被包含 | `'{"a":1}' <@ meta` |
|
|
3452
|
+
| `?` | 含指定键 | `meta ? 'plan'` |
|
|
3453
|
+
| `?\|` | 含任一键 | `meta ?\| '{a,b}'` |
|
|
3454
|
+
| `?&` | 含全部键 | `meta ?& '{a,b}'` |
|
|
3448
3455
|
|
|
3449
3456
|
可链式取值:`meta->'addr'->>'city'`。
|
|
3450
3457
|
|
|
3451
|
-
> `#>` `#>>`(路径访问)、`@>` `<@`(包含)、`?` `?|` `?&`(键存在)**暂不支持** —— 使用时会抛出明确错误,不会静默退化成别的运算。
|
|
3452
|
-
|
|
3453
3458
|
### LIKE / ILIKE / 正则
|
|
3454
3459
|
|
|
3455
3460
|
**LIKE 通配符**
|
|
@@ -3466,8 +3471,8 @@ INTERVAL 单位:`DAY` `HOUR` `MINUTE` `SECOND` `WEEK` `MONTH` `YEAR` 及其组
|
|
|
3466
3471
|
|
|
3467
3472
|
- 基于 JS RegExp 引擎
|
|
3468
3473
|
- 支持 flags:`i`(忽略大小写)、`m`(多行)、`s`(点匹配换行)
|
|
3469
|
-
- PG 风格的
|
|
3470
|
-
|
|
3474
|
+
- PG 风格的 `~`(大小写敏感)/ `~*`(不敏感)/ `!~` / `!~*` 双目运算符也支持;
|
|
3475
|
+
单目位置的 `~` 仍是按位取反。
|
|
3471
3476
|
|
|
3472
3477
|
```sql
|
|
3473
3478
|
SELECT * FROM users
|
|
@@ -3570,7 +3575,7 @@ INSERT INTO users (id, meta) VALUES
|
|
|
3570
3575
|
-- 查询路径
|
|
3571
3576
|
SELECT meta->>'name' AS name,
|
|
3572
3577
|
meta->'addr'->>'city' AS city
|
|
3573
|
-
FROM users;
|
|
3578
|
+
FROM users WHERE meta @> '{"tags":["admin"]}';
|
|
3574
3579
|
```
|
|
3575
3580
|
|
|
3576
3581
|
JSON 函数见 [系统与杂项函数](#系统与杂项函数)。Mongo 客户端视角下,JSON 列与 BSON 文档完全互通。
|
|
@@ -5798,6 +5803,8 @@ expr := literal
|
|
|
5798
5803
|
| expr [NOT] LIKE pattern [ESCAPE char]
|
|
5799
5804
|
| expr [NOT] ILIKE pattern
|
|
5800
5805
|
| expr [NOT] RLIKE pattern | expr [NOT] REGEXP pattern
|
|
5806
|
+
| expr ~ pattern | expr ~* pattern | expr !~ pattern | expr !~* pattern
|
|
5807
|
+
| expr ? key | expr ?| keys | expr ?& keys
|
|
5801
5808
|
| expr IS [NOT] NULL | expr IS [NOT] TRUE | expr IS [NOT] FALSE
|
|
5802
5809
|
| expr [NOT] IN ( ... )
|
|
5803
5810
|
| expr [NOT] ANY '(' select_statement ')'
|
|
@@ -5843,12 +5850,12 @@ function_call := func_name '(' [DISTINCT] args ')' -- 普通/聚合
|
|
|
5843
5850
|
### 运算符优先级(从高到低)
|
|
5844
5851
|
|
|
5845
5852
|
```
|
|
5846
|
-
1. () . [] -> ->> ::
|
|
5853
|
+
1. () . [] -> ->> #> #>> ::
|
|
5847
5854
|
2. CAST 一元 + - ~
|
|
5848
5855
|
3. ^ * / % DIV MOD
|
|
5849
5856
|
4. + -
|
|
5850
5857
|
5. << >> & | ^(位)
|
|
5851
|
-
6. = <> != < <= > >= <=> BETWEEN IN LIKE ILIKE RLIKE REGEXP
|
|
5858
|
+
6. = <> != < <= > >= <=> @> <@ ? ?| ?& BETWEEN IN LIKE ILIKE RLIKE REGEXP ~ ~* !~ !~*
|
|
5852
5859
|
7. NOT
|
|
5853
5860
|
8. AND &&
|
|
5854
5861
|
9. XOR
|
|
@@ -7445,12 +7452,12 @@ jsql import ./data app.sql
|
|
|
7445
7452
|
| 冲突处理 | `ON DUPLICATE KEY UPDATE` | `ON CONFLICT ... DO ...` | 两者都支持 |
|
|
7446
7453
|
| 占位符 | `?` | `$1, $2` | 两者(分协议) |
|
|
7447
7454
|
| 模糊匹配 | `LIKE`(大小写敏感) | `LIKE` + `ILIKE` | 两者 |
|
|
7448
|
-
| 正则 | `REGEXP` |
|
|
7455
|
+
| 正则 | `REGEXP` | `~` `~*` | 两者 + `RLIKE` |
|
|
7449
7456
|
| 标识符引用 | 反引号 `` ` `` | 双引号 `"` | 两者 |
|
|
7450
7457
|
| 字符串字面量 | 单引号 | 单引号 | 单引号(双引号按标识符,PG 语义) |
|
|
7451
7458
|
| 布尔 | `TRUE/FALSE`(1/0) | `t/f`(三值逻辑) | 兼容两者 |
|
|
7452
7459
|
| 分页 | `LIMIT off, n` | `LIMIT n OFFSET off` | 两者 |
|
|
7453
|
-
| JSON 访问 | `JSON_EXTRACT` | `->` `->>` | 两者 |
|
|
7460
|
+
| JSON 访问 | `JSON_EXTRACT` | `->` `->>` `#>>` | 两者 |
|
|
7454
7461
|
| 类型转换 | `CAST`/`CONVERT` | `::` | 三者 |
|
|
7455
7462
|
| 条件分支 | `IF()` | `CASE` | `IF` + `CASE` + `IIF` |
|
|
7456
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
|
-
|
|
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
|
}
|
package/lib/sql.js
CHANGED
|
@@ -11,6 +11,10 @@ const DANGEROUS_SQL = [
|
|
|
11
11
|
{ re: /^SYSEXEC$|^SYS_EXEC$/, name: 'sys_exec' },
|
|
12
12
|
];
|
|
13
13
|
|
|
14
|
+
// SQL 文本体积上限(默认 64 MiB):防止超大、无分号的输入造成无界内存消耗
|
|
15
|
+
// (CWE-770)。可在 executeSQL 通过 opts.maxSqlLength 放宽/关闭(传 Infinity)。
|
|
16
|
+
const DEFAULT_MAX_SQL_LENGTH = 64 * 1024 * 1024;
|
|
17
|
+
|
|
14
18
|
function findDangerousSQL(tokens) {
|
|
15
19
|
for (let i = 0; i < tokens.length; i++) {
|
|
16
20
|
const t = tokens[i];
|
|
@@ -79,10 +83,12 @@ function tokenize(sql) {
|
|
|
79
83
|
while (i < n && sql[i] !== '\n') i++;
|
|
80
84
|
continue;
|
|
81
85
|
}
|
|
82
|
-
// PG 的 JSON 路径操作符 #> /
|
|
83
|
-
// 若不在这里拦截,'#' 会被当成行注释吃掉,导致 SQL 后半段被静默丢弃。
|
|
86
|
+
// PG 的 JSON 路径操作符 #> / #>>(必须在 '#' 行注释之前识别)
|
|
84
87
|
if (c === '#' && sql[i + 1] === '>') {
|
|
85
|
-
|
|
88
|
+
const op = sql[i + 2] === '>' ? '#>>' : '#>';
|
|
89
|
+
tokens.push(new SQLToken('op', op, i));
|
|
90
|
+
i += op.length;
|
|
91
|
+
continue;
|
|
86
92
|
}
|
|
87
93
|
if (c === '#' || (c === '/' && sql[i + 1] === '*')) {
|
|
88
94
|
if (c === '#') { while (i < n && sql[i] !== '\n') i++; continue; }
|
|
@@ -106,12 +112,15 @@ function tokenize(sql) {
|
|
|
106
112
|
else str += esc;
|
|
107
113
|
j += 2;
|
|
108
114
|
} else if (sql[j] === quote) {
|
|
115
|
+
// SQL 标准转义:字符串内两个连续引号表示一个字面量引号('O''Brien' → O'Brien)
|
|
116
|
+
if (sql[j + 1] === quote) { str += quote; j += 2; continue; }
|
|
109
117
|
break;
|
|
110
118
|
} else {
|
|
111
119
|
str += sql[j];
|
|
112
120
|
j++;
|
|
113
121
|
}
|
|
114
122
|
}
|
|
123
|
+
if (j >= n) throw new Error(`Unterminated ${quote === '"' ? 'double' : 'single'}-quoted string at position ${i}`);
|
|
115
124
|
tokens.push(new SQLToken('string', str, i));
|
|
116
125
|
i = j + 1;
|
|
117
126
|
continue;
|
|
@@ -153,15 +162,17 @@ function tokenize(sql) {
|
|
|
153
162
|
if (c === '`') {
|
|
154
163
|
let j = i + 1;
|
|
155
164
|
while (j < n && sql[j] !== '`') j++;
|
|
165
|
+
if (j >= n) throw new Error(`Unterminated backtick-quoted identifier at position ${i}`);
|
|
156
166
|
tokens.push(new SQLToken('ident', sql.slice(i + 1, j), i));
|
|
157
167
|
i = j + 1;
|
|
158
168
|
continue;
|
|
159
169
|
}
|
|
160
170
|
|
|
161
|
-
// PG 的 JSON 包含操作符 @>
|
|
162
|
-
// 若不拦截,'@' 会被 sysvar 分支静默吞掉、'>' 退化成普通大于号,产生错误结果。
|
|
171
|
+
// PG 的 JSON 包含操作符 @>
|
|
163
172
|
if (c === '@' && sql[i + 1] === '>') {
|
|
164
|
-
|
|
173
|
+
tokens.push(new SQLToken('op', '@>', i));
|
|
174
|
+
i += 2;
|
|
175
|
+
continue;
|
|
165
176
|
}
|
|
166
177
|
|
|
167
178
|
if (c === '@') {
|
|
@@ -181,6 +192,12 @@ function tokenize(sql) {
|
|
|
181
192
|
}
|
|
182
193
|
|
|
183
194
|
// 参数占位符:? (顺序)、?N (编号)、?? (标识符占位)
|
|
195
|
+
// 注意:?| / ?& 是 PG 的 JSON 键存在操作符,必须优先识别
|
|
196
|
+
if (c === '?' && (sql[i + 1] === '|' || sql[i + 1] === '&')) {
|
|
197
|
+
tokens.push(new SQLToken('op', '?' + sql[i + 1], i));
|
|
198
|
+
i += 2;
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
184
201
|
if (c === '?') {
|
|
185
202
|
if (sql[i + 1] === '?') { tokens.push(new SQLToken('param', '??', i)); i += 2; continue; }
|
|
186
203
|
let j = i + 1;
|
|
@@ -193,9 +210,11 @@ function tokenize(sql) {
|
|
|
193
210
|
|
|
194
211
|
const three = sql.slice(i, i + 3);
|
|
195
212
|
if (sql.slice(i, i + 2) === '<@') {
|
|
196
|
-
|
|
213
|
+
tokens.push(new SQLToken('op', '<@', i));
|
|
214
|
+
i += 2;
|
|
215
|
+
continue;
|
|
197
216
|
}
|
|
198
|
-
if (three === '->>' || three === '<=>') {
|
|
217
|
+
if (three === '->>' || three === '<=>' || three === '!~*') {
|
|
199
218
|
tokens.push(new SQLToken('op', three, i));
|
|
200
219
|
i += 3;
|
|
201
220
|
continue;
|
|
@@ -204,7 +223,7 @@ function tokenize(sql) {
|
|
|
204
223
|
const two = sql.slice(i, i + 2);
|
|
205
224
|
if (two === '<=' || two === '>=' || two === '!=' || two === '<>' || two === '==' ||
|
|
206
225
|
two === '<<' || two === '>>' || two === '->' || two === '::' ||
|
|
207
|
-
two === '&&' || two === '||') {
|
|
226
|
+
two === '&&' || two === '||' || two === '~*' || two === '!~') {
|
|
208
227
|
tokens.push(new SQLToken('op', two, i));
|
|
209
228
|
i += 2;
|
|
210
229
|
continue;
|
|
@@ -1838,6 +1857,13 @@ class Parser {
|
|
|
1838
1857
|
this.expect('op', ')');
|
|
1839
1858
|
return { type: 'cast', expr, dataType };
|
|
1840
1859
|
}
|
|
1860
|
+
// 零参时间函数:CURRENT_DATE / CURRENT_TIME / CURRENT_TIMESTAMP(可省略括号)。
|
|
1861
|
+
// 此前不在 KEYWORDS 里,被当成列名解析 → "no such column: CURRENT_DATE"
|
|
1862
|
+
if (['CURRENT_DATE', 'CURRENT_TIME', 'CURRENT_TIMESTAMP'].includes(String(t.value).toUpperCase())) {
|
|
1863
|
+
const name = String(t.value).toUpperCase();
|
|
1864
|
+
if (this.peek().type === 'op' && this.peek().value === '(') { this.next(); this.expect('op', ')'); }
|
|
1865
|
+
return { type: 'func', name, args: [] };
|
|
1866
|
+
}
|
|
1841
1867
|
// EXTRACT(YEAR FROM expr) —— 标准 SQL 的 FROM 形式
|
|
1842
1868
|
if (String(t.value).toUpperCase() === 'EXTRACT' && this.peek().type === 'op' && this.peek().value === '(') {
|
|
1843
1869
|
this.next();
|
|
@@ -2101,6 +2127,13 @@ class Parser {
|
|
|
2101
2127
|
node = { type: 'jsonAccess', operand: node, key, asText: t.value === '->>' };
|
|
2102
2128
|
continue;
|
|
2103
2129
|
}
|
|
2130
|
+
// PG 路径取值:meta#>'{a,b}' / meta#>>'{a,b}'
|
|
2131
|
+
if (t.type === 'op' && (t.value === '#>' || t.value === '#>>')) {
|
|
2132
|
+
this.next();
|
|
2133
|
+
const key = this.parseValue();
|
|
2134
|
+
node = { type: 'jsonAccess', operand: node, key, asText: t.value === '#>>', isPath: true };
|
|
2135
|
+
continue;
|
|
2136
|
+
}
|
|
2104
2137
|
break;
|
|
2105
2138
|
}
|
|
2106
2139
|
return node;
|
|
@@ -2202,6 +2235,27 @@ class Parser {
|
|
|
2202
2235
|
return { type: 'like', operand: left, pattern, not, ci: isILike, escape };
|
|
2203
2236
|
}
|
|
2204
2237
|
|
|
2238
|
+
// PG 正则操作符(双目位置):~(大小写敏感)/ ~*(不敏感)/ !~ / !~*
|
|
2239
|
+
if (t2.type === 'op' && (t2.value === '~' || t2.value === '~*' || t2.value === '!~' || t2.value === '!~*')) {
|
|
2240
|
+
this.next();
|
|
2241
|
+
const pattern = this.parseValue();
|
|
2242
|
+
return {
|
|
2243
|
+
type: 'regexp', operand: left, pattern,
|
|
2244
|
+
not: t2.value.startsWith('!') || not,
|
|
2245
|
+
ci: t2.value.endsWith('*'),
|
|
2246
|
+
};
|
|
2247
|
+
}
|
|
2248
|
+
|
|
2249
|
+
// PG JSON 键存在:? / ?| / ?&
|
|
2250
|
+
// 裸 `?` 在此处处于"运算符位置",与值位置上的 `?` 参数占位符不冲突。
|
|
2251
|
+
if ((t2.type === 'op' && (t2.value === '?|' || t2.value === '?&')) ||
|
|
2252
|
+
(t2.type === 'param' && t2.value === null)) {
|
|
2253
|
+
const mode = t2.type === 'param' ? '?' : t2.value;
|
|
2254
|
+
this.next();
|
|
2255
|
+
const keys = this.parseValue();
|
|
2256
|
+
return { type: 'jsonKeyExists', operand: left, keys, mode, not };
|
|
2257
|
+
}
|
|
2258
|
+
|
|
2205
2259
|
// MySQL 中 RLIKE 是 REGEXP 的同义词
|
|
2206
2260
|
if (t2.type === 'keyword' && (t2.value === 'REGEXP' || t2.value === 'RLIKE')) {
|
|
2207
2261
|
this.next();
|
|
@@ -2209,6 +2263,13 @@ class Parser {
|
|
|
2209
2263
|
return { type: 'regexp', operand: left, pattern, not };
|
|
2210
2264
|
}
|
|
2211
2265
|
|
|
2266
|
+
// PG JSON 包含操作符
|
|
2267
|
+
if (t2.type === 'op' && (t2.value === '@>' || t2.value === '<@')) {
|
|
2268
|
+
this.next();
|
|
2269
|
+
const right = this.parseBitwise();
|
|
2270
|
+
return { type: 'jsonContains', op: t2.value, left, right, not };
|
|
2271
|
+
}
|
|
2272
|
+
|
|
2212
2273
|
if (t2.type === 'op' && ['=', '!=', '<>', '<', '<=', '>', '>=', '<=>'].includes(t2.value)) {
|
|
2213
2274
|
this.next();
|
|
2214
2275
|
// = ANY (SELECT ...) / > ALL (SELECT ...)
|
|
@@ -2377,15 +2438,18 @@ function resolveOperand(operand, row, ctx) {
|
|
|
2377
2438
|
if (typeof base === 'string') {
|
|
2378
2439
|
try { base = JSON.parse(base); } catch (e) { return null; }
|
|
2379
2440
|
}
|
|
2380
|
-
const
|
|
2381
|
-
let v;
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2441
|
+
const keys = operand.isPath ? parsePgPath(operand.key) : [operand.key];
|
|
2442
|
+
let v = base;
|
|
2443
|
+
for (const key of keys) {
|
|
2444
|
+
if (v === null || v === undefined) return null;
|
|
2445
|
+
if (Array.isArray(v)) {
|
|
2446
|
+
const idx = Number(key);
|
|
2447
|
+
v = Number.isInteger(idx) ? (idx < 0 ? v[v.length + idx] : v[idx]) : undefined;
|
|
2448
|
+
} else if (typeof v === 'object') {
|
|
2449
|
+
v = v[key];
|
|
2450
|
+
} else {
|
|
2451
|
+
return null;
|
|
2452
|
+
}
|
|
2389
2453
|
}
|
|
2390
2454
|
if (v === undefined) return null;
|
|
2391
2455
|
if (operand.asText) {
|
|
@@ -2394,6 +2458,17 @@ function resolveOperand(operand, row, ctx) {
|
|
|
2394
2458
|
}
|
|
2395
2459
|
return v;
|
|
2396
2460
|
}
|
|
2461
|
+
case 'jsonContains': {
|
|
2462
|
+
const l = resolveOperand(operand.left, row, ctx);
|
|
2463
|
+
const r = resolveOperand(operand.right, row, ctx);
|
|
2464
|
+
const res = jsonContains(l, r, operand.op === '<@');
|
|
2465
|
+
return operand.not ? !res : res;
|
|
2466
|
+
}
|
|
2467
|
+
case 'jsonKeyExists': {
|
|
2468
|
+
const base = resolveOperand(operand.operand, row, ctx);
|
|
2469
|
+
const res = jsonKeyExists(base, operand.keys, operand.mode);
|
|
2470
|
+
return operand.not ? !res : res;
|
|
2471
|
+
}
|
|
2397
2472
|
case 'quantified': {
|
|
2398
2473
|
// = ANY / > ALL (SELECT ...) —— 需先由执行层物化成 operand._list
|
|
2399
2474
|
const v = resolveOperand(operand.operand, row, ctx);
|
|
@@ -2425,6 +2500,12 @@ function parseSqlDate(v) {
|
|
|
2425
2500
|
|
|
2426
2501
|
function pad2(n) { return String(n).padStart(2, '0'); }
|
|
2427
2502
|
|
|
2503
|
+
/** 本地时区的 'YYYY-MM-DD HH:mm:ss'(MySQL 的 NOW() 语义;区别于 UTC_TIMESTAMP) */
|
|
2504
|
+
function sqlLocalDateTime(d = new Date()) {
|
|
2505
|
+
return d.getFullYear() + '-' + pad2(d.getMonth() + 1) + '-' + pad2(d.getDate()) + ' ' +
|
|
2506
|
+
pad2(d.getHours()) + ':' + pad2(d.getMinutes()) + ':' + pad2(d.getSeconds());
|
|
2507
|
+
}
|
|
2508
|
+
|
|
2428
2509
|
/** 按输入形态回写日期:纯日期返回 YYYY-MM-DD,带时间返回 YYYY-MM-DD HH:MM:SS */
|
|
2429
2510
|
function fmtSqlDate(dateOnly, d) {
|
|
2430
2511
|
const ymd = d.getFullYear() + '-' + pad2(d.getMonth() + 1) + '-' + pad2(d.getDate());
|
|
@@ -2456,9 +2537,11 @@ function applyScalarFunction(fnNode, row, ctx) {
|
|
|
2456
2537
|
case 'FOUND_ROWS': return session && session.foundRows !== undefined ? session.foundRows : 0;
|
|
2457
2538
|
case 'CONNECTION_ID': return session && session.connectionId !== undefined ? session.connectionId : 0;
|
|
2458
2539
|
case 'DATABASE': case 'SCHEMA': return session && session.currentDb ? session.currentDb : 'default';
|
|
2459
|
-
|
|
2460
|
-
|
|
2461
|
-
case '
|
|
2540
|
+
// NOW() / CURDATE() / CURTIME() 返回会话时区(本地)时间,不能用 toISOString()(UTC)——
|
|
2541
|
+
// 否则东八区凌晨会整整差一天(2024-02-29 被存成 2024-02-28)。
|
|
2542
|
+
case 'NOW': case 'CURRENT_TIMESTAMP': return sqlLocalDateTime();
|
|
2543
|
+
case 'CURDATE': case 'CURRENT_DATE': return sqlLocalDateTime().slice(0, 10);
|
|
2544
|
+
case 'CURTIME': return sqlLocalDateTime().slice(11, 19);
|
|
2462
2545
|
case 'UTC_TIMESTAMP': return new Date().toISOString().slice(0, 19).replace('T', ' ') + ' UTC';
|
|
2463
2546
|
case 'CONCAT': return args.map(a => a === null || a === undefined ? '' : String(a)).join('');
|
|
2464
2547
|
case 'CONCAT_WS': {
|
|
@@ -2633,6 +2716,57 @@ function likeMatch(value, pattern, ci = true, escape = '\\') {
|
|
|
2633
2716
|
return new RegExp('^' + out + '$', ci ? 'i' : '').test(value);
|
|
2634
2717
|
}
|
|
2635
2718
|
|
|
2719
|
+
/** 解析 PG 的路径字面量:'{a,b}' 或数组 → ['a','b'] */
|
|
2720
|
+
function parsePgPath(key) {
|
|
2721
|
+
if (Array.isArray(key)) return key.map(String);
|
|
2722
|
+
if (typeof key === 'string') {
|
|
2723
|
+
const s = key.trim();
|
|
2724
|
+
if (s.startsWith('{') && s.endsWith('}')) {
|
|
2725
|
+
return s.slice(1, -1).split(',').map(x => x.trim().replace(/^"(.*)"$/, '$1')).filter(x => x !== '');
|
|
2726
|
+
}
|
|
2727
|
+
return [s];
|
|
2728
|
+
}
|
|
2729
|
+
return [String(key)];
|
|
2730
|
+
}
|
|
2731
|
+
|
|
2732
|
+
/** 把可能是 JSON 字符串的值归一为 JS 值 */
|
|
2733
|
+
function toJsonValue(v) {
|
|
2734
|
+
if (typeof v === 'string') { try { return JSON.parse(v); } catch (e) { return v; } }
|
|
2735
|
+
return v;
|
|
2736
|
+
}
|
|
2737
|
+
|
|
2738
|
+
/** JSON 包含语义(PG 的 @> / <@)。swap=true 时判断 target 是否包含 container。 */
|
|
2739
|
+
function jsonContains(a, b, swap) {
|
|
2740
|
+
const container = toJsonValue(swap ? b : a);
|
|
2741
|
+
const target = toJsonValue(swap ? a : b);
|
|
2742
|
+
return jsonContainsValue(container, target);
|
|
2743
|
+
}
|
|
2744
|
+
|
|
2745
|
+
function jsonContainsValue(container, target) {
|
|
2746
|
+
if (target === null || target === undefined) return true;
|
|
2747
|
+
if (Array.isArray(target)) {
|
|
2748
|
+
if (!Array.isArray(container)) return false;
|
|
2749
|
+
return target.every(t => container.some(c => jsonContainsValue(c, t)));
|
|
2750
|
+
}
|
|
2751
|
+
if (typeof target === 'object') {
|
|
2752
|
+
if (container === null || typeof container !== 'object' || Array.isArray(container)) return false;
|
|
2753
|
+
return Object.keys(target).every(k => Object.prototype.hasOwnProperty.call(container, k) && jsonContainsValue(container[k], target[k]));
|
|
2754
|
+
}
|
|
2755
|
+
if (Array.isArray(container)) return container.some(c => c === target || String(c) === String(target));
|
|
2756
|
+
if (container !== null && typeof container === 'object') return false;
|
|
2757
|
+
return container === target || String(container) === String(target);
|
|
2758
|
+
}
|
|
2759
|
+
|
|
2760
|
+
/** JSON 键存在(PG 的 ? / ?| / ?&)。keys 为单个键名或 '{a,b}' 形式的键列表。 */
|
|
2761
|
+
function jsonKeyExists(base, keys, mode) {
|
|
2762
|
+
const v = toJsonValue(base);
|
|
2763
|
+
if (v === null || typeof v !== 'object' || Array.isArray(v)) return false;
|
|
2764
|
+
const has = (k) => Object.prototype.hasOwnProperty.call(v, k);
|
|
2765
|
+
if (mode === '?|') return parsePgPath(keys).some(has);
|
|
2766
|
+
if (mode === '?&') return parsePgPath(keys).every(has);
|
|
2767
|
+
return has(String(keys));
|
|
2768
|
+
}
|
|
2769
|
+
|
|
2636
2770
|
function extractEqualPushdown(expr, schema) {
|
|
2637
2771
|
if (!expr || !schema) return null;
|
|
2638
2772
|
const filter = {};
|
|
@@ -2712,7 +2846,8 @@ function evaluateExpr(expr, row, ctx) {
|
|
|
2712
2846
|
const v = resolveOperand(expr.operand, row, ctx);
|
|
2713
2847
|
if (expr.subquery && expr.subquery._values !== undefined) expr.list = expr.subquery._values;
|
|
2714
2848
|
if (!expr.list) return false;
|
|
2715
|
-
|
|
2849
|
+
const r = expr.list.some(x => x === v || String(x) === String(v));
|
|
2850
|
+
return expr.not ? !r : r; // NOT IN:此前漏了取反,导致 NOT IN 与 IN 结果相同
|
|
2716
2851
|
}
|
|
2717
2852
|
case 'like': {
|
|
2718
2853
|
const v = resolveOperand(expr.operand, row, ctx);
|
|
@@ -2721,13 +2856,18 @@ function evaluateExpr(expr, row, ctx) {
|
|
|
2721
2856
|
}
|
|
2722
2857
|
case 'quantified':
|
|
2723
2858
|
return !!resolveOperand(expr, row, ctx);
|
|
2859
|
+
case 'jsonContains':
|
|
2860
|
+
return !!resolveOperand(expr, row, ctx);
|
|
2724
2861
|
case 'regexp': {
|
|
2725
2862
|
const v = resolveOperand(expr.operand, row, ctx);
|
|
2726
2863
|
if (v === null || v === undefined) return false;
|
|
2727
|
-
|
|
2864
|
+
// REGEXP / RLIKE 历史上就是大小写不敏感的;`~` 敏感、`~*` 不敏感
|
|
2865
|
+
const re = new RegExp(String(expr.pattern), expr.ci === false ? '' : 'i');
|
|
2728
2866
|
const r = re.test(String(v));
|
|
2729
2867
|
return expr.not ? !r : r;
|
|
2730
2868
|
}
|
|
2869
|
+
case 'jsonKeyExists':
|
|
2870
|
+
return !!resolveOperand(expr, row, ctx);
|
|
2731
2871
|
case 'isTruth': {
|
|
2732
2872
|
const v = resolveOperand(expr.operand, row, ctx);
|
|
2733
2873
|
const isTrue = v === true || v === 1 || v === '1' || v === 'true' || v === 'TRUE' || v === 't' || (typeof v === 'number' && v !== 0);
|
|
@@ -3013,9 +3153,30 @@ class SQLExecutor {
|
|
|
3013
3153
|
dataRows = dataRows.map(stripDefault);
|
|
3014
3154
|
}
|
|
3015
3155
|
const pkCols = schema ? Object.keys(schema).filter(k => schema[k].primaryKey) : [];
|
|
3156
|
+
const uniqueCols = schema ? Object.keys(schema).filter(k => schema[k].unique && !schema[k].primaryKey) : [];
|
|
3016
3157
|
let toInsert = dataRows;
|
|
3017
3158
|
let updated = 0;
|
|
3018
3159
|
let skipped = 0;
|
|
3160
|
+
// 非主键 UNIQUE 约束:默认动作(无 ON CONFLICT / IGNORE / REPLACE)下违反即 ER_DUP_ENTRY。
|
|
3161
|
+
// 主键冲突由下面的冲突逻辑处理;这里补唯一列(含批内重复)。
|
|
3162
|
+
if (uniqueCols.length > 0 && this._conflictAction(statement) === 'throw') {
|
|
3163
|
+
const existing = (await this.engine.find(statement.name, {}, { limit: 1e9, offset: 0 }))
|
|
3164
|
+
.map(r => normalizeRow(r, schema));
|
|
3165
|
+
for (const col of uniqueCols) {
|
|
3166
|
+
const used = new Set();
|
|
3167
|
+
for (const r of existing) {
|
|
3168
|
+
const v = r[col];
|
|
3169
|
+
if (v !== undefined && v !== null) used.add(String(v));
|
|
3170
|
+
}
|
|
3171
|
+
for (const d of dataRows) {
|
|
3172
|
+
const v = d[col];
|
|
3173
|
+
if (v === undefined || v === null) continue; // UNIQUE 允许多个 NULL
|
|
3174
|
+
const s = String(v);
|
|
3175
|
+
if (used.has(s)) throw new Error(`ER_DUP_ENTRY: Duplicate entry '${s}' for unique column '${col}'`);
|
|
3176
|
+
used.add(s);
|
|
3177
|
+
}
|
|
3178
|
+
}
|
|
3179
|
+
}
|
|
3019
3180
|
if (pkCols.length > 0) {
|
|
3020
3181
|
const keyOf = (row) => pkCols.map(c => (row[c] !== undefined && row[c] !== null ? String(row[c]) : '')).join('|');
|
|
3021
3182
|
const hasExplicitPk = (row) => pkCols.some(c => row[c] !== undefined && row[c] !== null);
|
|
@@ -3028,11 +3189,8 @@ class SQLExecutor {
|
|
|
3028
3189
|
: null;
|
|
3029
3190
|
const extraIdx = [];
|
|
3030
3191
|
if (ocTarget && ocTarget.length) extraIdx.push(ocTarget);
|
|
3031
|
-
|
|
3032
|
-
|
|
3033
|
-
if (def && def.unique && !def.primaryKey) extraIdx.push([c]);
|
|
3034
|
-
}
|
|
3035
|
-
}
|
|
3192
|
+
// 唯一列始终参与冲突判定(PG 的 ON CONFLICT 与 MySQL 的 ON DUPLICATE 都包含唯一键)
|
|
3193
|
+
for (const c of uniqueCols) extraIdx.push([c]);
|
|
3036
3194
|
const hasAllOf = (cols) => (row) => cols.every(c => row[c] !== undefined && row[c] !== null);
|
|
3037
3195
|
|
|
3038
3196
|
const explicit = dataRows.filter((row) => hasExplicitPk(row) || extraIdx.some((cols) => hasAllOf(cols)(row)));
|
|
@@ -4504,16 +4662,16 @@ class SQLExecutor {
|
|
|
4504
4662
|
const rightNulls = (rightRes.schema) ? this._nullPrefixedRow(rightRes.schema, rightPrefix) : null;
|
|
4505
4663
|
const leftNulls = (rowsAll && rowsAll.schema) ? this._nullPrefixedRow(rowsAll.schema, firstPrefix) : null;
|
|
4506
4664
|
rows.forEach(l => {
|
|
4507
|
-
|
|
4665
|
+
// 一对多:一个左行可能匹配多个右行,必须全部收进来(此前只取第一个)
|
|
4666
|
+
const hits = [];
|
|
4508
4667
|
for (let ri = 0; ri < rightRows.length; ri++) {
|
|
4509
|
-
if (evaluateExpr(j.on, { ...l, ...rightRows[ri] }))
|
|
4510
|
-
m = ri;
|
|
4511
|
-
break;
|
|
4512
|
-
}
|
|
4668
|
+
if (evaluateExpr(j.on, { ...l, ...rightRows[ri] })) hits.push(ri);
|
|
4513
4669
|
}
|
|
4514
|
-
if (
|
|
4515
|
-
|
|
4516
|
-
|
|
4670
|
+
if (hits.length > 0) {
|
|
4671
|
+
for (const ri of hits) {
|
|
4672
|
+
matched.push({ ...l, ...rightRows[ri] });
|
|
4673
|
+
unmatchedRight.delete(ri);
|
|
4674
|
+
}
|
|
4517
4675
|
} else if (j.type === 'left' || j.type === 'full') {
|
|
4518
4676
|
matched.push(rightNulls ? { ...rightNulls, ...l } : { ...l });
|
|
4519
4677
|
}
|
|
@@ -4589,6 +4747,10 @@ class SQLExecutor {
|
|
|
4589
4747
|
const seen = new Set();
|
|
4590
4748
|
rows = rows.filter(r => {
|
|
4591
4749
|
const key = JSON.stringify(statement.columns.map(c => {
|
|
4750
|
+
// SELECT DISTINCT *:按整行去重(此前把 '*' 当列名解析成 null,导致所有行同 key 只剩 1 行)
|
|
4751
|
+
if (c.expr === '*') {
|
|
4752
|
+
return Object.keys(r).filter(k => !String(k).startsWith('_')).sort().map(k => r[k]);
|
|
4753
|
+
}
|
|
4592
4754
|
if (c.scalar) return resolveOperand(c.scalar, r, this.ctx);
|
|
4593
4755
|
if (c.caseExpr) return evaluateCaseVal(c.caseExpr, r);
|
|
4594
4756
|
if (c.aggregate) return this._aggValue(r._group || [r], c.aggregate || 'COUNT', c.column, c.distinct, c.separator, c.orderBy);
|
|
@@ -4636,10 +4798,20 @@ class SQLExecutor {
|
|
|
4636
4798
|
}
|
|
4637
4799
|
|
|
4638
4800
|
if (statement.orderBy) {
|
|
4639
|
-
// ORDER BY
|
|
4640
|
-
const
|
|
4641
|
-
|
|
4642
|
-
|
|
4801
|
+
// ORDER BY 项可能是列名字符串、表达式节点,或列的位置序号(ORDER BY 1,1-based)
|
|
4802
|
+
const ordNames = statement.columns.map(c => scalarColumnName(c));
|
|
4803
|
+
const ordVal = (row, o) => {
|
|
4804
|
+
const pos = (o.column && typeof o.column === 'object' && o.column.type === 'value' && typeof o.column.value === 'number')
|
|
4805
|
+
? o.column.value
|
|
4806
|
+
: (typeof o.column === 'number' ? o.column : null);
|
|
4807
|
+
if (pos !== null && pos !== undefined) {
|
|
4808
|
+
const name = ordNames[pos - 1];
|
|
4809
|
+
if (name !== undefined && name !== null) return resolveOperand({ type: 'column', name }, row, this.ctx);
|
|
4810
|
+
}
|
|
4811
|
+
return typeof o.column === 'string'
|
|
4812
|
+
? resolveOperand({ type: 'column', name: o.column }, row, this.ctx)
|
|
4813
|
+
: resolveOperand(o.column, row, this.ctx);
|
|
4814
|
+
};
|
|
4643
4815
|
const cmp = (a, b) => {
|
|
4644
4816
|
for (const o of statement.orderBy) {
|
|
4645
4817
|
const av = ordVal(a, o);
|
|
@@ -4690,53 +4862,88 @@ class SQLExecutor {
|
|
|
4690
4862
|
}
|
|
4691
4863
|
}
|
|
4692
4864
|
|
|
4693
|
-
|
|
4694
|
-
|
|
4695
|
-
|
|
4696
|
-
|
|
4697
|
-
|
|
4698
|
-
while (i < sql.length) {
|
|
4865
|
+
/** 跳过一个引号串(含转义/反引号成对),返回闭合引号之后的下标 */
|
|
4866
|
+
function skipQuoted(sql, i, q) {
|
|
4867
|
+
const n = sql.length;
|
|
4868
|
+
i++; // 跳过起始引号
|
|
4869
|
+
while (i < n) {
|
|
4699
4870
|
const c = sql[i];
|
|
4700
|
-
if (
|
|
4701
|
-
|
|
4702
|
-
if (
|
|
4703
|
-
|
|
4704
|
-
|
|
4705
|
-
if (c === '`') inStr = null;
|
|
4706
|
-
i++;
|
|
4707
|
-
continue;
|
|
4871
|
+
if (q === '`') {
|
|
4872
|
+
// 反引号标识符内部反斜杠不是转义符;两个连续反引号表示一个字面反引号。
|
|
4873
|
+
if (c === '`') {
|
|
4874
|
+
if (sql[i + 1] === '`') { i += 2; continue; }
|
|
4875
|
+
return i + 1;
|
|
4708
4876
|
}
|
|
4709
|
-
if (c === '\\' && i + 1 < sql.length) { current += sql[i + 1]; i += 2; continue; }
|
|
4710
|
-
if (c === inStr) inStr = null;
|
|
4711
4877
|
i++;
|
|
4712
4878
|
continue;
|
|
4713
4879
|
}
|
|
4714
|
-
if (c ===
|
|
4880
|
+
if (c === '\\') { i += 2; continue; }
|
|
4881
|
+
if (c === q) return i + 1;
|
|
4882
|
+
i++;
|
|
4883
|
+
}
|
|
4884
|
+
return n; // 未闭合:到结尾
|
|
4885
|
+
}
|
|
4886
|
+
|
|
4887
|
+
function splitStatements(sql, opts = {}) {
|
|
4888
|
+
if (typeof sql !== 'string') {
|
|
4889
|
+
throw new Error('splitStatements: sql must be a string');
|
|
4890
|
+
}
|
|
4891
|
+
const maxLength = opts.maxLength == null ? DEFAULT_MAX_SQL_LENGTH : opts.maxLength;
|
|
4892
|
+
if (typeof maxLength === 'number' && Number.isFinite(maxLength) && sql.length > maxLength) {
|
|
4893
|
+
throw new Error(
|
|
4894
|
+
`SQL text too large: ${sql.length} chars exceeds limit of ${maxLength} ` +
|
|
4895
|
+
`(raise opts.maxSqlLength to allow larger input)`
|
|
4896
|
+
);
|
|
4897
|
+
}
|
|
4898
|
+
const statements = [];
|
|
4899
|
+
// 用「区间切片」收集,而不是逐字符拼接:既避免 `current += c` 的 O(n²) 拷贝放大,
|
|
4900
|
+
// 也避免为每个字符建一个小字符串对象(20MB 无分号输入下两者都会打爆默认堆)。
|
|
4901
|
+
let parts = [];
|
|
4902
|
+
const take = end => { parts.push(sql.slice(segStart, end)); };
|
|
4903
|
+
const flush = () => {
|
|
4904
|
+
if (parts.length === 0) return;
|
|
4905
|
+
const stmt = parts.join('').trim();
|
|
4906
|
+
parts = [];
|
|
4907
|
+
if (stmt) statements.push(stmt);
|
|
4908
|
+
};
|
|
4909
|
+
|
|
4910
|
+
let segStart = 0;
|
|
4911
|
+
let i = 0;
|
|
4912
|
+
const n = sql.length;
|
|
4913
|
+
while (i < n) {
|
|
4914
|
+
const c = sql[i];
|
|
4915
|
+
if (c === "'" || c === '"' || c === '`') { i = skipQuoted(sql, i, c); continue; }
|
|
4715
4916
|
if (c === '-' && sql[i + 1] === '-') {
|
|
4716
|
-
|
|
4917
|
+
// 行注释:丢弃注释文本本身(与原实现一致),保留其后的换行
|
|
4918
|
+
take(i);
|
|
4919
|
+
while (i < n && sql[i] !== '\n') i++;
|
|
4920
|
+
segStart = i;
|
|
4717
4921
|
continue;
|
|
4718
4922
|
}
|
|
4719
|
-
if (c === '#') {
|
|
4720
|
-
|
|
4923
|
+
if (c === '#' && sql[i + 1] !== '>') {
|
|
4924
|
+
take(i);
|
|
4925
|
+
while (i < n && sql[i] !== '\n') i++;
|
|
4926
|
+
segStart = i;
|
|
4721
4927
|
continue;
|
|
4722
4928
|
}
|
|
4723
4929
|
if (c === '/' && sql[i + 1] === '*') {
|
|
4724
|
-
|
|
4930
|
+
// 块注释:原实现会保留 `/* */` 原文,这里通过切片一并保留
|
|
4725
4931
|
i += 2;
|
|
4726
|
-
while (i + 1 <
|
|
4727
|
-
|
|
4932
|
+
while (i + 1 < n && !(sql[i] === '*' && sql[i + 1] === '/')) i++;
|
|
4933
|
+
i = i + 1 < n ? i + 2 : n;
|
|
4728
4934
|
continue;
|
|
4729
4935
|
}
|
|
4730
4936
|
if (c === ';') {
|
|
4731
|
-
|
|
4732
|
-
|
|
4937
|
+
take(i);
|
|
4938
|
+
flush();
|
|
4733
4939
|
i++;
|
|
4940
|
+
segStart = i;
|
|
4734
4941
|
continue;
|
|
4735
4942
|
}
|
|
4736
|
-
current += c;
|
|
4737
4943
|
i++;
|
|
4738
4944
|
}
|
|
4739
|
-
|
|
4945
|
+
take(n);
|
|
4946
|
+
flush();
|
|
4740
4947
|
return statements;
|
|
4741
4948
|
}
|
|
4742
4949
|
|
|
@@ -4763,7 +4970,7 @@ function hasComments(sql) {
|
|
|
4763
4970
|
if (c === "'" || c === '"') { inStr = c; i++; continue; }
|
|
4764
4971
|
if (c === '`') { i++; while (i < sql.length && sql[i] !== '`') i++; i++; continue; }
|
|
4765
4972
|
if (c === '-' && sql[i + 1] === '-') return true;
|
|
4766
|
-
if (c === '#') return true;
|
|
4973
|
+
if (c === '#' && sql[i + 1] !== '>') return true; // '#>' / '#>>' 是 JSON 操作符,不是注释
|
|
4767
4974
|
if (c === '/' && sql[i + 1] === '*') return true;
|
|
4768
4975
|
i++;
|
|
4769
4976
|
}
|
|
@@ -4812,16 +5019,34 @@ function applyParams(sql, values) {
|
|
|
4812
5019
|
if (args && typeof args === 'object' && !Array.isArray(args)) {
|
|
4813
5020
|
hasNamed = true;
|
|
4814
5021
|
}
|
|
4815
|
-
|
|
5022
|
+
const n = sql.length;
|
|
5023
|
+
while (i < n) {
|
|
4816
5024
|
const c = sql[i];
|
|
4817
5025
|
if (inStr) {
|
|
4818
5026
|
out += c;
|
|
4819
|
-
if (c === '\\' && i + 1 <
|
|
5027
|
+
if (c === '\\' && i + 1 < n) { out += sql[i + 1]; i += 2; continue; }
|
|
4820
5028
|
if (c === inStr) inStr = null;
|
|
4821
5029
|
i++;
|
|
4822
5030
|
continue;
|
|
4823
5031
|
}
|
|
4824
5032
|
if (c === "'" || c === '"' || c === '`') { inStr = c; out += c; i++; continue; }
|
|
5033
|
+
// 跳过注释:`--` 和 `#`(但 `#>` / `#>>` 是 JSON 操作符,不是注释)
|
|
5034
|
+
if (c === '-' && sql[i + 1] === '-') {
|
|
5035
|
+
// 行注释:整段跳过,保留换行(不在注释里的字符不进入 out)
|
|
5036
|
+
while (i < n && sql[i] !== '\n') i++;
|
|
5037
|
+
continue;
|
|
5038
|
+
}
|
|
5039
|
+
if (c === '#' && sql[i + 1] !== '>') {
|
|
5040
|
+
while (i < n && sql[i] !== '\n') i++;
|
|
5041
|
+
continue;
|
|
5042
|
+
}
|
|
5043
|
+
if (c === '/' && sql[i + 1] === '*') {
|
|
5044
|
+
// 块注释:整段跳过
|
|
5045
|
+
i += 2;
|
|
5046
|
+
while (i + 1 < n && !(sql[i] === '*' && sql[i + 1] === '/')) i++;
|
|
5047
|
+
if (i + 1 < n) i += 2; else i = n;
|
|
5048
|
+
continue;
|
|
5049
|
+
}
|
|
4825
5050
|
if (c === '?' && sql[i + 1] === '?') {
|
|
4826
5051
|
if (!hasNamed && idx >= args.length) throw new Error('Not enough parameters for SQL: expected ' + (count + 1));
|
|
4827
5052
|
out += escapeId(hasNamed ? args['@@'] : args[idx++]);
|
|
@@ -4854,6 +5079,24 @@ function applyParams(sql, values) {
|
|
|
4854
5079
|
i++;
|
|
4855
5080
|
continue;
|
|
4856
5081
|
}
|
|
5082
|
+
// $N 编号占位符(PG 风格):$1 / $2 ...
|
|
5083
|
+
if (c === '$' && i + 1 < sql.length && sql[i + 1] >= '0' && sql[i + 1] <= '9') {
|
|
5084
|
+
let num = '';
|
|
5085
|
+
let j = i + 1;
|
|
5086
|
+
while (j < sql.length && sql[j] >= '0' && sql[j] <= '9') { num += sql[j]; j++; }
|
|
5087
|
+
const n = parseInt(num, 10);
|
|
5088
|
+
if (hasNamed) {
|
|
5089
|
+
if (!(n in args)) throw new Error(`No value for parameter $${n}`);
|
|
5090
|
+
out += escapeValue(args[n]);
|
|
5091
|
+
} else {
|
|
5092
|
+
if (n - 1 >= args.length) throw new Error(`Not enough parameters for SQL: expected $${n}`);
|
|
5093
|
+
out += escapeValue(args[n - 1]);
|
|
5094
|
+
if (n > idx) idx = n;
|
|
5095
|
+
}
|
|
5096
|
+
count++;
|
|
5097
|
+
i = j;
|
|
5098
|
+
continue;
|
|
5099
|
+
}
|
|
4857
5100
|
if ((c === ':' || c === '@' || c === '$') && i + 1 < sql.length && /[A-Za-z_]/.test(sql[i + 1])) {
|
|
4858
5101
|
// 命名占位符 :name @name $name
|
|
4859
5102
|
let name = '';
|
|
@@ -4936,14 +5179,19 @@ async function executeSQL(engine, sql, paramsOrOpts, opts = {}) {
|
|
|
4936
5179
|
if (Array.isArray(paramsOrOpts)) {
|
|
4937
5180
|
sql = applyParams(sql, paramsOrOpts);
|
|
4938
5181
|
} else if (paramsOrOpts && typeof paramsOrOpts === 'object') {
|
|
4939
|
-
opts
|
|
5182
|
+
// 第三参为对象时:含已知 opts 键 → 视为 opts;否则视为命名参数({id: 1} → :id / $id)
|
|
5183
|
+
const OPTS_KEYS = ['safety', 'session', 'params', 'functions', 'aggregates',
|
|
5184
|
+
'allowComments', 'maxStatements', 'maxSqlLength', 'context', 'timeout', 'dialect'];
|
|
5185
|
+
const looksLikeOpts = Object.keys(paramsOrOpts).some(k => OPTS_KEYS.includes(k));
|
|
5186
|
+
if (looksLikeOpts) opts = paramsOrOpts;
|
|
5187
|
+
else sql = applyParams(sql, paramsOrOpts);
|
|
4940
5188
|
}
|
|
4941
5189
|
if (opts.safety !== false) {
|
|
4942
5190
|
if (!opts.allowComments && hasComments(sql)) {
|
|
4943
5191
|
throw new Error('SQL comments are disabled for security (--, #, /* */)');
|
|
4944
5192
|
}
|
|
4945
5193
|
}
|
|
4946
|
-
let statements = splitStatements(sql);
|
|
5194
|
+
let statements = splitStatements(sql, { maxLength: opts.maxSqlLength });
|
|
4947
5195
|
if (opts.maxStatements != null && statements.length > opts.maxStatements) {
|
|
4948
5196
|
throw new Error(`too many statements (${statements.length} > ${opts.maxStatements})`);
|
|
4949
5197
|
}
|
package/package.json
CHANGED
package/postinstall.js
CHANGED
|
@@ -14,10 +14,43 @@ const BP = path.join(ROOT, 'bin', BIN_NAME);
|
|
|
14
14
|
// Native N-API module (native/jsql-neo-native.node)
|
|
15
15
|
const NATIVE_PATH = path.join(ROOT, 'native', 'jsql-neo-native.node');
|
|
16
16
|
|
|
17
|
-
// GitHub
|
|
18
|
-
|
|
17
|
+
// GitHub 加速代理前缀;加速站的用法是把原始 URL 直接拼在后面:
|
|
18
|
+
// https://<host>/https://github.com/owner/repo/releases/download/vX.Y.Z/asset?token=<TOKEN>
|
|
19
|
+
// 可用环境变量覆盖 / 叠加(逗号分隔多个,按序尝试);空字符串 '' 表示直连兜底。
|
|
20
|
+
// 加速站候选(按优先级):自建站优先,公共站回退,最后直连兜底。
|
|
21
|
+
const DEFAULT_PROXIES = [
|
|
22
|
+
'https://gh-proxyjsql-neoworkerapi.vexify.de5.net/',
|
|
23
|
+
'https://gh-proxy.com/',
|
|
24
|
+
];
|
|
25
|
+
const GHPROXY = process.env.JSQL_GHPROXY !== undefined
|
|
26
|
+
? process.env.JSQL_GHPROXY
|
|
27
|
+
: DEFAULT_PROXIES[0];
|
|
28
|
+
const GHPROXY_TOKEN = process.env.JSQL_GHPROXY_TOKEN || '';
|
|
19
29
|
const TAG = `v${pkg.version}`;
|
|
20
30
|
|
|
31
|
+
/**
|
|
32
|
+
* 生成候选下载地址列表(按优先级):
|
|
33
|
+
* 环境变量 JSQL_GHPROXY 指定的代理 → 内置加速站 → 直连
|
|
34
|
+
*/
|
|
35
|
+
function proxyCandidates(raw) {
|
|
36
|
+
const bases = String(process.env.JSQL_GHPROXY || '')
|
|
37
|
+
.split(',')
|
|
38
|
+
.map((s) => s.trim())
|
|
39
|
+
.filter(Boolean)
|
|
40
|
+
.concat(DEFAULT_PROXIES)
|
|
41
|
+
.concat('');
|
|
42
|
+
const list = [];
|
|
43
|
+
for (const base of bases) {
|
|
44
|
+
let url = raw;
|
|
45
|
+
if (base) url = (base.endsWith('/') ? base : base + '/') + raw;
|
|
46
|
+
if (base && GHPROXY_TOKEN) {
|
|
47
|
+
url += (url.indexOf('?') === -1 ? '?' : '&') + 'token=' + encodeURIComponent(GHPROXY_TOKEN);
|
|
48
|
+
}
|
|
49
|
+
if (list.indexOf(url) === -1) list.push(url);
|
|
50
|
+
}
|
|
51
|
+
return list;
|
|
52
|
+
}
|
|
53
|
+
|
|
21
54
|
// 从 repository 字段提取 owner/repo,例如 vexify-org/JSQL-neo
|
|
22
55
|
const REPO = (pkg.repository && pkg.repository.url)
|
|
23
56
|
? pkg.repository.url.replace(/\.git$/, '').replace(/^.*github\.com\//i, '')
|
|
@@ -95,20 +128,22 @@ function tryLoadNative() {
|
|
|
95
128
|
|
|
96
129
|
async function downloadAndVerify() {
|
|
97
130
|
const asset = `jsql-neo-native.${nativeTargetSuffix()}.node`;
|
|
98
|
-
const
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
131
|
+
const raw = `https://github.com/${REPO}/releases/download/${TAG}/${asset}`;
|
|
132
|
+
// 依次尝试:环境变量指定的代理 → 内置加速站 → 直连
|
|
133
|
+
for (const url of proxyCandidates(raw)) {
|
|
134
|
+
console.log(`[jsql-neo] downloading native module: ${url}`);
|
|
135
|
+
try {
|
|
136
|
+
await download(url, NATIVE_PATH);
|
|
137
|
+
if (tryLoadNative()) {
|
|
138
|
+
console.log('[jsql-neo] native module downloaded: native/jsql-neo-native.node');
|
|
139
|
+
return true;
|
|
140
|
+
}
|
|
141
|
+
console.warn('[jsql-neo] downloaded module cannot be loaded; trying next source.');
|
|
142
|
+
} catch (e) {
|
|
143
|
+
console.warn('[jsql-neo] download failed:', e.message);
|
|
105
144
|
}
|
|
106
|
-
console.log('[jsql-neo] native module downloaded: native/jsql-neo-native.node');
|
|
107
|
-
return true;
|
|
108
|
-
} catch (e) {
|
|
109
|
-
console.warn('[jsql-neo] download failed, falling back to source build:', e.message);
|
|
110
|
-
return false;
|
|
111
145
|
}
|
|
146
|
+
return false;
|
|
112
147
|
}
|
|
113
148
|
|
|
114
149
|
async function ensureNativeNode() {
|
|
@@ -142,16 +142,16 @@ async function main() {
|
|
|
142
142
|
console.log('\n--- 方言特性:DIV / XOR / && / || / <=> / :: / -> ->> / LIKE ESCAPE ---');
|
|
143
143
|
{
|
|
144
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 },
|
|
145
|
+
{ id: 1, n: 10, flag: 1, s: '100%', nm: 'Alice', meta: '{"name":"Alice","addr":{"city":"SH"},"tags":["x","y"]}' },
|
|
146
|
+
{ id: 2, n: 20, flag: 0, s: '1000', nm: 'Bob', meta: '{"name":"Bob","addr":{"city":"BJ"},"tags":[]}' },
|
|
147
|
+
{ id: 3, n: 30, flag: 1, s: '100', nm: 'Cara', meta: null },
|
|
148
148
|
];
|
|
149
149
|
const engine = {
|
|
150
150
|
hasTable: () => true, truncate: async () => {}, flush: async () => {},
|
|
151
151
|
find: async () => data.map(r => ({ ...r })),
|
|
152
152
|
getTableSchema: async () => ({
|
|
153
153
|
id: { type: 'integer', primaryKey: true }, n: { type: 'integer' }, flag: { type: 'integer' },
|
|
154
|
-
s: { type: 'string' }, meta: { type: 'json' },
|
|
154
|
+
s: { type: 'string' }, nm: { type: 'string' }, meta: { type: 'json' },
|
|
155
155
|
}),
|
|
156
156
|
};
|
|
157
157
|
const rowsOf = async (sql) => {
|
|
@@ -176,12 +176,56 @@ async function main() {
|
|
|
176
176
|
JSON.stringify(await rowsOf("SELECT meta->'addr'->>'city' AS x FROM tbl WHERE id = 1")) === '[["SH"]]');
|
|
177
177
|
ok("->'tags'->>0 数组下标",
|
|
178
178
|
JSON.stringify(await rowsOf("SELECT meta->'tags'->>0 AS x FROM tbl WHERE id = 1")) === '[["x"]]');
|
|
179
|
+
// PG JSON 包含 / 路径操作符
|
|
180
|
+
ok("meta @> '{...}' 包含",
|
|
181
|
+
JSON.stringify(await rowsOf(`SELECT id FROM tbl WHERE meta @> '{"addr":{"city":"SH"}}'`)) === '[[1]]');
|
|
182
|
+
ok("'{...}' <@ meta 被包含",
|
|
183
|
+
JSON.stringify(await rowsOf(`SELECT id FROM tbl WHERE '{"name":"Bob"}' <@ meta`)) === '[[2]]');
|
|
184
|
+
ok("meta#>'{addr,city}' 路径",
|
|
185
|
+
JSON.stringify(await rowsOf("SELECT meta#>'{addr,city}' AS x FROM tbl WHERE id = 1")) === '[["SH"]]');
|
|
186
|
+
ok("meta#>>'{addr,city}' 路径文本",
|
|
187
|
+
JSON.stringify(await rowsOf("SELECT meta#>>'{addr,city}' AS x FROM tbl WHERE id = 1")) === '[["SH"]]');
|
|
188
|
+
ok("meta#>'{tags,0}' 路径含下标",
|
|
189
|
+
JSON.stringify(await rowsOf("SELECT meta#>'{tags,0}' AS x FROM tbl WHERE id = 1")) === '[["x"]]');
|
|
190
|
+
// JSON 键存在 ? / ?| / ?&
|
|
191
|
+
ok("meta ? 'name'", JSON.stringify(await rowsOf("SELECT id FROM tbl WHERE meta ? 'name'")) === '[[1],[2]]');
|
|
192
|
+
ok("meta ? 'nope'(不存在)",
|
|
193
|
+
JSON.stringify(await rowsOf("SELECT id FROM tbl WHERE meta ? 'nope'")) === '[]');
|
|
194
|
+
ok("meta ?| '{name,nope}'(任一)",
|
|
195
|
+
JSON.stringify(await rowsOf("SELECT id FROM tbl WHERE meta ?| '{name,nope}'")) === '[[1],[2]]');
|
|
196
|
+
ok("meta ?& '{name,addr}'(全部)",
|
|
197
|
+
JSON.stringify(await rowsOf("SELECT id FROM tbl WHERE meta ?& '{name,addr}'")) === '[[1],[2]]');
|
|
198
|
+
ok("meta ?& '{name,nope}'(缺一个)",
|
|
199
|
+
JSON.stringify(await rowsOf("SELECT id FROM tbl WHERE meta ?& '{name,nope}'")) === '[]');
|
|
200
|
+
// PG 正则:~ 敏感 / ~* 不敏感 / !~ 取反
|
|
201
|
+
ok("nm ~ '^A' 大小写敏感", JSON.stringify(await rowsOf("SELECT id FROM tbl WHERE nm ~ '^A'")) === '[[1]]');
|
|
202
|
+
ok("nm ~* '^a' 不敏感", JSON.stringify(await rowsOf("SELECT id FROM tbl WHERE nm ~* '^a'")) === '[[1]]');
|
|
203
|
+
ok("nm !~ '^A' 取反", JSON.stringify(await rowsOf("SELECT id FROM tbl WHERE nm !~ '^A'")) === '[[2],[3]]');
|
|
204
|
+
ok('REGEXP 仍可用', JSON.stringify(await rowsOf("SELECT id FROM tbl WHERE nm REGEXP '^A'")) === '[[1]]');
|
|
205
|
+
// —— 6.2.0 回归项(这些曾不报错但结果错)——
|
|
206
|
+
ok('NOT IN 取反', JSON.stringify(await rowsOf('SELECT id FROM tbl WHERE id NOT IN (1,2)')) === '[[3]]');
|
|
207
|
+
const dStar = await rowsOf('SELECT DISTINCT * FROM tbl');
|
|
208
|
+
ok('DISTINCT * 按整行去重(此前只剩 1 行)', dStar.length === 3, dStar);
|
|
209
|
+
ok('ORDER BY 1 按第 1 列排序', JSON.stringify(await rowsOf('SELECT n FROM tbl ORDER BY 1')) === '[[10],[20],[30]]');
|
|
210
|
+
ok("'' 内双写引号转义",
|
|
211
|
+
JSON.stringify(await rowsOf("SELECT 'O''Brien' AS x FROM tbl WHERE id = 1")) === '[["O\'Brien"]]');
|
|
179
212
|
ok("LIKE ... ESCAPE '!' 匹配字面量 %",
|
|
180
213
|
JSON.stringify(await rowsOf("SELECT id FROM tbl WHERE s LIKE '100!%' ESCAPE '!'")) === '[[1]]');
|
|
181
214
|
ok('LIKE 反斜杠转义匹配字面量 %',
|
|
182
215
|
JSON.stringify(await rowsOf("SELECT id FROM tbl WHERE s LIKE '100\\%'")) === '[[1]]');
|
|
183
216
|
ok('LIKE 普通 % 仍为通配',
|
|
184
217
|
JSON.stringify(await rowsOf("SELECT id FROM tbl WHERE s LIKE '100%'")) === '[[1],[2],[3]]');
|
|
218
|
+
|
|
219
|
+
// 参数绑定:位置 ? / 命名 :name / 编号 $1(README「Params」一节)
|
|
220
|
+
const rowsOfP = async (sql, params) => {
|
|
221
|
+
const r = await executeSQL(engine, sql, params, OPTS);
|
|
222
|
+
return (Array.isArray(r) ? r[0] : r).rows;
|
|
223
|
+
};
|
|
224
|
+
ok('位置参数 ?', JSON.stringify(await rowsOfP('SELECT id FROM tbl WHERE id = ?', [1])) === '[[1]]');
|
|
225
|
+
ok('命名参数 :id', JSON.stringify(await rowsOfP('SELECT id FROM tbl WHERE id = :id', { id: 2 })) === '[[2]]');
|
|
226
|
+
ok('命名参数多占位', JSON.stringify(await rowsOfP('SELECT id FROM tbl WHERE id = :a OR id = :b', { a: 1, b: 3 })) === '[[1],[3]]');
|
|
227
|
+
ok('编号参数 $1', JSON.stringify(await rowsOfP('SELECT id FROM tbl WHERE id = $1', [3])) === '[[3]]');
|
|
228
|
+
ok('编号参数 $2/$1', JSON.stringify(await rowsOfP('SELECT id FROM tbl WHERE id = $2 OR id = $1', [1, 2])) === '[[1],[2]]');
|
|
185
229
|
}
|
|
186
230
|
|
|
187
231
|
console.log('\n--- DDL / 解析器节点覆盖 ---');
|
|
@@ -325,6 +369,64 @@ async function main() {
|
|
|
325
369
|
}
|
|
326
370
|
}
|
|
327
371
|
|
|
372
|
+
console.log('\n--- 唯一约束:非主键 UNIQUE 必须报 ER_DUP_ENTRY ---');
|
|
373
|
+
{
|
|
374
|
+
let JSQL, db;
|
|
375
|
+
try {
|
|
376
|
+
({ JSQL } = require('../lib/native_client.js'));
|
|
377
|
+
db = new JSQL();
|
|
378
|
+
await db.start();
|
|
379
|
+
} catch (e) {
|
|
380
|
+
console.log('[SKIP] native 不可用:' + e.message.slice(0, 60));
|
|
381
|
+
}
|
|
382
|
+
if (db) {
|
|
383
|
+
const run = (sql) => executeSQL(db, sql, OPTS);
|
|
384
|
+
const dupErrMsg = async (sql) => {
|
|
385
|
+
try { await run(sql); return null; } catch (e) { return e.message; }
|
|
386
|
+
};
|
|
387
|
+
await run('CREATE TABLE u (id SERIAL PRIMARY KEY, email TEXT UNIQUE, nick TEXT UNIQUE)');
|
|
388
|
+
await run("INSERT INTO u (email, nick) VALUES ('a@x.com', 'a')");
|
|
389
|
+
ok('唯一列重复 → ER_DUP_ENTRY',
|
|
390
|
+
/ER_DUP_ENTRY/.test(await dupErrMsg("INSERT INTO u (email, nick) VALUES ('a@x.com', 'z')") || ''));
|
|
391
|
+
ok('批内唯一重复 → ER_DUP_ENTRY',
|
|
392
|
+
/ER_DUP_ENTRY/.test(await dupErrMsg("INSERT INTO u (email, nick) VALUES ('b@x.com', 'n1'), ('c@x.com', 'n1')") || ''));
|
|
393
|
+
const r1 = await run('SELECT id FROM u');
|
|
394
|
+
ok('冲突后仍只有 1 行', r1.rows.length === 1, r1.rows);
|
|
395
|
+
await run("INSERT INTO u (email, nick) VALUES (NULL, 'x'), (NULL, 'y')");
|
|
396
|
+
const r2 = await run('SELECT id FROM u');
|
|
397
|
+
ok('UNIQUE 允许多个 NULL', r2.rows.length === 3, r2.rows);
|
|
398
|
+
await db.stop();
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
console.log('\n--- 6.2.0 回归:JOIN 一对多 / NOW() 用本地时区 ---');
|
|
403
|
+
{
|
|
404
|
+
let JSQL, db;
|
|
405
|
+
try {
|
|
406
|
+
({ JSQL } = require('../lib/native_client.js'));
|
|
407
|
+
db = new JSQL();
|
|
408
|
+
await db.start();
|
|
409
|
+
} catch (e) {
|
|
410
|
+
console.log('[SKIP] native 不可用:' + e.message.slice(0, 60));
|
|
411
|
+
}
|
|
412
|
+
if (db) {
|
|
413
|
+
const run = (sql) => executeSQL(db, sql, OPTS);
|
|
414
|
+
await run('CREATE TABLE ru (id INT PRIMARY KEY, name TEXT)');
|
|
415
|
+
await run('CREATE TABLE ro (id INT PRIMARY KEY, uid INT, amt INT)');
|
|
416
|
+
await run("INSERT INTO ru (id,name) VALUES (1,'x')");
|
|
417
|
+
await run('INSERT INTO ro (id,uid,amt) VALUES (1,1,10),(2,1,20),(3,1,30)');
|
|
418
|
+
const j = await run('SELECT ru.name, ro.amt FROM ru JOIN ro ON ru.id = ro.uid');
|
|
419
|
+
ok('JOIN 一对多返回 3 行', j.rows.length === 3, j.rows);
|
|
420
|
+
|
|
421
|
+
const n = await run('SELECT NOW() AS d');
|
|
422
|
+
const nowLocal = new Date();
|
|
423
|
+
const p = (x) => String(x).padStart(2, '0');
|
|
424
|
+
const expectYmd = nowLocal.getFullYear() + '-' + p(nowLocal.getMonth() + 1) + '-' + p(nowLocal.getDate());
|
|
425
|
+
ok('NOW() 返回本地时区日期', String(n.rows[0][0]).slice(0, 10) === expectYmd, n.rows);
|
|
426
|
+
await db.stop();
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
328
430
|
console.log('\n--- Redis:RESP2 帧解析(曾 inline 命令恒为 unknown command)---');
|
|
329
431
|
{
|
|
330
432
|
const { RedisServer } = require('../lib/redis_server.js');
|