jsql-neo 6.4.1 → 6.4.3
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 +1 -1
- package/index.js +1 -1
- package/lib/database.js +6 -1
- package/lib/native_client.js +14 -2
- package/lib/sql.js +130 -33
- package/lib/table.js +182 -4
- package/lib/wasm_client.js +3 -1
- package/native/jsql-neo-native.node +0 -0
- package/nativesrc/jsql-neo-core/src/engine/mod.rs +9 -0
- package/nativesrc/jsql-neo-native/src/lib.rs +17 -0
- package/nativesrc/jsql-neo-wasm/src/lib.rs +11 -89
- package/package.json +5 -3
- package/test/alter-table.test.js +19 -0
- package/test/extra-coverage.test.js +618 -0
- package/test/filter-compile.test.js +199 -0
- package/test/sql-semantics.test.js +131 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jsql-neo",
|
|
3
|
-
"version": "6.4.
|
|
3
|
+
"version": "6.4.3",
|
|
4
4
|
"description": "JSQL-NEO — Rust-powered embedded database with WASM, REST API, B-Tree indexes, WAL, crash recovery",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"types": "index.d.ts",
|
|
@@ -51,7 +51,7 @@
|
|
|
51
51
|
"test:btree": "node test/btree.test.js",
|
|
52
52
|
"test:native": "node test/native.test.js",
|
|
53
53
|
"test:regress": "node test/regress-5.1.0.js",
|
|
54
|
-
"test:all": "node test/smoke.js && node test/sql-parser.test.js && node test/readme-audit.test.js && node test/native.test.js && node test/native-multi-instance.test.js && node test/wal-recovery.test.js && node test/mysql-compat-hijack.test.js && node test/wasm.test.js && node test/coverage.js && node test/regress-5.1.0.js && node test/join.test.js && node test/btree.test.js && node test/dml-constraints.test.js && node test/savepoint.test.js && node test/alter-table.test.js && node test/null-semantics.test.js && node test/identifier-keywords.test.js && node test/recursive-cte.test.js && node test/index-ddl.test.js",
|
|
54
|
+
"test:all": "node test/smoke.js && node test/sql-parser.test.js && node test/readme-audit.test.js && node test/native.test.js && node test/native-multi-instance.test.js && node test/wal-recovery.test.js && node test/mysql-compat-hijack.test.js && node test/wasm.test.js && node test/coverage.js && node test/regress-5.1.0.js && node test/join.test.js && node test/btree.test.js && node test/dml-constraints.test.js && node test/savepoint.test.js && node test/alter-table.test.js && node test/null-semantics.test.js && node test/identifier-keywords.test.js && node test/sql-semantics.test.js && node test/extra-coverage.test.js && node test/filter-compile.test.js && node test/recursive-cte.test.js && node test/index-ddl.test.js",
|
|
55
55
|
"test:coverage": "c8 --all --include='lib/**' --exclude='lib/web_ui.js' --reporter=text --reporter=lcov node test/coverage.js",
|
|
56
56
|
"test:orms": "node examples/orms/run-all.js",
|
|
57
57
|
"test:dml": "node test/dml-constraints.test.js",
|
|
@@ -61,7 +61,9 @@
|
|
|
61
61
|
"test:savepoint": "node test/savepoint.test.js",
|
|
62
62
|
"test:alter": "node test/alter-table.test.js",
|
|
63
63
|
"test:ident": "node test/identifier-keywords.test.js",
|
|
64
|
-
"test:null": "node test/null-semantics.test.js"
|
|
64
|
+
"test:null": "node test/null-semantics.test.js",
|
|
65
|
+
"test:semantics": "node test/sql-semantics.test.js",
|
|
66
|
+
"test:filter": "node test/filter-compile.test.js"
|
|
65
67
|
},
|
|
66
68
|
"keywords": [
|
|
67
69
|
"database",
|
package/test/alter-table.test.js
CHANGED
|
@@ -83,6 +83,25 @@ async function t(name, fn) {
|
|
|
83
83
|
return eq((await db.getTables()).slice().sort(), ['t']);
|
|
84
84
|
});
|
|
85
85
|
|
|
86
|
+
// renameTable 后来被改成 async(它要 await _runHooks)。执行器这边一度
|
|
87
|
+
// 忘了 await —— 冲突时报错变成未处理的 Promise 拒绝:SQL 照常返回 ok:true,
|
|
88
|
+
// 调用方以为改名成功,随后进程被 rejection 打崩。
|
|
89
|
+
await t('改名冲突:报错且不产生未处理的 Promise 拒绝', async ({ db, exec, mustFail }) => {
|
|
90
|
+
let unhandled = null;
|
|
91
|
+
const onUnhandled = (e) => { unhandled = e; };
|
|
92
|
+
process.on('unhandledRejection', onUnhandled);
|
|
93
|
+
try {
|
|
94
|
+
await exec('CREATE TABLE a (id INT PRIMARY KEY)');
|
|
95
|
+
await exec('CREATE TABLE b (id INT PRIMARY KEY)');
|
|
96
|
+
const bad = await mustFail('ALTER TABLE a RENAME TO b');
|
|
97
|
+
// 让微任务队列跑完,未处理的 rejection 会在此暴露
|
|
98
|
+
await new Promise(r => setImmediate(r));
|
|
99
|
+
return bad && unhandled === null && eq((await db.getTables()).slice().sort(), ['a', 'b']);
|
|
100
|
+
} finally {
|
|
101
|
+
process.removeListener('unhandledRejection', onUnhandled);
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
|
|
86
105
|
await t('改名后主键/自增/唯一约束仍然生效', async ({ exec, rows, mustFail }) => {
|
|
87
106
|
await exec('CREATE TABLE t (id INTEGER PRIMARY KEY AUTOINCREMENT, e TEXT UNIQUE)');
|
|
88
107
|
await exec("INSERT INTO t (e) VALUES ('a')");
|
|
@@ -0,0 +1,618 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* extra-coverage.test.js — JSQL-neo 边界情况全覆盖测试
|
|
3
|
+
*
|
|
4
|
+
* 运行:node test/extra-coverage.test.js
|
|
5
|
+
*
|
|
6
|
+
* 覆盖五大类边界情况:
|
|
7
|
+
* 1. SQL 语义边界(NULL、除零、精度、Unicode、日期、BETWEEN)
|
|
8
|
+
* 2. 聚合函数边界(COUNT/SUM/AVG、GROUP BY、HAVING、嵌套)
|
|
9
|
+
* 3. 子查询和 JOIN(相关子查询、笛卡尔积、EXISTS、嵌套)
|
|
10
|
+
* 4. DDL 和表操作(ALTER、TRUNCATE、RENAME、约束)
|
|
11
|
+
* 5. 表达式和函数(CASE、COALESCE、字符串/数学函数、嵌套)
|
|
12
|
+
*/
|
|
13
|
+
import path from 'path';
|
|
14
|
+
import { fileURLToPath } from 'url';
|
|
15
|
+
import assert from 'assert';
|
|
16
|
+
import { createRequire } from 'module';
|
|
17
|
+
|
|
18
|
+
/*
|
|
19
|
+
* 原先这里是 `import { test } from 'vitest'`。但 vitest 既不在 package.json 的
|
|
20
|
+
* 依赖里、也没安装,而这个文件又不在 test:all 里 —— 结果是这 68 个用例
|
|
21
|
+
* **从来没跑过一次**,纯摆设。死测试比没有测试更糟:它给人虚假的信心。
|
|
22
|
+
*
|
|
23
|
+
* 好在断言本来就全用 Node 内置的 assert,需要替换的只有 test 这一层。
|
|
24
|
+
* 下面这个极简 runner 行为对齐 vitest:顺序执行、逐个报结果、
|
|
25
|
+
* 有失败就以非零码退出。
|
|
26
|
+
*/
|
|
27
|
+
const _tests = [];
|
|
28
|
+
const test = (name, fn) => { _tests.push({ name, fn }); };
|
|
29
|
+
|
|
30
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
31
|
+
const __dirname = path.dirname(__filename);
|
|
32
|
+
const require = createRequire(import.meta.url);
|
|
33
|
+
const ROOT = path.join(__dirname, '..');
|
|
34
|
+
const Database = require(path.join(ROOT, 'lib/database'));
|
|
35
|
+
const { executeSQL } = require(path.join(ROOT, 'lib/sql'));
|
|
36
|
+
|
|
37
|
+
const OPTS = { safety: false };
|
|
38
|
+
|
|
39
|
+
/** 创建内存数据库并返回查询辅助函数 */
|
|
40
|
+
function makeDb() {
|
|
41
|
+
const db = new Database(':memory:', { autoSave: false });
|
|
42
|
+
const q = (sql) => executeSQL(db, sql, OPTS);
|
|
43
|
+
return { db, q };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** 取第一行第一列 */
|
|
47
|
+
async function one(q, sql) {
|
|
48
|
+
const r = await q(sql);
|
|
49
|
+
return r.rows[0][0];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** 取所有行 */
|
|
53
|
+
async function rows(q, sql) {
|
|
54
|
+
const r = await q(sql);
|
|
55
|
+
return r.rows;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// ────────────────────────────────────────────────────────────────
|
|
59
|
+
// 1. SQL 语义边界(10+ tests)
|
|
60
|
+
// ────────────────────────────────────────────────────────────────
|
|
61
|
+
|
|
62
|
+
test('NULL = NULL 返回 NULL(UNKNOWN,三值逻辑)', async () => {
|
|
63
|
+
const { q } = makeDb();
|
|
64
|
+
assert.strictEqual(await one(q, 'SELECT NULL = NULL'), null);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test('NULL != 1 返回 NULL(UNKNOWN)', async () => {
|
|
68
|
+
const { q } = makeDb();
|
|
69
|
+
assert.strictEqual(await one(q, 'SELECT NULL != 1'), null);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test('NULL IN (1,2) 返回 NULL(UNKNOWN)', async () => {
|
|
73
|
+
const { q } = makeDb();
|
|
74
|
+
assert.strictEqual(await one(q, 'SELECT NULL IN (1,2)'), null);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test('NULL NOT IN (1,2) 返回 NULL(UNKNOWN)', async () => {
|
|
78
|
+
const { q } = makeDb();
|
|
79
|
+
assert.strictEqual(await one(q, 'SELECT NULL NOT IN (1,2)'), null);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test('1 IN (1,NULL) 返回 TRUE(有一个元素匹配就成立)', async () => {
|
|
83
|
+
const { q } = makeDb();
|
|
84
|
+
// 等价于 `1=1 OR 1=NULL` —— TRUE OR UNKNOWN 还是 TRUE。
|
|
85
|
+
// 列表里别的位置有没有 NULL 不影响;只有**没匹配上**时才是 UNKNOWN。
|
|
86
|
+
// (原先这里断言的是 null,即「列表含 NULL 就 UNKNOWN」——那是个常见误解,
|
|
87
|
+
// MySQL / PostgreSQL 实测都返回 TRUE。)
|
|
88
|
+
assert.strictEqual(await one(q, 'SELECT 1 IN (1,NULL)'), 1);
|
|
89
|
+
assert.strictEqual(await one(q, 'SELECT 1 IN (NULL,1)'), 1);
|
|
90
|
+
assert.strictEqual(await one(q, 'SELECT 99 IN (1,NULL)'), null);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test('3 NOT IN (1,2) 返回 1(正常匹配)', async () => {
|
|
94
|
+
const { q } = makeDb();
|
|
95
|
+
assert.strictEqual(await one(q, 'SELECT 3 NOT IN (1,2)'), 1);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test('NULL <=> NULL 返回 1(NULL 安全相等)', async () => {
|
|
99
|
+
const { q } = makeDb();
|
|
100
|
+
assert.strictEqual(await one(q, 'SELECT NULL <=> NULL'), 1);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test('1 <=> NULL 返回 0(NULL 安全相等,不匹配)', async () => {
|
|
104
|
+
const { q } = makeDb();
|
|
105
|
+
assert.strictEqual(await one(q, 'SELECT 1 <=> NULL'), 0);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test("空字符串 '' = '' 返回 1", async () => {
|
|
109
|
+
const { q } = makeDb();
|
|
110
|
+
assert.strictEqual(await one(q, "SELECT '' = ''"), 1);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test("空字符串 '' = NULL 返回 NULL(空串不是 NULL)", async () => {
|
|
114
|
+
const { q } = makeDb();
|
|
115
|
+
assert.strictEqual(await one(q, "SELECT '' = NULL"), null);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test('1/0 返回 NULL(除零不抛错)', async () => {
|
|
119
|
+
const { q } = makeDb();
|
|
120
|
+
assert.strictEqual(await one(q, 'SELECT 1/0'), null);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test('10/3 返回浮点数 3.333...', async () => {
|
|
124
|
+
const { q } = makeDb();
|
|
125
|
+
const v = await one(q, 'SELECT 10/3');
|
|
126
|
+
assert.strictEqual(typeof v, 'number');
|
|
127
|
+
assert.ok(Math.abs(v - 3.3333333333333335) < 1e-15);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test('0.1+0.2 暴露 JS 浮点精度问题(0.3000...04)', async () => {
|
|
131
|
+
const { q } = makeDb();
|
|
132
|
+
const v = await one(q, 'SELECT 0.1+0.2');
|
|
133
|
+
assert.strictEqual(v, 0.1 + 0.2);
|
|
134
|
+
assert.notStrictEqual(v, 0.3);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test('大整数超出 JS 安全范围会抛错或返回非精确值', async () => {
|
|
138
|
+
const { q } = makeDb();
|
|
139
|
+
let result, error = null;
|
|
140
|
+
try {
|
|
141
|
+
result = await one(q, 'SELECT 9999999999999999');
|
|
142
|
+
} catch (e) {
|
|
143
|
+
error = e;
|
|
144
|
+
}
|
|
145
|
+
// 引擎内部用 BigInt,序列化会抛错;若未抛错则值不应精确
|
|
146
|
+
if (error) return;
|
|
147
|
+
assert.notStrictEqual(result, 9999999999999999);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
test('-0.0 归一为 0(负零消失)', async () => {
|
|
151
|
+
const { q } = makeDb();
|
|
152
|
+
const v = await one(q, 'SELECT -0.0');
|
|
153
|
+
assert.ok(v === 0);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
test('超长字符串 100000 字符正常处理', async () => {
|
|
157
|
+
const { q } = makeDb();
|
|
158
|
+
const long = 'x'.repeat(100000);
|
|
159
|
+
assert.strictEqual(await one(q, `SELECT LENGTH('${long}')`), 100000);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
test('Unicode é 正确处理(LENGTH=5, UPPER 含重音)', async () => {
|
|
163
|
+
const { q } = makeDb();
|
|
164
|
+
assert.strictEqual(await one(q, "SELECT LENGTH('héllo')"), 5);
|
|
165
|
+
assert.strictEqual(await one(q, "SELECT UPPER('café')"), 'CAFÉ');
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
test('Emoji 按 UTF-16 单元计数(LENGTH=4)', async () => {
|
|
169
|
+
const { q } = makeDb();
|
|
170
|
+
assert.strictEqual(await one(q, "SELECT LENGTH('🚀🌟')"), 4);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
test('Emoji UPPER 无变化', async () => {
|
|
174
|
+
const { q } = makeDb();
|
|
175
|
+
assert.strictEqual(await one(q, "SELECT UPPER('🚀')"), '🚀');
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
test("DATE('0000-00-00') 返回 NULL(无效日期)", async () => {
|
|
179
|
+
const { q } = makeDb();
|
|
180
|
+
assert.strictEqual(await one(q, "SELECT DATE('0000-00-00')"), null);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
test("DATE('9999-12-31') 返回 '9999-12-31'", async () => {
|
|
184
|
+
const { q } = makeDb();
|
|
185
|
+
assert.strictEqual(await one(q, "SELECT DATE('9999-12-31')"), '9999-12-31');
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
test("DATE('2024-02-29') 闰年正确", async () => {
|
|
189
|
+
const { q } = makeDb();
|
|
190
|
+
assert.strictEqual(await one(q, "SELECT DATE('2024-02-29')"), '2024-02-29');
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
test("DATE('2026-02-29') 非法日期溢出修正为 '2026-03-01'", async () => {
|
|
194
|
+
const { q } = makeDb();
|
|
195
|
+
assert.strictEqual(await one(q, "SELECT DATE('2026-02-29')"), '2026-03-01');
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
test("YEAR('9999-12-31') 返回 9999", async () => {
|
|
199
|
+
const { q } = makeDb();
|
|
200
|
+
assert.strictEqual(await one(q, "SELECT YEAR('9999-12-31')"), 9999);
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
test('BETWEEN 包含边界(5 BETWEEN 1 AND 5 → 1)', async () => {
|
|
204
|
+
const { q } = makeDb();
|
|
205
|
+
assert.strictEqual(await one(q, 'SELECT 5 BETWEEN 1 AND 5'), 1);
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
test('BETWEEN NULL 返回 NULL(UNKNOWN)', async () => {
|
|
209
|
+
const { q } = makeDb();
|
|
210
|
+
assert.strictEqual(await one(q, 'SELECT NULL BETWEEN 1 AND 5'), null);
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
test('NOT BETWEEN NULL 返回 NULL(UNKNOWN)', async () => {
|
|
214
|
+
const { q } = makeDb();
|
|
215
|
+
assert.strictEqual(await one(q, 'SELECT NULL NOT BETWEEN 1 AND 5'), null);
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
// ────────────────────────────────────────────────────────────────
|
|
219
|
+
// 2. 聚合函数边界(8+ tests)
|
|
220
|
+
// ────────────────────────────────────────────────────────────────
|
|
221
|
+
|
|
222
|
+
test('COUNT(*) 计所有行(含 NULL)', async () => {
|
|
223
|
+
const { q } = makeDb();
|
|
224
|
+
await q('CREATE TABLE t (id INT PRIMARY KEY, v INT)');
|
|
225
|
+
await q('INSERT INTO t VALUES (1,10),(2,NULL),(3,30)');
|
|
226
|
+
assert.strictEqual(await one(q, 'SELECT COUNT(*) FROM t'), 3);
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
test('COUNT(col) 跳过 NULL', async () => {
|
|
230
|
+
const { q } = makeDb();
|
|
231
|
+
await q('CREATE TABLE t (id INT PRIMARY KEY, v INT)');
|
|
232
|
+
await q('INSERT INTO t VALUES (1,10),(2,NULL),(3,30)');
|
|
233
|
+
assert.strictEqual(await one(q, 'SELECT COUNT(v) FROM t'), 2);
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
test('COUNT(DISTINCT col) 去重且跳过 NULL', async () => {
|
|
237
|
+
const { q } = makeDb();
|
|
238
|
+
await q('CREATE TABLE t (id INT PRIMARY KEY, s STRING)');
|
|
239
|
+
await q("INSERT INTO t VALUES (1,'a'),(2,'a'),(3,NULL),(4,'b')");
|
|
240
|
+
assert.strictEqual(await one(q, 'SELECT COUNT(DISTINCT s) FROM t'), 2);
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
test('SUM 跳过 NULL(不全为 NULL 时)', async () => {
|
|
244
|
+
const { q } = makeDb();
|
|
245
|
+
await q('CREATE TABLE t (id INT PRIMARY KEY, v INT)');
|
|
246
|
+
await q('INSERT INTO t VALUES (1,10),(2,NULL),(3,30)');
|
|
247
|
+
assert.strictEqual(await one(q, 'SELECT SUM(v) FROM t'), 40);
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
test('SUM 全 NULL 返回 NULL', async () => {
|
|
251
|
+
const { q } = makeDb();
|
|
252
|
+
await q('CREATE TABLE t (id INT PRIMARY KEY, v INT)');
|
|
253
|
+
await q('INSERT INTO t VALUES (1,NULL),(2,NULL)');
|
|
254
|
+
assert.strictEqual(await one(q, 'SELECT SUM(v) FROM t'), null);
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
test('AVG 跳过 NULL(分母不含 NULL)', async () => {
|
|
258
|
+
const { q } = makeDb();
|
|
259
|
+
await q('CREATE TABLE t (id INT PRIMARY KEY, v INT)');
|
|
260
|
+
await q('INSERT INTO t VALUES (1,10),(2,NULL),(3,30)');
|
|
261
|
+
assert.strictEqual(await one(q, 'SELECT AVG(v) FROM t'), 20);
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
test('GROUP BY 多列正确分组', async () => {
|
|
265
|
+
const { q } = makeDb();
|
|
266
|
+
await q('CREATE TABLE t (id INT PRIMARY KEY, g INT, s STRING)');
|
|
267
|
+
await q("INSERT INTO t VALUES (1,1,'a'),(2,1,'a'),(3,2,'a'),(4,2,'b')");
|
|
268
|
+
const r = await rows(q, 'SELECT g, s, COUNT(*) AS cnt FROM t GROUP BY g, s ORDER BY g, s');
|
|
269
|
+
assert.strictEqual(r.length, 3);
|
|
270
|
+
assert.deepStrictEqual(r[0], [1, 'a', 2]);
|
|
271
|
+
assert.deepStrictEqual(r[1], [2, 'a', 1]);
|
|
272
|
+
assert.deepStrictEqual(r[2], [2, 'b', 1]);
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
test('HAVING 无 GROUP BY 时对整表聚合结果过滤', async () => {
|
|
276
|
+
const { q } = makeDb();
|
|
277
|
+
await q('CREATE TABLE t (id INT PRIMARY KEY, v INT)');
|
|
278
|
+
await q('INSERT INTO t VALUES (1,10),(2,20),(3,30)');
|
|
279
|
+
assert.strictEqual(await one(q, 'SELECT COUNT(*) FROM t HAVING COUNT(*) > 1'), 3);
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
test('聚合嵌套:SELECT COUNT(*) FROM (SELECT ...) t', async () => {
|
|
283
|
+
const { q } = makeDb();
|
|
284
|
+
await q('CREATE TABLE t (id INT PRIMARY KEY, g INT)');
|
|
285
|
+
await q('INSERT INTO t VALUES (1,1),(2,1),(3,2),(4,2),(5,NULL)');
|
|
286
|
+
assert.strictEqual(
|
|
287
|
+
await one(q, 'SELECT COUNT(*) FROM (SELECT DISTINCT g FROM t) x'),
|
|
288
|
+
3
|
|
289
|
+
);
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
test('混合聚合和非聚合列(GROUP BY 覆盖非聚合列)', async () => {
|
|
293
|
+
const { q } = makeDb();
|
|
294
|
+
await q('CREATE TABLE t (id INT PRIMARY KEY, g INT, v INT)');
|
|
295
|
+
await q('INSERT INTO t VALUES (1,1,10),(2,1,20),(3,2,30)');
|
|
296
|
+
const r = await rows(q, 'SELECT g, v, COUNT(*) AS cnt FROM t GROUP BY g, v ORDER BY g, v');
|
|
297
|
+
assert.strictEqual(r.length, 3);
|
|
298
|
+
assert.deepStrictEqual(r[0], [1, 10, 1]);
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
// ────────────────────────────────────────────────────────────────
|
|
302
|
+
// 3. 子查询和 JOIN(8+ tests)
|
|
303
|
+
// ────────────────────────────────────────────────────────────────
|
|
304
|
+
|
|
305
|
+
test('相关子查询:每行独立求值', async () => {
|
|
306
|
+
const { q } = makeDb();
|
|
307
|
+
await q('CREATE TABLE t (id INT PRIMARY KEY, g INT, v INT)');
|
|
308
|
+
await q('INSERT INTO t VALUES (1,1,100),(2,1,5),(3,2,50),(4,2,200)');
|
|
309
|
+
// g=1 的 AVG=52.5,g=2 的 AVG=125
|
|
310
|
+
// g=1: 100>52.5 ✓, 5>52.5 ✗; g=2: 50>125 ✗, 200>125 ✓ → 共 2 行
|
|
311
|
+
const r = await rows(q, 'SELECT COUNT(*) FROM t a WHERE v > (SELECT AVG(v) FROM t b WHERE b.g = a.g)');
|
|
312
|
+
assert.strictEqual(r[0][0], 2);
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
test('子查询返回多列', async () => {
|
|
316
|
+
const { q } = makeDb();
|
|
317
|
+
await q('CREATE TABLE t (id INT PRIMARY KEY, g INT, v INT)');
|
|
318
|
+
await q('INSERT INTO t VALUES (1,1,10),(2,2,20),(3,3,30)');
|
|
319
|
+
const r = await rows(q, 'SELECT * FROM (SELECT g, v FROM t WHERE v >= 20) x ORDER BY g');
|
|
320
|
+
assert.strictEqual(r.length, 2);
|
|
321
|
+
assert.deepStrictEqual(r[0], [2, 20]);
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
test('JOIN 无 ON 条件产生笛卡尔积', async () => {
|
|
325
|
+
const { q } = makeDb();
|
|
326
|
+
await q('CREATE TABLE a (id INT PRIMARY KEY)');
|
|
327
|
+
await q('CREATE TABLE b (id INT PRIMARY KEY)');
|
|
328
|
+
await q('INSERT INTO a VALUES (1),(2)');
|
|
329
|
+
await q('INSERT INTO b VALUES (1),(2),(3)');
|
|
330
|
+
assert.strictEqual(await one(q, 'SELECT COUNT(*) FROM a JOIN b'), 6);
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
test('逗号连接产生笛卡尔积', async () => {
|
|
334
|
+
const { q } = makeDb();
|
|
335
|
+
await q('CREATE TABLE a (id INT PRIMARY KEY)');
|
|
336
|
+
await q('CREATE TABLE b (id INT PRIMARY KEY)');
|
|
337
|
+
await q('INSERT INTO a VALUES (1),(2)');
|
|
338
|
+
await q('INSERT INTO b VALUES (1),(2),(3)');
|
|
339
|
+
assert.strictEqual(await one(q, 'SELECT COUNT(*) FROM a, b'), 6);
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
test('LEFT JOIN 右表无匹配时填 NULL', async () => {
|
|
343
|
+
const { q } = makeDb();
|
|
344
|
+
await q('CREATE TABLE a (id INT PRIMARY KEY, name STRING)');
|
|
345
|
+
await q('CREATE TABLE b (id INT PRIMARY KEY, a_id INT, val STRING)');
|
|
346
|
+
await q("INSERT INTO a VALUES (1,'x'),(2,'y'),(3,'z')");
|
|
347
|
+
await q("INSERT INTO b VALUES (10,2,'matched')");
|
|
348
|
+
const r = await rows(q, 'SELECT a.id, a.name, b.val FROM a LEFT JOIN b ON a.id = b.a_id ORDER BY a.id');
|
|
349
|
+
assert.strictEqual(r.length, 3);
|
|
350
|
+
assert.deepStrictEqual(r[0], [1, 'x', null]);
|
|
351
|
+
assert.deepStrictEqual(r[2], [3, 'z', null]);
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
test('子查询在 SELECT 列表(标量子查询)', async () => {
|
|
355
|
+
const { q } = makeDb();
|
|
356
|
+
await q('CREATE TABLE t (id INT PRIMARY KEY, v INT)');
|
|
357
|
+
await q('INSERT INTO t VALUES (1,10),(2,20),(3,30)');
|
|
358
|
+
assert.strictEqual(await one(q, 'SELECT MAX(v) FROM t'), 30);
|
|
359
|
+
const r = await one(q, 'SELECT (SELECT MAX(v) FROM t) FROM t LIMIT 1');
|
|
360
|
+
assert.strictEqual(r, 30);
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
test('嵌套子查询 3 层', async () => {
|
|
364
|
+
const { q } = makeDb();
|
|
365
|
+
await q('CREATE TABLE t (id INT PRIMARY KEY, g INT)');
|
|
366
|
+
await q('INSERT INTO t VALUES (1,1),(2,1),(3,2),(4,3)');
|
|
367
|
+
assert.strictEqual(
|
|
368
|
+
await one(q, 'SELECT COUNT(*) FROM (SELECT g FROM (SELECT DISTINCT g FROM t) a) b'),
|
|
369
|
+
3
|
|
370
|
+
);
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
test('IN 子查询', async () => {
|
|
374
|
+
const { q } = makeDb();
|
|
375
|
+
await q('CREATE TABLE t (id INT PRIMARY KEY, g INT, v INT)');
|
|
376
|
+
await q('INSERT INTO t VALUES (1,1,10),(2,1,20),(3,2,30)');
|
|
377
|
+
assert.strictEqual(
|
|
378
|
+
await one(q, 'SELECT COUNT(*) FROM t WHERE g IN (SELECT DISTINCT g FROM t WHERE g IS NOT NULL)'),
|
|
379
|
+
3
|
|
380
|
+
);
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
test('IN 子查询含 NULL:匹配上的行仍应返回', async () => {
|
|
384
|
+
const { q } = makeDb();
|
|
385
|
+
await q('CREATE TABLE t (id INT PRIMARY KEY, g INT)');
|
|
386
|
+
await q('INSERT INTO t VALUES (1,1),(2,NULL)');
|
|
387
|
+
// 子查询结果是 {1, NULL}。g=1 那行匹配得上(1 IN (1,NULL) 为 TRUE),
|
|
388
|
+
// 只有 g=NULL 那行是 UNKNOWN 被过滤 —— 所以是 1 行,不是 0 行。
|
|
389
|
+
// (原先断言 0 行,把 bug 当成了预期。)
|
|
390
|
+
assert.strictEqual(
|
|
391
|
+
await one(q, 'SELECT COUNT(*) FROM t WHERE g IN (SELECT g FROM t)'),
|
|
392
|
+
1
|
|
393
|
+
);
|
|
394
|
+
// 反过来:NOT IN 的列表含 NULL 时恒为 UNKNOWN,一行都不返回
|
|
395
|
+
assert.strictEqual(
|
|
396
|
+
await one(q, 'SELECT COUNT(*) FROM t WHERE g NOT IN (SELECT g FROM t)'),
|
|
397
|
+
0
|
|
398
|
+
);
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
test('EXISTS 子查询', async () => {
|
|
402
|
+
const { q } = makeDb();
|
|
403
|
+
await q('CREATE TABLE t (id INT PRIMARY KEY, g INT)');
|
|
404
|
+
await q('INSERT INTO t VALUES (1,1),(2,2),(3,1)');
|
|
405
|
+
assert.strictEqual(
|
|
406
|
+
await one(q, 'SELECT COUNT(*) FROM t WHERE EXISTS (SELECT 1 FROM t WHERE t.g = g)'),
|
|
407
|
+
3
|
|
408
|
+
);
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
test('NOT EXISTS 子查询', async () => {
|
|
412
|
+
const { q } = makeDb();
|
|
413
|
+
await q('CREATE TABLE t (id INT PRIMARY KEY, g INT)');
|
|
414
|
+
await q('INSERT INTO t VALUES (1,1),(2,2),(3,1)');
|
|
415
|
+
// g=2 没有其他行同组,NOT EXISTS 为真
|
|
416
|
+
assert.strictEqual(
|
|
417
|
+
await one(q, 'SELECT COUNT(*) FROM t a WHERE NOT EXISTS (SELECT 1 FROM t b WHERE b.g = a.g AND b.id != a.id)'),
|
|
418
|
+
1
|
|
419
|
+
);
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
// ────────────────────────────────────────────────────────────────
|
|
423
|
+
// 4. DDL 和表操作(6+ tests)
|
|
424
|
+
// ────────────────────────────────────────────────────────────────
|
|
425
|
+
|
|
426
|
+
test('CREATE TABLE 带 DEFAULT 值对新行生效', async () => {
|
|
427
|
+
const { q } = makeDb();
|
|
428
|
+
await q("CREATE TABLE t (id INT PRIMARY KEY, n STRING, extra STRING DEFAULT 'x')");
|
|
429
|
+
await q("INSERT INTO t (id, n) VALUES (1,'a')");
|
|
430
|
+
const r = await rows(q, 'SELECT extra FROM t');
|
|
431
|
+
assert.strictEqual(r[0][0], 'x');
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
test('ALTER TABLE ADD COLUMN 带 DEFAULT 子句(DEFAULT 对新行不生效,已知限制)', async () => {
|
|
435
|
+
const { q } = makeDb();
|
|
436
|
+
await q('CREATE TABLE t (id INT PRIMARY KEY, n STRING)');
|
|
437
|
+
await q("INSERT INTO t VALUES (1,'a')");
|
|
438
|
+
await q("ALTER TABLE t ADD COLUMN extra STRING DEFAULT 'x'");
|
|
439
|
+
await q("INSERT INTO t (id, n) VALUES (2,'b')");
|
|
440
|
+
const r = await rows(q, 'SELECT id, n, extra FROM t ORDER BY id');
|
|
441
|
+
// 已知限制:ALTER TABLE ADD COLUMN 的 DEFAULT 不应用到新行
|
|
442
|
+
assert.ok(r[1][2] == null); // null 或 undefined(序列化差异)
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
test('ALTER TABLE DROP COLUMN', async () => {
|
|
446
|
+
const { q } = makeDb();
|
|
447
|
+
await q('CREATE TABLE t (id INT PRIMARY KEY, a STRING, b STRING)');
|
|
448
|
+
await q("INSERT INTO t VALUES (1,'x','y')");
|
|
449
|
+
await q('ALTER TABLE t DROP COLUMN b');
|
|
450
|
+
const r = await rows(q, 'SELECT * FROM t');
|
|
451
|
+
assert.deepStrictEqual(r[0], [1, 'x']);
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
test('CREATE TABLE 带各种约束(PK、UNIQUE、NOT NULL、DEFAULT)', async () => {
|
|
455
|
+
const { q } = makeDb();
|
|
456
|
+
await q("CREATE TABLE cons (id INT PRIMARY KEY, u STRING UNIQUE, nn INT NOT NULL, d INT DEFAULT 99)");
|
|
457
|
+
// 合法插入
|
|
458
|
+
await q("INSERT INTO cons (id, u, nn) VALUES (1, 'a', 1)");
|
|
459
|
+
// DEFAULT 生效
|
|
460
|
+
const r = await rows(q, 'SELECT d FROM cons WHERE id=1');
|
|
461
|
+
assert.strictEqual(r[0][0], 99);
|
|
462
|
+
// 违反 PK
|
|
463
|
+
await assert.rejects(() => q("INSERT INTO cons (id, u, nn) VALUES (1, 'c', 3)"));
|
|
464
|
+
// 违反 UNIQUE
|
|
465
|
+
await assert.rejects(() => q("INSERT INTO cons (id, u, nn) VALUES (2, 'a', 4)"));
|
|
466
|
+
// 违反 NOT NULL
|
|
467
|
+
await assert.rejects(() => q("INSERT INTO cons (id, u, nn) VALUES (3, 'b', NULL)"));
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
test('TRUNCATE 后自增重置', async () => {
|
|
471
|
+
const { q } = makeDb();
|
|
472
|
+
await q('CREATE TABLE t (id INT PRIMARY KEY AUTO_INCREMENT, n STRING)');
|
|
473
|
+
await q("INSERT INTO t (n) VALUES ('a'),('b'),('c')");
|
|
474
|
+
await q('TRUNCATE TABLE t');
|
|
475
|
+
assert.strictEqual(await one(q, 'SELECT COUNT(*) FROM t'), 0);
|
|
476
|
+
await q("INSERT INTO t (n) VALUES ('z')");
|
|
477
|
+
const r = await rows(q, 'SELECT id FROM t');
|
|
478
|
+
assert.strictEqual(r[0][0], 1); // 自增重置
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
test('RENAME TABLE 后旧名不可用', async () => {
|
|
482
|
+
const { q } = makeDb();
|
|
483
|
+
await q('CREATE TABLE old (id INT PRIMARY KEY)');
|
|
484
|
+
await q('ALTER TABLE old RENAME TO new');
|
|
485
|
+
await q('INSERT INTO new VALUES (1)');
|
|
486
|
+
assert.strictEqual(await one(q, 'SELECT COUNT(*) FROM new'), 1);
|
|
487
|
+
await assert.rejects(() => q('SELECT COUNT(*) FROM old'));
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
test('创建重复表名报错', async () => {
|
|
491
|
+
const { q } = makeDb();
|
|
492
|
+
await q('CREATE TABLE dup (id INT PRIMARY KEY)');
|
|
493
|
+
await assert.rejects(() => q('CREATE TABLE dup (id INT PRIMARY KEY)'));
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
// ────────────────────────────────────────────────────────────────
|
|
497
|
+
// 5. 表达式和函数(6+ tests)
|
|
498
|
+
// ────────────────────────────────────────────────────────────────
|
|
499
|
+
|
|
500
|
+
test('CASE WHEN 搜索形式', async () => {
|
|
501
|
+
const { q } = makeDb();
|
|
502
|
+
assert.strictEqual(
|
|
503
|
+
await one(q, "SELECT CASE WHEN 1=1 THEN 'yes' ELSE 'no' END"),
|
|
504
|
+
'yes'
|
|
505
|
+
);
|
|
506
|
+
});
|
|
507
|
+
|
|
508
|
+
test('CASE 简单形式', async () => {
|
|
509
|
+
const { q } = makeDb();
|
|
510
|
+
assert.strictEqual(
|
|
511
|
+
await one(q, "SELECT CASE 2 WHEN 1 THEN 'a' WHEN 2 THEN 'b' ELSE 'c' END"),
|
|
512
|
+
'b'
|
|
513
|
+
);
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
test('CASE 无匹配且无 ELSE 返回 NULL', async () => {
|
|
517
|
+
const { q } = makeDb();
|
|
518
|
+
assert.strictEqual(
|
|
519
|
+
await one(q, "SELECT CASE WHEN 0=1 THEN 'yes' END"),
|
|
520
|
+
null
|
|
521
|
+
);
|
|
522
|
+
});
|
|
523
|
+
|
|
524
|
+
test('CASE 条件为 NULL 时走 ELSE', async () => {
|
|
525
|
+
const { q } = makeDb();
|
|
526
|
+
assert.strictEqual(
|
|
527
|
+
await one(q, "SELECT CASE NULL WHEN 1 THEN 'a' ELSE 'b' END"),
|
|
528
|
+
'b'
|
|
529
|
+
);
|
|
530
|
+
});
|
|
531
|
+
|
|
532
|
+
test('COALESCE / NULLIF / IFNULL', async () => {
|
|
533
|
+
const { q } = makeDb();
|
|
534
|
+
assert.strictEqual(await one(q, 'SELECT COALESCE(NULL, NULL, 3)'), 3);
|
|
535
|
+
assert.strictEqual(await one(q, 'SELECT NULLIF(1, 1)'), null);
|
|
536
|
+
assert.strictEqual(await one(q, 'SELECT NULLIF(1, 2)'), 1);
|
|
537
|
+
assert.strictEqual(await one(q, 'SELECT IFNULL(NULL, 9)'), 9);
|
|
538
|
+
});
|
|
539
|
+
|
|
540
|
+
test('字符串函数:LENGTH / UPPER / LOWER / TRIM / CONCAT / SUBSTR', async () => {
|
|
541
|
+
const { q } = makeDb();
|
|
542
|
+
assert.strictEqual(await one(q, "SELECT LENGTH('hello')"), 5);
|
|
543
|
+
assert.strictEqual(await one(q, "SELECT UPPER('hi')"), 'HI');
|
|
544
|
+
assert.strictEqual(await one(q, "SELECT LOWER('HI')"), 'hi');
|
|
545
|
+
assert.strictEqual(await one(q, "SELECT TRIM(' x ')"), 'x');
|
|
546
|
+
assert.strictEqual(await one(q, "SELECT CONCAT('a', 'b', 'c')"), 'abc');
|
|
547
|
+
assert.strictEqual(await one(q, "SELECT SUBSTR('hello', 2, 3)"), 'ell');
|
|
548
|
+
});
|
|
549
|
+
|
|
550
|
+
test('数学函数:ABS / CEIL / FLOOR / ROUND / POWER / SQRT', async () => {
|
|
551
|
+
const { q } = makeDb();
|
|
552
|
+
assert.strictEqual(await one(q, 'SELECT ABS(-5)'), 5);
|
|
553
|
+
assert.strictEqual(await one(q, 'SELECT CEIL(2.1)'), 3);
|
|
554
|
+
assert.strictEqual(await one(q, 'SELECT FLOOR(2.9)'), 2);
|
|
555
|
+
assert.strictEqual(await one(q, 'SELECT ROUND(2.5)'), 3);
|
|
556
|
+
assert.strictEqual(await one(q, 'SELECT ROUND(-2.5)'), -3); // 半远离零
|
|
557
|
+
assert.strictEqual(await one(q, 'SELECT POWER(2, 3)'), 8);
|
|
558
|
+
assert.strictEqual(await one(q, 'SELECT SQRT(16)'), 4);
|
|
559
|
+
});
|
|
560
|
+
|
|
561
|
+
test('SQRT 负数返回 NaN', async () => {
|
|
562
|
+
const { q } = makeDb();
|
|
563
|
+
const v = await one(q, 'SELECT SQRT(-1)');
|
|
564
|
+
assert.ok(Number.isNaN(v));
|
|
565
|
+
});
|
|
566
|
+
|
|
567
|
+
test('REPLACE 函数', async () => {
|
|
568
|
+
const { q } = makeDb();
|
|
569
|
+
assert.strictEqual(await one(q, "SELECT REPLACE('hello', 'l', 'r')"), 'herro');
|
|
570
|
+
});
|
|
571
|
+
|
|
572
|
+
test('嵌套函数调用', async () => {
|
|
573
|
+
const { q } = makeDb();
|
|
574
|
+
assert.strictEqual(await one(q, "SELECT UPPER(TRIM(' hi '))"), 'HI');
|
|
575
|
+
assert.strictEqual(await one(q, "SELECT LENGTH(CONCAT('a', 'b', 'c'))"), 3);
|
|
576
|
+
});
|
|
577
|
+
|
|
578
|
+
test('ABS 无参数返回 NULL', async () => {
|
|
579
|
+
const { q } = makeDb();
|
|
580
|
+
assert.strictEqual(await one(q, 'SELECT ABS()'), null);
|
|
581
|
+
});
|
|
582
|
+
|
|
583
|
+
test('LENGTH 多参数只取第一个', async () => {
|
|
584
|
+
const { q } = makeDb();
|
|
585
|
+
assert.strictEqual(await one(q, "SELECT LENGTH('ab', 'cd')"), 2);
|
|
586
|
+
});
|
|
587
|
+
|
|
588
|
+
test('IF 函数三值行为(NULL / 0 / 空串均为假)', async () => {
|
|
589
|
+
const { q } = makeDb();
|
|
590
|
+
assert.strictEqual(await one(q, "SELECT IF(NULL, 'a', 'b')"), 'b');
|
|
591
|
+
assert.strictEqual(await one(q, "SELECT IF(0, 'a', 'b')"), 'b');
|
|
592
|
+
assert.strictEqual(await one(q, "SELECT IF('', 'a', 'b')"), 'b');
|
|
593
|
+
assert.strictEqual(await one(q, "SELECT IF(1, 'a', 'b')"), 'a');
|
|
594
|
+
});
|
|
595
|
+
|
|
596
|
+
// ---- runner:顺序执行全部用例,按 vitest 的习惯输出与退出码 ----
|
|
597
|
+
;(async () => {
|
|
598
|
+
let pass = 0, fail = 0;
|
|
599
|
+
const failures = [];
|
|
600
|
+
for (const t of _tests) {
|
|
601
|
+
try {
|
|
602
|
+
await t.fn();
|
|
603
|
+
pass++;
|
|
604
|
+
console.log('[OK] ', t.name);
|
|
605
|
+
} catch (e) {
|
|
606
|
+
fail++;
|
|
607
|
+
failures.push(t.name);
|
|
608
|
+
const msg = e && e.message ? e.message : String(e);
|
|
609
|
+
console.log('[FAIL]', t.name, '->', msg.slice(0, 130));
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
console.log(`\n${pass} passed, ${fail} failed`);
|
|
613
|
+
if (failures.length) {
|
|
614
|
+
console.log('\n失败用例:');
|
|
615
|
+
for (const n of failures) console.log(' - ' + n);
|
|
616
|
+
}
|
|
617
|
+
process.exit(fail === 0 ? 0 : 1);
|
|
618
|
+
})();
|