jsql-neo 6.4.1 → 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 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.1** — official release build · [github.com/vexify-org/JSQL-neo](https://github.com/vexify-org/JSQL-neo) · [jsql.vexify.top](https://jsql.vexify.top)
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
  ![Engines](https://img.shields.io/badge/engines-Native%20%7C%20WASM%20%7C%20Pure%20JS-7ee787)
9
9
  ![MySQL](https://img.shields.io/badge/protocol-MySQL%20compatible-1f6feb)
package/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * JSQL-NEO v6.4.1 — Rust-Powered Embedded Database (WASM + HTTP)
2
+ * JSQL-NEO v6.4.2 — Rust-Powered Embedded Database (WASM + HTTP)
3
3
  *
4
4
  * @example
5
5
  * const jsql = require('jsql-neo');
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
- this.renameTable(op.from, op.to);
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
 
@@ -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 bin = encodeBatch(rows);
514
- const r = safeParse(this._n('jsqlInsertBuf', table, bin));
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
- this.engine.renameTable(statement.name, op.newName);
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;
@@ -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 r = safeJsonParse(wasmBindings.jsql_insert_json(table, safeStringify(rows, table, '<rows>')));
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
- let res = (|| -> Result<Vec<u64>, String> {
80
- let mut eng = eng.borrow_mut();
81
- let t = eng.tables.get_mut(table).ok_or_else(|| format!("table '{}' not found", table))?;
82
-
83
- let data: Vec<serde_json::Value> = serde_json::from_str(json)
84
- .map_err(|e| format!("invalid JSON: {}", e))?;
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
- let ids: Vec<u64> = (start_id..start_id + n as u64).collect();
166
- Ok(ids)
167
- })();
168
- match res {
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jsql-neo",
3
- "version": "6.4.1",
3
+ "version": "6.4.2",
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",
@@ -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
+ });