jsql-neo 6.4.0 → 6.4.2
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 +11 -1
- package/lib/native_client.js +14 -2
- package/lib/sql.js +4 -1
- 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 +1 -1
- package/test/alter-table.test.js +19 -0
- package/test/extra-coverage.test.js +569 -0
package/README.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
> **One engine to rule them all** — a Rust-powered embedded database that speaks your language:
|
|
4
4
|
> MySQL, PostgreSQL, MongoDB, Redis, SQL, TypeScript, **and the browser**.
|
|
5
5
|
|
|
6
|
-
> **v6.4.
|
|
6
|
+
> **v6.4.2** — official release build · [github.com/vexify-org/JSQL-neo](https://github.com/vexify-org/JSQL-neo) · [jsql.vexify.top](https://jsql.vexify.top)
|
|
7
7
|
|
|
8
8
|

|
|
9
9
|

|
package/index.js
CHANGED
package/lib/database.js
CHANGED
|
@@ -666,7 +666,12 @@ class Database {
|
|
|
666
666
|
// 重命名也要能回放:否则崩溃恢复后表名会退回去,
|
|
667
667
|
// 而后续的 insert/update 记录全都打在新名字上,全部丢失。
|
|
668
668
|
if (op.from && op.to && this._tables[op.from] && !this._tables[op.to]) {
|
|
669
|
-
|
|
669
|
+
// renameTable 是 async 的(内部 await _runHooks),而本回放路径
|
|
670
|
+
// 是同步的,没法 await。**注意不能把 promise 直接 return** ——
|
|
671
|
+
// `if (promise)` 恒为真,失败会被当成成功。
|
|
672
|
+
// 这里同步发起并接住 rejection,避免未处理的 Promise 拒绝
|
|
673
|
+
// 把进程打崩;真失败了会体现在后续按新表名的记录上。
|
|
674
|
+
Promise.resolve(this.renameTable(op.from, op.to)).catch(() => {});
|
|
670
675
|
}
|
|
671
676
|
return true;
|
|
672
677
|
|
|
@@ -1292,6 +1297,11 @@ class Database {
|
|
|
1292
1297
|
this._tables[name] = table;
|
|
1293
1298
|
}
|
|
1294
1299
|
table._loadRows(rows);
|
|
1300
|
+
// B1-2: 恢复后必须重新标记脏,否则 rollback 后 _flushDirty 空转
|
|
1301
|
+
// —— 事务写已落盘(900/600),回滚恢复正确数据(1000/500)到内存,
|
|
1302
|
+
// 但若不标记脏,_flushDirty 检测 _dirtyTables.size===0 直接 return,
|
|
1303
|
+
// 正确数据永远写不进磁盘,重启后回滚被逆转(900 复活)。
|
|
1304
|
+
if (this._dirMode) this._dirtyTables.add(name);
|
|
1295
1305
|
// _loadRows 只会把计数器往上抬(取现有行的最大值),
|
|
1296
1306
|
// 所以「回滚掉刚插入的行」之后还得显式退回去。
|
|
1297
1307
|
if (entry.autoIncrement !== undefined) {
|
package/lib/native_client.js
CHANGED
|
@@ -490,7 +490,10 @@ class JSQL {
|
|
|
490
490
|
}
|
|
491
491
|
|
|
492
492
|
async _insertBatch(table, rows) {
|
|
493
|
+
// 直接传 JSON 字符串,让 Rust 处理 schema 映射和插入
|
|
494
|
+
// 消除 JS encodeBatch 二进制序列化和 Rust 解码的开销
|
|
493
495
|
const schema = this._schemas[table];
|
|
496
|
+
// 对 JSON/array/object 列保留 safeStringify 以给出可读错误(环形对象等)
|
|
494
497
|
if (schema) {
|
|
495
498
|
rows = rows.map(row => {
|
|
496
499
|
const out = {};
|
|
@@ -510,8 +513,17 @@ class JSQL {
|
|
|
510
513
|
return out;
|
|
511
514
|
});
|
|
512
515
|
}
|
|
513
|
-
const
|
|
514
|
-
|
|
516
|
+
const rawJson = JSON.stringify(rows);
|
|
517
|
+
// 优先用 jsqlInsertRaw(Rust 直接 JSON 解析,消除二进制序列化)
|
|
518
|
+
// 旧环境未编译 jsqlInsertRaw 时 fallback 到 jsqlInsertBuf
|
|
519
|
+
let r;
|
|
520
|
+
if (typeof native.jsqlInsertRaw === 'function') {
|
|
521
|
+
r = safeParse(this._n('jsqlInsertRaw', table, rawJson));
|
|
522
|
+
} else {
|
|
523
|
+
// fallback: 用 encodeBatch 二进制格式(向后兼容)
|
|
524
|
+
const bin = encodeBatch(rows);
|
|
525
|
+
r = safeParse(this._n('jsqlInsertBuf', table, bin));
|
|
526
|
+
}
|
|
515
527
|
if (r && r.error) throw new Error(r.error);
|
|
516
528
|
if (r && Array.isArray(r) && !this._pkAutoIncrement(table)) {
|
|
517
529
|
const pk = this._pkOf(table);
|
package/lib/sql.js
CHANGED
|
@@ -4579,7 +4579,10 @@ class SQLExecutor {
|
|
|
4579
4579
|
if (typeof this.engine.renameTable !== 'function') {
|
|
4580
4580
|
throw new Error(`ALTER TABLE ... RENAME is not supported by this engine (no renameTable method)`);
|
|
4581
4581
|
}
|
|
4582
|
-
|
|
4582
|
+
// renameTable 是 async 的(它要 await _runHooks),漏了 await 会让
|
|
4583
|
+
// 它抛出的错变成未处理的 Promise 拒绝:SQL 这边照常返回 ok:true,
|
|
4584
|
+
// 调用方以为改名成功,而进程随后被 rejection 打崩。
|
|
4585
|
+
await this.engine.renameTable(statement.name, op.newName);
|
|
4583
4586
|
table = this.engine._tables[op.newName] || table;
|
|
4584
4587
|
statement.name = op.newName;
|
|
4585
4588
|
break;
|
package/lib/wasm_client.js
CHANGED
|
@@ -481,6 +481,7 @@ class JSQL {
|
|
|
481
481
|
}
|
|
482
482
|
|
|
483
483
|
async _insertBatch(table, rows) {
|
|
484
|
+
// schema-aware 转换:JSON 列用 safeStringify 给出可读错误(环形对象等)
|
|
484
485
|
const schema = this._schemas[table];
|
|
485
486
|
if (schema) {
|
|
486
487
|
rows = rows.map(row => {
|
|
@@ -501,7 +502,8 @@ class JSQL {
|
|
|
501
502
|
return out;
|
|
502
503
|
});
|
|
503
504
|
}
|
|
504
|
-
const
|
|
505
|
+
const rawJson = JSON.stringify(rows);
|
|
506
|
+
const r = safeJsonParse(wasmBindings.jsql_insert_json(table, rawJson));
|
|
505
507
|
if (r && r.error) throw new Error(r.error);
|
|
506
508
|
if (r && Array.isArray(r) && !this._pkAutoIncrement(table)) {
|
|
507
509
|
const pk = this._pkOf(table);
|
|
Binary file
|
|
@@ -41,6 +41,15 @@ pub trait Engine {
|
|
|
41
41
|
fn list_tables(&self) -> Vec<String>;
|
|
42
42
|
|
|
43
43
|
fn insert(&mut self, table: &str, fields: Vec<HashMap<String, serde_json::Value>>) -> Result<Vec<u64>, String>;
|
|
44
|
+
/// 接收原始 JSON 数组字符串,直接解析并存入(消除 JS encodeBatch 二进制序列化开销)
|
|
45
|
+
fn insert_raw(&mut self, table: &str, raw_json: &str) -> Result<Vec<u64>, String> {
|
|
46
|
+
let rows: Vec<serde_json::Value> = serde_json::from_str(raw_json)
|
|
47
|
+
.map_err(|e| format!("invalid JSON array: {}", e))?;
|
|
48
|
+
let batch: Vec<HashMap<String, serde_json::Value>> = rows.into_iter()
|
|
49
|
+
.map(|v| v.as_object().map(|m| m.clone().into_iter().collect()).unwrap_or_else(HashMap::new))
|
|
50
|
+
.collect();
|
|
51
|
+
self.insert(table, batch)
|
|
52
|
+
}
|
|
44
53
|
fn find_by_id(&self, table: &str, id: u64) -> Result<Option<Row>, String>;
|
|
45
54
|
fn find_by_id_json(&self, table: &str, id: u64) -> Result<Option<String>, String>;
|
|
46
55
|
fn find_by_ids_json(&self, table: &str, ids: &[u64]) -> Result<String, String>;
|
|
@@ -274,6 +274,23 @@ fn do_insert_buf(eng: &mut HybridEngine, table: &str, buf_val: napi::JsBufferVal
|
|
|
274
274
|
safe_json_result(res)
|
|
275
275
|
}
|
|
276
276
|
|
|
277
|
+
#[napi]
|
|
278
|
+
pub fn jsql_insert_raw(table: String, raw_json: String) -> String {
|
|
279
|
+
with_engine_mut(|eng| do_insert_raw(eng, &table, &raw_json))
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
#[napi]
|
|
283
|
+
pub fn jsql_insert_raw_h(handle: u32, table: String, raw_json: String) -> String {
|
|
284
|
+
with_engine_handle_mut(handle, |eng| do_insert_raw(eng, &table, &raw_json))
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
fn do_insert_raw(eng: &mut HybridEngine, table: &str, raw_json: &str) -> String {
|
|
288
|
+
match eng.insert_raw(table, raw_json) {
|
|
289
|
+
Ok(ids) => serde_json::to_string(&ids).unwrap_or_else(|_| "[]".to_string()),
|
|
290
|
+
Err(e) => format!(r#"{{"error":"{}"}}"#, e.replace('"', r#"\""#)),
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
277
294
|
napi_engine_fn!(jsql_find, jsql_find_h,
|
|
278
295
|
(table: String, filter_json: String, limit: i32, offset: i32) => |eng| {
|
|
279
296
|
let filter: Option<HashMap<String, serde_json::Value>> =
|
|
@@ -76,96 +76,18 @@ pub fn jsql_insert(table: &str, data_json: &str) -> String {
|
|
|
76
76
|
#[wasm_bindgen]
|
|
77
77
|
pub fn jsql_insert_json(table: &str, json: &str) -> String {
|
|
78
78
|
ENGINE.with(|eng| {
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
let n = data.len();
|
|
86
|
-
if n == 0 { return Ok(Vec::new()); }
|
|
87
|
-
|
|
88
|
-
let first = data[0].as_object()
|
|
89
|
-
.ok_or_else(|| "expected array of objects".to_string())?;
|
|
90
|
-
let field_names: Vec<String> = first.keys().cloned().collect();
|
|
91
|
-
|
|
92
|
-
let remap: Vec<Option<usize>> = t.field_order.iter()
|
|
93
|
-
.map(|name| field_names.iter().position(|n| n == name))
|
|
94
|
-
.collect();
|
|
95
|
-
|
|
96
|
-
let pk_idx = t.pk_field.as_ref().and_then(|pk| {
|
|
97
|
-
t.schema.get(pk.as_str()).and_then(|fs| {
|
|
98
|
-
if fs.auto_increment { t.field_index.get(pk.as_str()).copied() } else { None }
|
|
99
|
-
})
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
let now = js_sys::Date::now() as u64;
|
|
103
|
-
let start_id = t.next_id;
|
|
104
|
-
t.next_id += n as u64;
|
|
105
|
-
t.pk_index.reserve(n);
|
|
106
|
-
t.rows.reserve(n);
|
|
107
|
-
t.values.reserve(t.field_order.len());
|
|
108
|
-
let start_ri = t.rows.len();
|
|
109
|
-
|
|
110
|
-
for (ri, row_val) in data.iter().enumerate() {
|
|
111
|
-
let auto_id = start_id + ri as u64;
|
|
112
|
-
let mut id = auto_id;
|
|
113
|
-
let obj = row_val.as_object()
|
|
114
|
-
.ok_or_else(|| "non-object in array".to_string())?;
|
|
115
|
-
|
|
116
|
-
let mut values = Vec::with_capacity(t.field_order.len());
|
|
117
|
-
for (i, rp) in remap.iter().enumerate() {
|
|
118
|
-
if let Some(fi) = rp {
|
|
119
|
-
let key = &field_names[*fi];
|
|
120
|
-
if let Some(jv) = obj.get(key) {
|
|
121
|
-
values.push(FieldValue::from_json(jv.clone()));
|
|
122
|
-
continue;
|
|
123
|
-
}
|
|
124
|
-
} else if let Some(pi) = pk_idx {
|
|
125
|
-
if pi == i {
|
|
126
|
-
values.push(FieldValue::Int(id as i64));
|
|
127
|
-
continue;
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
if let Some(name) = t.field_order.get(i) {
|
|
131
|
-
if let Some(fs) = t.schema.get(name) {
|
|
132
|
-
if let Some(ref default) = fs.default {
|
|
133
|
-
values.push(FieldValue::from_json(default.clone()));
|
|
134
|
-
continue;
|
|
135
|
-
}
|
|
136
|
-
if !fs.nullable {
|
|
137
|
-
values.push(default_value(&fs.field_type));
|
|
138
|
-
continue;
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
values.push(FieldValue::Null);
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
// auto-increment PK: keep explicit positive value, else fill with auto id
|
|
146
|
-
if let Some(pi) = pk_idx {
|
|
147
|
-
if let Some(v) = values.get_mut(pi) {
|
|
148
|
-
match v {
|
|
149
|
-
FieldValue::Null => *v = FieldValue::Int(id as i64),
|
|
150
|
-
FieldValue::Int(e) if *e > 0 => {
|
|
151
|
-
id = *e as u64;
|
|
152
|
-
t.next_id = t.next_id.max(id + 1);
|
|
153
|
-
}
|
|
154
|
-
_ => {}
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
t.pk_index.insert(id, start_ri + ri);
|
|
160
|
-
let vs = t.values.len();
|
|
161
|
-
t.values.extend(values);
|
|
162
|
-
t.rows.push(RowStore { id, values_start: vs, num_values: t.field_order.len(), created_at: now, updated_at: now });
|
|
163
|
-
}
|
|
79
|
+
match eng.borrow_mut().insert_raw(table, json) {
|
|
80
|
+
Ok(ids) => serde_json::to_string(&ids).unwrap_or_else(|_| "[]".to_string()),
|
|
81
|
+
Err(e) => format!(r#"{{"error":"{}"}}"#, e),
|
|
82
|
+
}
|
|
83
|
+
})
|
|
84
|
+
}
|
|
164
85
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
86
|
+
/// JSON 字符串直接插入(绕过 JS encodeBatch 二进制序列化)
|
|
87
|
+
#[wasm_bindgen]
|
|
88
|
+
pub fn jsql_insert_raw(table: &str, raw_json: &str) -> String {
|
|
89
|
+
ENGINE.with(|eng| {
|
|
90
|
+
match eng.borrow_mut().insert_raw(table, raw_json) {
|
|
169
91
|
Ok(ids) => serde_json::to_string(&ids).unwrap_or_else(|_| "[]".to_string()),
|
|
170
92
|
Err(e) => format!(r#"{{"error":"{}"}}"#, e),
|
|
171
93
|
}
|
package/package.json
CHANGED
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,569 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* extra-coverage.test.js — JSQL-neo 边界情况全覆盖测试
|
|
3
|
+
*
|
|
4
|
+
* 运行:npx vitest run 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 { test } from 'vitest';
|
|
16
|
+
import assert from 'assert';
|
|
17
|
+
import { createRequire } from 'module';
|
|
18
|
+
|
|
19
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
20
|
+
const __dirname = path.dirname(__filename);
|
|
21
|
+
const require = createRequire(import.meta.url);
|
|
22
|
+
const ROOT = path.join(__dirname, '..');
|
|
23
|
+
const Database = require(path.join(ROOT, 'lib/database'));
|
|
24
|
+
const { executeSQL } = require(path.join(ROOT, 'lib/sql'));
|
|
25
|
+
|
|
26
|
+
const OPTS = { safety: false };
|
|
27
|
+
|
|
28
|
+
/** 创建内存数据库并返回查询辅助函数 */
|
|
29
|
+
function makeDb() {
|
|
30
|
+
const db = new Database(':memory:', { autoSave: false });
|
|
31
|
+
const q = (sql) => executeSQL(db, sql, OPTS);
|
|
32
|
+
return { db, q };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** 取第一行第一列 */
|
|
36
|
+
async function one(q, sql) {
|
|
37
|
+
const r = await q(sql);
|
|
38
|
+
return r.rows[0][0];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** 取所有行 */
|
|
42
|
+
async function rows(q, sql) {
|
|
43
|
+
const r = await q(sql);
|
|
44
|
+
return r.rows;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// ────────────────────────────────────────────────────────────────
|
|
48
|
+
// 1. SQL 语义边界(10+ tests)
|
|
49
|
+
// ────────────────────────────────────────────────────────────────
|
|
50
|
+
|
|
51
|
+
test('NULL = NULL 返回 NULL(UNKNOWN,三值逻辑)', async () => {
|
|
52
|
+
const { q } = makeDb();
|
|
53
|
+
assert.strictEqual(await one(q, 'SELECT NULL = NULL'), null);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test('NULL != 1 返回 NULL(UNKNOWN)', async () => {
|
|
57
|
+
const { q } = makeDb();
|
|
58
|
+
assert.strictEqual(await one(q, 'SELECT NULL != 1'), null);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test('NULL IN (1,2) 返回 NULL(UNKNOWN)', async () => {
|
|
62
|
+
const { q } = makeDb();
|
|
63
|
+
assert.strictEqual(await one(q, 'SELECT NULL IN (1,2)'), null);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test('NULL NOT IN (1,2) 返回 NULL(UNKNOWN)', async () => {
|
|
67
|
+
const { q } = makeDb();
|
|
68
|
+
assert.strictEqual(await one(q, 'SELECT NULL NOT IN (1,2)'), null);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test('1 IN (1,NULL) 返回 NULL(列表含 NULL → UNKNOWN)', async () => {
|
|
72
|
+
const { q } = makeDb();
|
|
73
|
+
assert.strictEqual(await one(q, 'SELECT 1 IN (1,NULL)'), null);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test('3 NOT IN (1,2) 返回 1(正常匹配)', async () => {
|
|
77
|
+
const { q } = makeDb();
|
|
78
|
+
assert.strictEqual(await one(q, 'SELECT 3 NOT IN (1,2)'), 1);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test('NULL <=> NULL 返回 1(NULL 安全相等)', async () => {
|
|
82
|
+
const { q } = makeDb();
|
|
83
|
+
assert.strictEqual(await one(q, 'SELECT NULL <=> NULL'), 1);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test('1 <=> NULL 返回 0(NULL 安全相等,不匹配)', async () => {
|
|
87
|
+
const { q } = makeDb();
|
|
88
|
+
assert.strictEqual(await one(q, 'SELECT 1 <=> NULL'), 0);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test("空字符串 '' = '' 返回 1", async () => {
|
|
92
|
+
const { q } = makeDb();
|
|
93
|
+
assert.strictEqual(await one(q, "SELECT '' = ''"), 1);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test("空字符串 '' = NULL 返回 NULL(空串不是 NULL)", async () => {
|
|
97
|
+
const { q } = makeDb();
|
|
98
|
+
assert.strictEqual(await one(q, "SELECT '' = NULL"), null);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test('1/0 返回 NULL(除零不抛错)', async () => {
|
|
102
|
+
const { q } = makeDb();
|
|
103
|
+
assert.strictEqual(await one(q, 'SELECT 1/0'), null);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test('10/3 返回浮点数 3.333...', async () => {
|
|
107
|
+
const { q } = makeDb();
|
|
108
|
+
const v = await one(q, 'SELECT 10/3');
|
|
109
|
+
assert.strictEqual(typeof v, 'number');
|
|
110
|
+
assert.ok(Math.abs(v - 3.3333333333333335) < 1e-15);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test('0.1+0.2 暴露 JS 浮点精度问题(0.3000...04)', async () => {
|
|
114
|
+
const { q } = makeDb();
|
|
115
|
+
const v = await one(q, 'SELECT 0.1+0.2');
|
|
116
|
+
assert.strictEqual(v, 0.1 + 0.2);
|
|
117
|
+
assert.notStrictEqual(v, 0.3);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test('大整数超出 JS 安全范围会抛错或返回非精确值', async () => {
|
|
121
|
+
const { q } = makeDb();
|
|
122
|
+
let result, error = null;
|
|
123
|
+
try {
|
|
124
|
+
result = await one(q, 'SELECT 9999999999999999');
|
|
125
|
+
} catch (e) {
|
|
126
|
+
error = e;
|
|
127
|
+
}
|
|
128
|
+
// 引擎内部用 BigInt,序列化会抛错;若未抛错则值不应精确
|
|
129
|
+
if (error) return;
|
|
130
|
+
assert.notStrictEqual(result, 9999999999999999);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test('-0.0 归一为 0(负零消失)', async () => {
|
|
134
|
+
const { q } = makeDb();
|
|
135
|
+
const v = await one(q, 'SELECT -0.0');
|
|
136
|
+
assert.ok(v === 0);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
test('超长字符串 100000 字符正常处理', async () => {
|
|
140
|
+
const { q } = makeDb();
|
|
141
|
+
const long = 'x'.repeat(100000);
|
|
142
|
+
assert.strictEqual(await one(q, `SELECT LENGTH('${long}')`), 100000);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
test('Unicode é 正确处理(LENGTH=5, UPPER 含重音)', async () => {
|
|
146
|
+
const { q } = makeDb();
|
|
147
|
+
assert.strictEqual(await one(q, "SELECT LENGTH('héllo')"), 5);
|
|
148
|
+
assert.strictEqual(await one(q, "SELECT UPPER('café')"), 'CAFÉ');
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
test('Emoji 按 UTF-16 单元计数(LENGTH=4)', async () => {
|
|
152
|
+
const { q } = makeDb();
|
|
153
|
+
assert.strictEqual(await one(q, "SELECT LENGTH('🚀🌟')"), 4);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
test('Emoji UPPER 无变化', async () => {
|
|
157
|
+
const { q } = makeDb();
|
|
158
|
+
assert.strictEqual(await one(q, "SELECT UPPER('🚀')"), '🚀');
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
test("DATE('0000-00-00') 返回 NULL(无效日期)", async () => {
|
|
162
|
+
const { q } = makeDb();
|
|
163
|
+
assert.strictEqual(await one(q, "SELECT DATE('0000-00-00')"), null);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
test("DATE('9999-12-31') 返回 '9999-12-31'", async () => {
|
|
167
|
+
const { q } = makeDb();
|
|
168
|
+
assert.strictEqual(await one(q, "SELECT DATE('9999-12-31')"), '9999-12-31');
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
test("DATE('2024-02-29') 闰年正确", async () => {
|
|
172
|
+
const { q } = makeDb();
|
|
173
|
+
assert.strictEqual(await one(q, "SELECT DATE('2024-02-29')"), '2024-02-29');
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
test("DATE('2026-02-29') 非法日期溢出修正为 '2026-03-01'", async () => {
|
|
177
|
+
const { q } = makeDb();
|
|
178
|
+
assert.strictEqual(await one(q, "SELECT DATE('2026-02-29')"), '2026-03-01');
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
test("YEAR('9999-12-31') 返回 9999", async () => {
|
|
182
|
+
const { q } = makeDb();
|
|
183
|
+
assert.strictEqual(await one(q, "SELECT YEAR('9999-12-31')"), 9999);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
test('BETWEEN 包含边界(5 BETWEEN 1 AND 5 → 1)', async () => {
|
|
187
|
+
const { q } = makeDb();
|
|
188
|
+
assert.strictEqual(await one(q, 'SELECT 5 BETWEEN 1 AND 5'), 1);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
test('BETWEEN NULL 返回 NULL(UNKNOWN)', async () => {
|
|
192
|
+
const { q } = makeDb();
|
|
193
|
+
assert.strictEqual(await one(q, 'SELECT NULL BETWEEN 1 AND 5'), null);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
test('NOT BETWEEN NULL 返回 NULL(UNKNOWN)', async () => {
|
|
197
|
+
const { q } = makeDb();
|
|
198
|
+
assert.strictEqual(await one(q, 'SELECT NULL NOT BETWEEN 1 AND 5'), null);
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
// ────────────────────────────────────────────────────────────────
|
|
202
|
+
// 2. 聚合函数边界(8+ tests)
|
|
203
|
+
// ────────────────────────────────────────────────────────────────
|
|
204
|
+
|
|
205
|
+
test('COUNT(*) 计所有行(含 NULL)', async () => {
|
|
206
|
+
const { q } = makeDb();
|
|
207
|
+
await q('CREATE TABLE t (id INT PRIMARY KEY, v INT)');
|
|
208
|
+
await q('INSERT INTO t VALUES (1,10),(2,NULL),(3,30)');
|
|
209
|
+
assert.strictEqual(await one(q, 'SELECT COUNT(*) FROM t'), 3);
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
test('COUNT(col) 跳过 NULL', async () => {
|
|
213
|
+
const { q } = makeDb();
|
|
214
|
+
await q('CREATE TABLE t (id INT PRIMARY KEY, v INT)');
|
|
215
|
+
await q('INSERT INTO t VALUES (1,10),(2,NULL),(3,30)');
|
|
216
|
+
assert.strictEqual(await one(q, 'SELECT COUNT(v) FROM t'), 2);
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
test('COUNT(DISTINCT col) 去重且跳过 NULL', async () => {
|
|
220
|
+
const { q } = makeDb();
|
|
221
|
+
await q('CREATE TABLE t (id INT PRIMARY KEY, s STRING)');
|
|
222
|
+
await q("INSERT INTO t VALUES (1,'a'),(2,'a'),(3,NULL),(4,'b')");
|
|
223
|
+
assert.strictEqual(await one(q, 'SELECT COUNT(DISTINCT s) FROM t'), 2);
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
test('SUM 跳过 NULL(不全为 NULL 时)', async () => {
|
|
227
|
+
const { q } = makeDb();
|
|
228
|
+
await q('CREATE TABLE t (id INT PRIMARY KEY, v INT)');
|
|
229
|
+
await q('INSERT INTO t VALUES (1,10),(2,NULL),(3,30)');
|
|
230
|
+
assert.strictEqual(await one(q, 'SELECT SUM(v) FROM t'), 40);
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
test('SUM 全 NULL 返回 NULL', async () => {
|
|
234
|
+
const { q } = makeDb();
|
|
235
|
+
await q('CREATE TABLE t (id INT PRIMARY KEY, v INT)');
|
|
236
|
+
await q('INSERT INTO t VALUES (1,NULL),(2,NULL)');
|
|
237
|
+
assert.strictEqual(await one(q, 'SELECT SUM(v) FROM t'), null);
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
test('AVG 跳过 NULL(分母不含 NULL)', async () => {
|
|
241
|
+
const { q } = makeDb();
|
|
242
|
+
await q('CREATE TABLE t (id INT PRIMARY KEY, v INT)');
|
|
243
|
+
await q('INSERT INTO t VALUES (1,10),(2,NULL),(3,30)');
|
|
244
|
+
assert.strictEqual(await one(q, 'SELECT AVG(v) FROM t'), 20);
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
test('GROUP BY 多列正确分组', async () => {
|
|
248
|
+
const { q } = makeDb();
|
|
249
|
+
await q('CREATE TABLE t (id INT PRIMARY KEY, g INT, s STRING)');
|
|
250
|
+
await q("INSERT INTO t VALUES (1,1,'a'),(2,1,'a'),(3,2,'a'),(4,2,'b')");
|
|
251
|
+
const r = await rows(q, 'SELECT g, s, COUNT(*) AS cnt FROM t GROUP BY g, s ORDER BY g, s');
|
|
252
|
+
assert.strictEqual(r.length, 3);
|
|
253
|
+
assert.deepStrictEqual(r[0], [1, 'a', 2]);
|
|
254
|
+
assert.deepStrictEqual(r[1], [2, 'a', 1]);
|
|
255
|
+
assert.deepStrictEqual(r[2], [2, 'b', 1]);
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
test('HAVING 无 GROUP BY 时对整表聚合结果过滤', async () => {
|
|
259
|
+
const { q } = makeDb();
|
|
260
|
+
await q('CREATE TABLE t (id INT PRIMARY KEY, v INT)');
|
|
261
|
+
await q('INSERT INTO t VALUES (1,10),(2,20),(3,30)');
|
|
262
|
+
assert.strictEqual(await one(q, 'SELECT COUNT(*) FROM t HAVING COUNT(*) > 1'), 3);
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
test('聚合嵌套:SELECT COUNT(*) FROM (SELECT ...) t', async () => {
|
|
266
|
+
const { q } = makeDb();
|
|
267
|
+
await q('CREATE TABLE t (id INT PRIMARY KEY, g INT)');
|
|
268
|
+
await q('INSERT INTO t VALUES (1,1),(2,1),(3,2),(4,2),(5,NULL)');
|
|
269
|
+
assert.strictEqual(
|
|
270
|
+
await one(q, 'SELECT COUNT(*) FROM (SELECT DISTINCT g FROM t) x'),
|
|
271
|
+
3
|
|
272
|
+
);
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
test('混合聚合和非聚合列(GROUP BY 覆盖非聚合列)', async () => {
|
|
276
|
+
const { q } = makeDb();
|
|
277
|
+
await q('CREATE TABLE t (id INT PRIMARY KEY, g INT, v INT)');
|
|
278
|
+
await q('INSERT INTO t VALUES (1,1,10),(2,1,20),(3,2,30)');
|
|
279
|
+
const r = await rows(q, 'SELECT g, v, COUNT(*) AS cnt FROM t GROUP BY g, v ORDER BY g, v');
|
|
280
|
+
assert.strictEqual(r.length, 3);
|
|
281
|
+
assert.deepStrictEqual(r[0], [1, 10, 1]);
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
// ────────────────────────────────────────────────────────────────
|
|
285
|
+
// 3. 子查询和 JOIN(8+ tests)
|
|
286
|
+
// ────────────────────────────────────────────────────────────────
|
|
287
|
+
|
|
288
|
+
test('相关子查询:每行独立求值', async () => {
|
|
289
|
+
const { q } = makeDb();
|
|
290
|
+
await q('CREATE TABLE t (id INT PRIMARY KEY, g INT, v INT)');
|
|
291
|
+
await q('INSERT INTO t VALUES (1,1,100),(2,1,5),(3,2,50),(4,2,200)');
|
|
292
|
+
// g=1 的 AVG=52.5,g=2 的 AVG=125
|
|
293
|
+
// g=1: 100>52.5 ✓, 5>52.5 ✗; g=2: 50>125 ✗, 200>125 ✓ → 共 2 行
|
|
294
|
+
const r = await rows(q, 'SELECT COUNT(*) FROM t a WHERE v > (SELECT AVG(v) FROM t b WHERE b.g = a.g)');
|
|
295
|
+
assert.strictEqual(r[0][0], 2);
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
test('子查询返回多列', async () => {
|
|
299
|
+
const { q } = makeDb();
|
|
300
|
+
await q('CREATE TABLE t (id INT PRIMARY KEY, g INT, v INT)');
|
|
301
|
+
await q('INSERT INTO t VALUES (1,1,10),(2,2,20),(3,3,30)');
|
|
302
|
+
const r = await rows(q, 'SELECT * FROM (SELECT g, v FROM t WHERE v >= 20) x ORDER BY g');
|
|
303
|
+
assert.strictEqual(r.length, 2);
|
|
304
|
+
assert.deepStrictEqual(r[0], [2, 20]);
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
test('JOIN 无 ON 条件产生笛卡尔积', async () => {
|
|
308
|
+
const { q } = makeDb();
|
|
309
|
+
await q('CREATE TABLE a (id INT PRIMARY KEY)');
|
|
310
|
+
await q('CREATE TABLE b (id INT PRIMARY KEY)');
|
|
311
|
+
await q('INSERT INTO a VALUES (1),(2)');
|
|
312
|
+
await q('INSERT INTO b VALUES (1),(2),(3)');
|
|
313
|
+
assert.strictEqual(await one(q, 'SELECT COUNT(*) FROM a JOIN b'), 6);
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
test('逗号连接产生笛卡尔积', async () => {
|
|
317
|
+
const { q } = makeDb();
|
|
318
|
+
await q('CREATE TABLE a (id INT PRIMARY KEY)');
|
|
319
|
+
await q('CREATE TABLE b (id INT PRIMARY KEY)');
|
|
320
|
+
await q('INSERT INTO a VALUES (1),(2)');
|
|
321
|
+
await q('INSERT INTO b VALUES (1),(2),(3)');
|
|
322
|
+
assert.strictEqual(await one(q, 'SELECT COUNT(*) FROM a, b'), 6);
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
test('LEFT JOIN 右表无匹配时填 NULL', async () => {
|
|
326
|
+
const { q } = makeDb();
|
|
327
|
+
await q('CREATE TABLE a (id INT PRIMARY KEY, name STRING)');
|
|
328
|
+
await q('CREATE TABLE b (id INT PRIMARY KEY, a_id INT, val STRING)');
|
|
329
|
+
await q("INSERT INTO a VALUES (1,'x'),(2,'y'),(3,'z')");
|
|
330
|
+
await q("INSERT INTO b VALUES (10,2,'matched')");
|
|
331
|
+
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');
|
|
332
|
+
assert.strictEqual(r.length, 3);
|
|
333
|
+
assert.deepStrictEqual(r[0], [1, 'x', null]);
|
|
334
|
+
assert.deepStrictEqual(r[2], [3, 'z', null]);
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
test('子查询在 SELECT 列表(标量子查询)', async () => {
|
|
338
|
+
const { q } = makeDb();
|
|
339
|
+
await q('CREATE TABLE t (id INT PRIMARY KEY, v INT)');
|
|
340
|
+
await q('INSERT INTO t VALUES (1,10),(2,20),(3,30)');
|
|
341
|
+
assert.strictEqual(await one(q, 'SELECT MAX(v) FROM t'), 30);
|
|
342
|
+
const r = await one(q, 'SELECT (SELECT MAX(v) FROM t) FROM t LIMIT 1');
|
|
343
|
+
assert.strictEqual(r, 30);
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
test('嵌套子查询 3 层', async () => {
|
|
347
|
+
const { q } = makeDb();
|
|
348
|
+
await q('CREATE TABLE t (id INT PRIMARY KEY, g INT)');
|
|
349
|
+
await q('INSERT INTO t VALUES (1,1),(2,1),(3,2),(4,3)');
|
|
350
|
+
assert.strictEqual(
|
|
351
|
+
await one(q, 'SELECT COUNT(*) FROM (SELECT g FROM (SELECT DISTINCT g FROM t) a) b'),
|
|
352
|
+
3
|
|
353
|
+
);
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
test('IN 子查询', async () => {
|
|
357
|
+
const { q } = makeDb();
|
|
358
|
+
await q('CREATE TABLE t (id INT PRIMARY KEY, g INT, v INT)');
|
|
359
|
+
await q('INSERT INTO t VALUES (1,1,10),(2,1,20),(3,2,30)');
|
|
360
|
+
assert.strictEqual(
|
|
361
|
+
await one(q, 'SELECT COUNT(*) FROM t WHERE g IN (SELECT DISTINCT g FROM t WHERE g IS NOT NULL)'),
|
|
362
|
+
3
|
|
363
|
+
);
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
test('IN 子查询含 NULL 导致 UNKNOWN(返回 0 行)', async () => {
|
|
367
|
+
const { q } = makeDb();
|
|
368
|
+
await q('CREATE TABLE t (id INT PRIMARY KEY, g INT)');
|
|
369
|
+
await q('INSERT INTO t VALUES (1,1),(2,NULL)');
|
|
370
|
+
assert.strictEqual(
|
|
371
|
+
await one(q, 'SELECT COUNT(*) FROM t WHERE g IN (SELECT g FROM t)'),
|
|
372
|
+
0
|
|
373
|
+
);
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
test('EXISTS 子查询', async () => {
|
|
377
|
+
const { q } = makeDb();
|
|
378
|
+
await q('CREATE TABLE t (id INT PRIMARY KEY, g INT)');
|
|
379
|
+
await q('INSERT INTO t VALUES (1,1),(2,2),(3,1)');
|
|
380
|
+
assert.strictEqual(
|
|
381
|
+
await one(q, 'SELECT COUNT(*) FROM t WHERE EXISTS (SELECT 1 FROM t WHERE t.g = g)'),
|
|
382
|
+
3
|
|
383
|
+
);
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
test('NOT EXISTS 子查询', async () => {
|
|
387
|
+
const { q } = makeDb();
|
|
388
|
+
await q('CREATE TABLE t (id INT PRIMARY KEY, g INT)');
|
|
389
|
+
await q('INSERT INTO t VALUES (1,1),(2,2),(3,1)');
|
|
390
|
+
// g=2 没有其他行同组,NOT EXISTS 为真
|
|
391
|
+
assert.strictEqual(
|
|
392
|
+
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)'),
|
|
393
|
+
1
|
|
394
|
+
);
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
// ────────────────────────────────────────────────────────────────
|
|
398
|
+
// 4. DDL 和表操作(6+ tests)
|
|
399
|
+
// ────────────────────────────────────────────────────────────────
|
|
400
|
+
|
|
401
|
+
test('CREATE TABLE 带 DEFAULT 值对新行生效', async () => {
|
|
402
|
+
const { q } = makeDb();
|
|
403
|
+
await q("CREATE TABLE t (id INT PRIMARY KEY, n STRING, extra STRING DEFAULT 'x')");
|
|
404
|
+
await q("INSERT INTO t (id, n) VALUES (1,'a')");
|
|
405
|
+
const r = await rows(q, 'SELECT extra FROM t');
|
|
406
|
+
assert.strictEqual(r[0][0], 'x');
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
test('ALTER TABLE ADD COLUMN 带 DEFAULT 子句(DEFAULT 对新行不生效,已知限制)', async () => {
|
|
410
|
+
const { q } = makeDb();
|
|
411
|
+
await q('CREATE TABLE t (id INT PRIMARY KEY, n STRING)');
|
|
412
|
+
await q("INSERT INTO t VALUES (1,'a')");
|
|
413
|
+
await q("ALTER TABLE t ADD COLUMN extra STRING DEFAULT 'x'");
|
|
414
|
+
await q("INSERT INTO t (id, n) VALUES (2,'b')");
|
|
415
|
+
const r = await rows(q, 'SELECT id, n, extra FROM t ORDER BY id');
|
|
416
|
+
// 已知限制:ALTER TABLE ADD COLUMN 的 DEFAULT 不应用到新行
|
|
417
|
+
assert.ok(r[1][2] == null); // null 或 undefined(序列化差异)
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
test('ALTER TABLE DROP COLUMN', async () => {
|
|
421
|
+
const { q } = makeDb();
|
|
422
|
+
await q('CREATE TABLE t (id INT PRIMARY KEY, a STRING, b STRING)');
|
|
423
|
+
await q("INSERT INTO t VALUES (1,'x','y')");
|
|
424
|
+
await q('ALTER TABLE t DROP COLUMN b');
|
|
425
|
+
const r = await rows(q, 'SELECT * FROM t');
|
|
426
|
+
assert.deepStrictEqual(r[0], [1, 'x']);
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
test('CREATE TABLE 带各种约束(PK、UNIQUE、NOT NULL、DEFAULT)', async () => {
|
|
430
|
+
const { q } = makeDb();
|
|
431
|
+
await q("CREATE TABLE cons (id INT PRIMARY KEY, u STRING UNIQUE, nn INT NOT NULL, d INT DEFAULT 99)");
|
|
432
|
+
// 合法插入
|
|
433
|
+
await q("INSERT INTO cons (id, u, nn) VALUES (1, 'a', 1)");
|
|
434
|
+
// DEFAULT 生效
|
|
435
|
+
const r = await rows(q, 'SELECT d FROM cons WHERE id=1');
|
|
436
|
+
assert.strictEqual(r[0][0], 99);
|
|
437
|
+
// 违反 PK
|
|
438
|
+
await assert.rejects(() => q("INSERT INTO cons (id, u, nn) VALUES (1, 'c', 3)"));
|
|
439
|
+
// 违反 UNIQUE
|
|
440
|
+
await assert.rejects(() => q("INSERT INTO cons (id, u, nn) VALUES (2, 'a', 4)"));
|
|
441
|
+
// 违反 NOT NULL
|
|
442
|
+
await assert.rejects(() => q("INSERT INTO cons (id, u, nn) VALUES (3, 'b', NULL)"));
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
test('TRUNCATE 后自增重置', async () => {
|
|
446
|
+
const { q } = makeDb();
|
|
447
|
+
await q('CREATE TABLE t (id INT PRIMARY KEY AUTO_INCREMENT, n STRING)');
|
|
448
|
+
await q("INSERT INTO t (n) VALUES ('a'),('b'),('c')");
|
|
449
|
+
await q('TRUNCATE TABLE t');
|
|
450
|
+
assert.strictEqual(await one(q, 'SELECT COUNT(*) FROM t'), 0);
|
|
451
|
+
await q("INSERT INTO t (n) VALUES ('z')");
|
|
452
|
+
const r = await rows(q, 'SELECT id FROM t');
|
|
453
|
+
assert.strictEqual(r[0][0], 1); // 自增重置
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
test('RENAME TABLE 后旧名不可用', async () => {
|
|
457
|
+
const { q } = makeDb();
|
|
458
|
+
await q('CREATE TABLE old (id INT PRIMARY KEY)');
|
|
459
|
+
await q('ALTER TABLE old RENAME TO new');
|
|
460
|
+
await q('INSERT INTO new VALUES (1)');
|
|
461
|
+
assert.strictEqual(await one(q, 'SELECT COUNT(*) FROM new'), 1);
|
|
462
|
+
await assert.rejects(() => q('SELECT COUNT(*) FROM old'));
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
test('创建重复表名报错', async () => {
|
|
466
|
+
const { q } = makeDb();
|
|
467
|
+
await q('CREATE TABLE dup (id INT PRIMARY KEY)');
|
|
468
|
+
await assert.rejects(() => q('CREATE TABLE dup (id INT PRIMARY KEY)'));
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
// ────────────────────────────────────────────────────────────────
|
|
472
|
+
// 5. 表达式和函数(6+ tests)
|
|
473
|
+
// ────────────────────────────────────────────────────────────────
|
|
474
|
+
|
|
475
|
+
test('CASE WHEN 搜索形式', async () => {
|
|
476
|
+
const { q } = makeDb();
|
|
477
|
+
assert.strictEqual(
|
|
478
|
+
await one(q, "SELECT CASE WHEN 1=1 THEN 'yes' ELSE 'no' END"),
|
|
479
|
+
'yes'
|
|
480
|
+
);
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
test('CASE 简单形式', async () => {
|
|
484
|
+
const { q } = makeDb();
|
|
485
|
+
assert.strictEqual(
|
|
486
|
+
await one(q, "SELECT CASE 2 WHEN 1 THEN 'a' WHEN 2 THEN 'b' ELSE 'c' END"),
|
|
487
|
+
'b'
|
|
488
|
+
);
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
test('CASE 无匹配且无 ELSE 返回 NULL', async () => {
|
|
492
|
+
const { q } = makeDb();
|
|
493
|
+
assert.strictEqual(
|
|
494
|
+
await one(q, "SELECT CASE WHEN 0=1 THEN 'yes' END"),
|
|
495
|
+
null
|
|
496
|
+
);
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
test('CASE 条件为 NULL 时走 ELSE', async () => {
|
|
500
|
+
const { q } = makeDb();
|
|
501
|
+
assert.strictEqual(
|
|
502
|
+
await one(q, "SELECT CASE NULL WHEN 1 THEN 'a' ELSE 'b' END"),
|
|
503
|
+
'b'
|
|
504
|
+
);
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
test('COALESCE / NULLIF / IFNULL', async () => {
|
|
508
|
+
const { q } = makeDb();
|
|
509
|
+
assert.strictEqual(await one(q, 'SELECT COALESCE(NULL, NULL, 3)'), 3);
|
|
510
|
+
assert.strictEqual(await one(q, 'SELECT NULLIF(1, 1)'), null);
|
|
511
|
+
assert.strictEqual(await one(q, 'SELECT NULLIF(1, 2)'), 1);
|
|
512
|
+
assert.strictEqual(await one(q, 'SELECT IFNULL(NULL, 9)'), 9);
|
|
513
|
+
});
|
|
514
|
+
|
|
515
|
+
test('字符串函数:LENGTH / UPPER / LOWER / TRIM / CONCAT / SUBSTR', async () => {
|
|
516
|
+
const { q } = makeDb();
|
|
517
|
+
assert.strictEqual(await one(q, "SELECT LENGTH('hello')"), 5);
|
|
518
|
+
assert.strictEqual(await one(q, "SELECT UPPER('hi')"), 'HI');
|
|
519
|
+
assert.strictEqual(await one(q, "SELECT LOWER('HI')"), 'hi');
|
|
520
|
+
assert.strictEqual(await one(q, "SELECT TRIM(' x ')"), 'x');
|
|
521
|
+
assert.strictEqual(await one(q, "SELECT CONCAT('a', 'b', 'c')"), 'abc');
|
|
522
|
+
assert.strictEqual(await one(q, "SELECT SUBSTR('hello', 2, 3)"), 'ell');
|
|
523
|
+
});
|
|
524
|
+
|
|
525
|
+
test('数学函数:ABS / CEIL / FLOOR / ROUND / POWER / SQRT', async () => {
|
|
526
|
+
const { q } = makeDb();
|
|
527
|
+
assert.strictEqual(await one(q, 'SELECT ABS(-5)'), 5);
|
|
528
|
+
assert.strictEqual(await one(q, 'SELECT CEIL(2.1)'), 3);
|
|
529
|
+
assert.strictEqual(await one(q, 'SELECT FLOOR(2.9)'), 2);
|
|
530
|
+
assert.strictEqual(await one(q, 'SELECT ROUND(2.5)'), 3);
|
|
531
|
+
assert.strictEqual(await one(q, 'SELECT ROUND(-2.5)'), -3); // 半远离零
|
|
532
|
+
assert.strictEqual(await one(q, 'SELECT POWER(2, 3)'), 8);
|
|
533
|
+
assert.strictEqual(await one(q, 'SELECT SQRT(16)'), 4);
|
|
534
|
+
});
|
|
535
|
+
|
|
536
|
+
test('SQRT 负数返回 NaN', async () => {
|
|
537
|
+
const { q } = makeDb();
|
|
538
|
+
const v = await one(q, 'SELECT SQRT(-1)');
|
|
539
|
+
assert.ok(Number.isNaN(v));
|
|
540
|
+
});
|
|
541
|
+
|
|
542
|
+
test('REPLACE 函数', async () => {
|
|
543
|
+
const { q } = makeDb();
|
|
544
|
+
assert.strictEqual(await one(q, "SELECT REPLACE('hello', 'l', 'r')"), 'herro');
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
test('嵌套函数调用', async () => {
|
|
548
|
+
const { q } = makeDb();
|
|
549
|
+
assert.strictEqual(await one(q, "SELECT UPPER(TRIM(' hi '))"), 'HI');
|
|
550
|
+
assert.strictEqual(await one(q, "SELECT LENGTH(CONCAT('a', 'b', 'c'))"), 3);
|
|
551
|
+
});
|
|
552
|
+
|
|
553
|
+
test('ABS 无参数返回 NULL', async () => {
|
|
554
|
+
const { q } = makeDb();
|
|
555
|
+
assert.strictEqual(await one(q, 'SELECT ABS()'), null);
|
|
556
|
+
});
|
|
557
|
+
|
|
558
|
+
test('LENGTH 多参数只取第一个', async () => {
|
|
559
|
+
const { q } = makeDb();
|
|
560
|
+
assert.strictEqual(await one(q, "SELECT LENGTH('ab', 'cd')"), 2);
|
|
561
|
+
});
|
|
562
|
+
|
|
563
|
+
test('IF 函数三值行为(NULL / 0 / 空串均为假)', async () => {
|
|
564
|
+
const { q } = makeDb();
|
|
565
|
+
assert.strictEqual(await one(q, "SELECT IF(NULL, 'a', 'b')"), 'b');
|
|
566
|
+
assert.strictEqual(await one(q, "SELECT IF(0, 'a', 'b')"), 'b');
|
|
567
|
+
assert.strictEqual(await one(q, "SELECT IF('', 'a', 'b')"), 'b');
|
|
568
|
+
assert.strictEqual(await one(q, "SELECT IF(1, 'a', 'b')"), 'a');
|
|
569
|
+
});
|