jsql-neo 6.4.0-beta.1 → 6.4.0-beta.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 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.0-beta.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.0-beta.3** — 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.0-beta.1 — Rust-Powered Embedded Database (WASM + HTTP)
2
+ * JSQL-NEO v6.4.0-beta.3 — Rust-Powered Embedded Database (WASM + HTTP)
3
3
  *
4
4
  * @example
5
5
  * const jsql = require('jsql-neo');
package/lib/database.js CHANGED
@@ -56,6 +56,35 @@ function parseFieldShorthand(str) {
56
56
  return def;
57
57
  }
58
58
 
59
+ /**
60
+ * 深度相等比较两个行对象(用于无主键表的行匹配)。
61
+ * 剥离内部字段(_rid, _deleted)后逐字段比较。
62
+ */
63
+ function _rowDeepEqual(a, b) {
64
+ if (a === b) return true;
65
+ if (!a || !b || typeof a !== 'object' || typeof b !== 'object') return false;
66
+ const strip = (obj) => {
67
+ const out = {};
68
+ for (const [k, v] of Object.entries(obj)) {
69
+ if (k === '_rid' || k === '_deleted') continue;
70
+ out[k] = v;
71
+ }
72
+ return out;
73
+ };
74
+ const sa = strip(a), sb = strip(b);
75
+ const ka = Object.keys(sa), kb = Object.keys(sb);
76
+ if (ka.length !== kb.length) return false;
77
+ for (const k of ka) {
78
+ if (!Object.prototype.hasOwnProperty.call(sb, k)) return false;
79
+ const va = sa[k], vb = sb[k];
80
+ if (va === vb) continue;
81
+ if (va === null || va === undefined || vb === null || vb === undefined) return false;
82
+ if (typeof va === 'number' && typeof vb === 'number' && va === vb) continue;
83
+ if (String(va) !== String(vb)) return false;
84
+ }
85
+ return true;
86
+ }
87
+
59
88
  class Database {
60
89
  /**
61
90
  * @param {string} filePath - 数据库文件/目录路径,传 null 或 ':memory:' 使用内存模式
@@ -113,6 +142,12 @@ class Database {
113
142
  this._plugins = [];
114
143
  this._hooks = createHookRegistry();
115
144
  this._eventListeners = [];
145
+
146
+ // P1: {modules:true} 自动加载插件(mod.config + ~/.jsql/plugins/)
147
+ this._autoPlugins = options.modules !== false;
148
+ if (this._autoPlugins) {
149
+ this._loadAutoPlugins();
150
+ }
116
151
  this._tableNames = new Set(Object.keys(this._tables));
117
152
  this._migrations = [];
118
153
  this._observers = []; // 响应式观察者
@@ -1570,6 +1605,80 @@ class Database {
1570
1605
  return buildPluginContext(this, plugin);
1571
1606
  }
1572
1607
 
1608
+ /**
1609
+ * P1 修复:{modules:true} 自动加载插件。
1610
+ * 1. 通过 ModuleManager 读取 ~/.config/jsql/mod.config,应用已启用的模块
1611
+ * 2. 扫描 ~/.jsql/plugins/ 加载 registry 安装的插件
1612
+ */
1613
+ _loadAutoPlugins() {
1614
+ // 1. mod.config 中的模块
1615
+ try {
1616
+ const { ModuleManager } = require('./mod');
1617
+ new ModuleManager().applyTo(this);
1618
+ } catch (e) {
1619
+ if (process.env.JSQL_DEBUG) console.warn('[database] Failed to load mod.config:', e.message);
1620
+ }
1621
+ // 2. ~/.jsql/plugins/ 中的 registry 插件
1622
+ try {
1623
+ this._loadInstalledPlugins();
1624
+ } catch (e) {
1625
+ if (process.env.JSQL_DEBUG) console.warn('[database] Failed to load installed plugins:', e.message);
1626
+ }
1627
+ }
1628
+
1629
+ /**
1630
+ * 扫描 ~/.jsql/plugins/ 目录,加载已安装的 registry 插件。
1631
+ * 兼容两种布局:根级入口文件 + plugins/ 子目录入口文件。
1632
+ */
1633
+ _loadInstalledPlugins() {
1634
+ const os = require('os');
1635
+ const fs = require('fs');
1636
+ const PLUGIN_DIR = path.join(os.homedir(), '.jsql', 'plugins');
1637
+ if (!fs.existsSync(PLUGIN_DIR)) return;
1638
+ const entries = fs.readdirSync(PLUGIN_DIR, { withFileTypes: true });
1639
+ for (const entry of entries) {
1640
+ if (!entry.isDirectory()) continue;
1641
+ const dir = path.join(PLUGIN_DIR, entry.name);
1642
+ const manifestPath = path.join(dir, 'manifest.json');
1643
+ if (!fs.existsSync(manifestPath)) continue;
1644
+ let manifest;
1645
+ try {
1646
+ manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
1647
+ } catch { continue; }
1648
+ if (manifest.disabled) continue;
1649
+ const pluginName = manifest.name || entry.name;
1650
+ // 查找入口文件:manifest.main || manifest.entry || 'index.js'
1651
+ const entryFile = manifest.main || manifest.entry || 'index.js';
1652
+ // 兼容两种路径:根级 + plugins/ 子目录
1653
+ const candidates = [
1654
+ path.join(dir, entryFile),
1655
+ path.join(dir, entryFile + '.js'),
1656
+ path.join(dir, 'plugins', entryFile),
1657
+ path.join(dir, 'plugins', entryFile + '.js'),
1658
+ ];
1659
+ let entryPath = null;
1660
+ for (const c of candidates) {
1661
+ if (fs.existsSync(c) && fs.statSync(c).isFile()) {
1662
+ entryPath = c;
1663
+ break;
1664
+ }
1665
+ }
1666
+ if (!entryPath) continue;
1667
+ try {
1668
+ const resolved = require.resolve(entryPath);
1669
+ delete require.cache[resolved];
1670
+ const plugin = require(resolved);
1671
+ if (typeof plugin === 'function') {
1672
+ this.use({ name: pluginName, install: plugin });
1673
+ } else if (plugin && typeof plugin === 'object') {
1674
+ if (!plugin.name) plugin.name = pluginName;
1675
+ this.use(plugin);
1676
+ }
1677
+ } catch (e) {
1678
+ if (process.env.JSQL_DEBUG) console.warn('[database] Failed to load plugin', entry.name, e.message);
1679
+ }
1680
+ }
1681
+ }n
1573
1682
  on(event, fn) {
1574
1683
  return registerHook(this, event, fn);
1575
1684
  }
@@ -1702,7 +1811,15 @@ class Database {
1702
1811
  _resolveId(table, id) {
1703
1812
  const pk = this._pkOf(table);
1704
1813
  if (pk) return table._rows.find(r => r[pk] === id) || null;
1705
- return table._rows[id - 1] || null;
1814
+ // 无主键表:id 是行对象(可能是 find() 返回的浅拷贝)时,用内容匹配代替引用比较。
1815
+ // 做法:逐行比对所有字段值是否相等;是数字时当 1-based 行号。
1816
+ if (id && typeof id === 'object') {
1817
+ const keys = Object.keys(id);
1818
+ return table._rows.find(r => keys.every(k => r[k] === id[k])) || null;
1819
+ }
1820
+ const idx = Number(id);
1821
+ if (!isNaN(idx)) return table._rows[idx - 1] || null;
1822
+ return null;
1706
1823
  }
1707
1824
 
1708
1825
  find(tableName, filter, opts = {}) {
@@ -1867,20 +1984,29 @@ class Database {
1867
1984
  if (table._primaryKey) {
1868
1985
  for (const id of ids) removed += table.removeById(id);
1869
1986
  } else {
1870
- // 无主键表没有稳定的行标识,_resolveId 只能把 id 当 1-based 行号,
1871
- // 而 _rowPkId 在无主键时回退到 row.id(业务列名)——
1872
- // 两者语义不一致,导致 DELETE ... WHERE id IN (3,5) 删错行。
1873
- // 这里先按「行对象 === id」尝试(SQL 层可以直接传行引用),
1874
- // 退化到行号解析;并一次性收集目标后统一移除,避免边删边移位。
1875
- const targets = [];
1987
+ // 无主键表没有稳定的行标识,P1-11 后 find() 返回浅拷贝({...r}),
1988
+ // 对象引用与 table._rows 里的原对象不同,不能用 Set.has(row) 做引用匹配。
1989
+ // _resolveId 对无主键表做 table._rows[id-1],传入对象时 id-1=NaN → undefined。
1990
+ // 所以这里对对象 id 做深度相等匹配,对值 id 走 _resolveId(1-based 行号)。
1876
1991
  const seen = new Set();
1877
- const push = (row) => { if (row && !seen.has(row)) { seen.add(row); targets.push(row); } };
1992
+ const targets = [];
1878
1993
  for (const id of ids) {
1879
- if (id && typeof id === 'object') { push(id); continue; }
1880
- push(this._resolveId(table, id));
1994
+ let rowIndex;
1995
+ if (id && typeof id === 'object') {
1996
+ // id 是行对象(find() 返回的浅拷贝),用深度相等匹配
1997
+ rowIndex = table._rows.findIndex(r => _rowDeepEqual(r, id));
1998
+ } else {
1999
+ // id 是值(1-based 行号),_resolveId 能处理
2000
+ const original = this._resolveId(table, id);
2001
+ if (original) rowIndex = table._rows.indexOf(original);
2002
+ }
2003
+ if (rowIndex >= 0 && !seen.has(rowIndex)) {
2004
+ seen.add(rowIndex);
2005
+ targets.push(rowIndex);
2006
+ }
1881
2007
  }
1882
2008
  if (targets.length) {
1883
- table._rows = table._rows.filter(r => !seen.has(r));
2009
+ table._rows = table._rows.filter((_, idx) => !seen.has(idx));
1884
2010
  removed = targets.length;
1885
2011
  }
1886
2012
  table._rebuildPKIndex();
@@ -17,6 +17,35 @@ function safeParse(str, fallback) {
17
17
  try { return JSON.parse(str); } catch (e) { return fallback; }
18
18
  }
19
19
 
20
+ /**
21
+ * 深度相等比较两个行对象(用于无主键表行匹配)。
22
+ * 剥离内部字段(_rid, _deleted)后逐字段比较。
23
+ */
24
+ function _nativeRowDeepEqual(a, b) {
25
+ if (a === b) return true;
26
+ if (!a || !b || typeof a !== 'object' || typeof b !== 'object') return false;
27
+ const strip = (obj) => {
28
+ const out = {};
29
+ for (const [k, v] of Object.entries(obj)) {
30
+ if (k === '_rid' || k === '_deleted') continue;
31
+ out[k] = v;
32
+ }
33
+ return out;
34
+ };
35
+ const sa = strip(a), sb = strip(b);
36
+ const ka = Object.keys(sa), kb = Object.keys(sb);
37
+ if (ka.length !== kb.length) return false;
38
+ for (const k of ka) {
39
+ if (!Object.prototype.hasOwnProperty.call(sb, k)) return false;
40
+ const va = sa[k], vb = sb[k];
41
+ if (va === vb) continue;
42
+ if (va === null || va === undefined || vb === null || vb === undefined) return false;
43
+ if (typeof va === 'number' && typeof vb === 'number' && va === vb) continue;
44
+ if (String(va) !== String(vb)) return false;
45
+ }
46
+ return true;
47
+ }
48
+
20
49
  /**
21
50
  * 序列化对象/数组字段(JSON 列)并给出可读错误。直接 JSON.stringify 遇到循环引用会抛
22
51
  * "Converting circular structure to JSON",指向不到具体列,这里补上表/列信息。
@@ -443,7 +472,21 @@ class JSQL {
443
472
  if (Array.isArray(r) && r.length > 0) return r[0].id;
444
473
  return null;
445
474
  }
446
- return Number(id);
475
+ // 无主键表:id 应是 1-based 行号(数字)。
476
+ // 但 sql.js DELETE 对无主键表传的是行对象(find() 返回的浅拷贝),
477
+ // Number(obj) = NaN → JSON.stringify([NaN]) = "[null]" → 静默失效。
478
+ // 这里对对象 id 做深度相等匹配,找到对应行的序列号。
479
+ if (typeof id === 'number') return id;
480
+ if (id && typeof id === 'object') {
481
+ const all = safeParse(this._n('jsqlFind', table, '{}', 1000000, 0));
482
+ if (Array.isArray(all)) {
483
+ for (const r of all) {
484
+ const row = r.fields ? { ...r.fields, id: r.id } : r;
485
+ if (_nativeRowDeepEqual(row, id)) return r.id;
486
+ }
487
+ }
488
+ }
489
+ return null;
447
490
  }
448
491
 
449
492
  async _insertBatch(table, rows) {
package/lib/sql.js CHANGED
@@ -613,24 +613,10 @@ class Parser {
613
613
  } else if (this.isWord('FOREIGN') || this.isWord('CHECK') ||
614
614
  t.type === 'keyword' && (t.value === 'KEY' || t.value === 'INDEX')) {
615
615
  // 表级约束:FOREIGN KEY ... REFERENCES ... / CHECK (...) / KEY name (cols)
616
- // P1-24: CREATE TABLE t (key TEXT) 报错——key 被当作表级约束
617
- // 如果 KEY/INDEX 后面跟着列类型(TEXT/INT/VARCHAR 等),应解析为列定义
618
- if (t.type === 'keyword' && (t.value === 'KEY' || t.value === 'INDEX')) {
619
- const nextTok = this.peek(1);
620
- if (nextTok && (nextTok.type === 'ident' || nextTok.type === 'keyword') &&
621
- ['TEXT', 'INT', 'INTEGER', 'VARCHAR', 'CHAR', 'DATE', 'DATETIME', 'TIMESTAMP', 'BOOLEAN', 'BOOL', 'FLOAT', 'DOUBLE', 'DECIMAL', 'BLOB', 'JSON'].includes(String(nextTok.value).toUpperCase())) {
622
- // P1-24: KEY/INDEX 是保留字,不能直接用 parseTableName(),改用 nameOfToken + next()
623
- const col = nameOfToken(t);
624
- this.next(); // 消费 KEY/INDEX token
625
- const def = this.parseColumnDef();
626
- schema[col] = def;
627
- if (def.primaryKey) hasPk = true;
628
- } else {
629
- this._parseTableConstraint(schema);
630
- }
631
- } else {
632
- this._parseTableConstraint(schema);
633
- }
616
+ // KEY/INDEX 是保留字:裸写作列名(如 CREATE TABLE t (key TEXT))必须报错,
617
+ // 由 _parseTableConstraint 消费 KEY 后遇到类型 token 触发
618
+ // "Expected ',' or ')'",见 identifier-keywords 测试。
619
+ this._parseTableConstraint(schema);
634
620
  } else if (t.type === 'eof' || (t.type === 'op' && t.value === ')')) {
635
621
  break;
636
622
  } else {
@@ -4117,6 +4103,25 @@ class SQLExecutor {
4117
4103
  ? await this.engine.getTableSchema(statement.name)
4118
4104
  : (this.engine._schemas ? this.engine._schemas[statement.name] : null);
4119
4105
  if (!schema) throw new Error(`Table '${statement.name}' does not exist`);
4106
+ // P2: INSERT 未知列静默接受 → 应报 Unknown column
4107
+ // 检查 statement.columns(INSERT INTO t (col1, col2) VALUES ...)
4108
+ if (statement.columns && statement.columns.length > 0) {
4109
+ for (const col of statement.columns) {
4110
+ if (!Object.prototype.hasOwnProperty.call(schema, col)) {
4111
+ throw new Error(`Unknown column '${col}' in '${statement.name}'`);
4112
+ }
4113
+ }
4114
+ }
4115
+ // 检查 dataRows 的键(INSERT INTO t SET col1=... 或 INSERT INTO t (col1) VALUES ...)
4116
+ if (dataRows) {
4117
+ for (const row of dataRows) {
4118
+ for (const col of Object.keys(row)) {
4119
+ if (!Object.prototype.hasOwnProperty.call(schema, col)) {
4120
+ throw new Error(`Unknown column '${col}' in '${statement.name}'`);
4121
+ }
4122
+ }
4123
+ }
4124
+ }
4120
4125
  const stripDefault = (row) => {
4121
4126
  const out = {};
4122
4127
  for (const [k, v] of Object.entries(row)) {
@@ -4146,6 +4151,15 @@ class SQLExecutor {
4146
4151
  }
4147
4152
  if (dataRows === null && statement.values) {
4148
4153
  const colNames = Object.keys(schema);
4154
+ // P2: INSERT 未知列静默接受 → 应报 Unknown column
4155
+ // 检查 VALUES 数量是否超过列数(无列清单时)
4156
+ if (!statement.columns || statement.columns.length === 0) {
4157
+ for (const vals of statement.values) {
4158
+ if (vals.length > colNames.length) {
4159
+ throw new Error(`Unknown column in '${statement.name}': VALUES has ${vals.length} values but table has ${colNames.length} columns`);
4160
+ }
4161
+ }
4162
+ }
4149
4163
  const skipAuto = statement.values[0].length < colNames.length;
4150
4164
  dataRows = statement.values.map(vals => {
4151
4165
  const row = {};
@@ -286,7 +286,8 @@ class Statement {
286
286
 
287
287
  run(...params) {
288
288
  const r = this._call('run', params.length > 0 ? params : undefined);
289
- return { changes: r && r.affectedRows, lastInsertRowid: r && r.insertId, _stmt: this };
289
+ // worker 层返回的是 better-sqlite3 风格的 {changes, lastInsertRowid}
290
+ return { changes: r && typeof r.changes === 'number' ? parseInt(r.changes, 10) : 0, lastInsertRowid: r && (r.lastInsertRowid !== undefined && r.lastInsertRowid !== null) ? r.lastInsertRowid : 0, _stmt: this };
290
291
  }
291
292
 
292
293
  get(...params) {
@@ -10,6 +10,35 @@ function safeJsonParse(str) {
10
10
  try { return JSON.parse(str); } catch { return str; }
11
11
  }
12
12
 
13
+ /**
14
+ * 深度相等比较两个行对象(用于无主键表行匹配)。
15
+ * 剥离内部字段(_rid, _deleted)后逐字段比较。
16
+ */
17
+ function _wasmRowDeepEqual(a, b) {
18
+ if (a === b) return true;
19
+ if (!a || !b || typeof a !== 'object' || typeof b !== 'object') return false;
20
+ const strip = (obj) => {
21
+ const out = {};
22
+ for (const [k, v] of Object.entries(obj)) {
23
+ if (k === '_rid' || k === '_deleted') continue;
24
+ out[k] = v;
25
+ }
26
+ return out;
27
+ };
28
+ const sa = strip(a), sb = strip(b);
29
+ const ka = Object.keys(sa), kb = Object.keys(sb);
30
+ if (ka.length !== kb.length) return false;
31
+ for (const k of ka) {
32
+ if (!Object.prototype.hasOwnProperty.call(sb, k)) return false;
33
+ const va = sa[k], vb = sb[k];
34
+ if (va === vb) continue;
35
+ if (va === null || va === undefined || vb === null || vb === undefined) return false;
36
+ if (typeof va === 'number' && typeof vb === 'number' && va === vb) continue;
37
+ if (String(va) !== String(vb)) return false;
38
+ }
39
+ return true;
40
+ }
41
+
13
42
  /**
14
43
  * 序列化并给出可读错误。直接 JSON.stringify 遇到循环引用会抛
15
44
  * "Converting circular structure to JSON",指向不到具体列,这里补上表/列信息。
@@ -434,7 +463,21 @@ class JSQL {
434
463
  if (Array.isArray(r) && r.length > 0) return r[0].id;
435
464
  return null;
436
465
  }
437
- return Number(id);
466
+ // 无主键表:id 应是 1-based 行号(数字)。
467
+ // 但 sql.js DELETE 对无主键表传的是行对象(find() 返回的浅拷贝),
468
+ // Number(obj) = NaN → JSON.stringify([NaN]) = "[null]" → 静默失效。
469
+ // 这里对对象 id 做深度相等匹配,找到对应行的序列号。
470
+ if (typeof id === 'number') return id;
471
+ if (id && typeof id === 'object') {
472
+ const all = safeJsonParse(wasmBindings.jsql_find(table, '{}', 1000000, 0));
473
+ if (Array.isArray(all)) {
474
+ for (const r of all) {
475
+ const row = r.fields ? { ...r.fields, id: r.id } : r;
476
+ if (_wasmRowDeepEqual(row, id)) return r.id;
477
+ }
478
+ }
479
+ }
480
+ return null;
438
481
  }
439
482
 
440
483
  async _insertBatch(table, rows) {
package/lib/web_ui.js CHANGED
@@ -116,6 +116,7 @@ class WebUI {
116
116
  this.readonly = !!opts.readonly;
117
117
  this.host = opts.host || '127.0.0.1';
118
118
  this.authToken = opts.authToken || null;
119
+ this.noAuth = !!opts.noAuth; // 显式免认证(仅本地开发/测试用)
119
120
  this.allowOrigin = opts.allowOrigin || null;
120
121
  this.echoOrigin = !opts.allowOrigin && !!this.authToken;
121
122
  this.cache = new Map();
@@ -199,17 +200,22 @@ class WebUI {
199
200
  // API 认证:必须设置 authToken(从环境变量或构造选项)。
200
201
  // 仅当显式 noAuth: true 时允许无认证访问(仅本地开发用)。
201
202
  if (url.pathname.startsWith('/api/')) {
202
- if (!this.noAuth && !this.authToken) {
203
- res.writeHead(401, { 'Content-Type': 'application/json' });
204
- return res.end(JSON.stringify({ ok: false, error: 'API auth not configured' }));
205
- }
203
+ const deny = (msg) => {
204
+ // 显式配置了 CORS(allowOrigin / echoOrigin)时,401 也要带 ACAO,
205
+ // 否则浏览器会把认证失败误判为 CORS 拦截,无法区分两者。
206
+ const headers = { 'Content-Type': 'application/json' };
207
+ if (corsOrigin) {
208
+ headers['Access-Control-Allow-Origin'] = corsOrigin;
209
+ headers['Vary'] = 'Origin';
210
+ }
211
+ res.writeHead(401, headers);
212
+ res.end(JSON.stringify({ ok: false, error: msg }));
213
+ };
214
+ if (!this.noAuth && !this.authToken) return deny('API auth not configured');
206
215
  if (this.authToken) {
207
216
  const provided = req.headers.authorization || '';
208
217
  const token = provided.startsWith('Bearer ') ? provided.slice(7) : '';
209
- if (token !== this.authToken) {
210
- res.writeHead(401, { 'Content-Type': 'application/json' });
211
- return res.end(JSON.stringify({ ok: false, error: 'unauthorized' }));
212
- }
218
+ if (token !== this.authToken) return deny('unauthorized');
213
219
  }
214
220
  }
215
221
  if (url.pathname === '/api/databases' && req.method === 'GET') {
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jsql-neo",
3
- "version": "6.4.0-beta.1",
3
+ "version": "6.4.0-beta.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",
@@ -46,7 +46,7 @@
46
46
  ],
47
47
  "scripts": {
48
48
  "postinstall": "node postinstall.js",
49
- "test": "node test/smoke.js && node test/sql-parser.test.js && node test/readme-audit.test.js && node test/query-opt.test.js && node test/wal-recovery.test.js && node test/mysql-compat-hijack.test.js",
49
+ "test": "node test/smoke.js && node test/sql-parser.test.js && node test/readme-audit.test.js && node test/query-opt.test.js && node test/wal-recovery.test.js && node test/mysql-compat-hijack.test.js && node test/dml-constraints.test.js",
50
50
  "test:parser": "node test/sql-parser.test.js",
51
51
  "test:btree": "node test/btree.test.js",
52
52
  "test:native": "node test/native.test.js",
package/test/coverage.js CHANGED
@@ -91,7 +91,9 @@ function tmp(name) {
91
91
  __meta__: { version: 1 },
92
92
  users: [{ id: 1, name: 'Alice' }, { id: 2, name: 'Bob' }],
93
93
  }));
94
- const ui = new WebUI({ port: 0, dataDir: path.dirname(dbFile) });
94
+ // API 认证模型(3895dca 起):无 authToken 时 /api/* 一律 401。
95
+ // 本测试是功能测试,需要真实访问 API,故显式 noAuth: true。
96
+ const ui = new WebUI({ port: 0, dataDir: path.dirname(dbFile), noAuth: true });
95
97
  const port = await ui.start();
96
98
  const dbs = await (await fetch(`http://127.0.0.1:${port}/api/databases`)).json();
97
99
  ok('webui list databases', dbs.length === 1 && dbs[0].name === 'db');
@@ -198,6 +198,22 @@ async function t(name, fn) {
198
198
  return eq(await scalar(`SELECT v FROM t WHERE id = 1`), 'c');
199
199
  });
200
200
 
201
+ // P2: INSERT 未知列曾静默接受 → 应报 Unknown column
202
+ console.log('\n--- INSERT 未知列应报错 ---');
203
+ await t('INSERT 未知列(指定列清单)→ 报错', async ({ exec, mustFail }) => {
204
+ await exec(`CREATE TABLE t (id INT, v TEXT)`);
205
+ return mustFail(`INSERT INTO t (id, unknown_col) VALUES (1, 'x')`);
206
+ });
207
+ await t('INSERT 未知列(VALUES 全列)→ 报错', async ({ exec, mustFail }) => {
208
+ await exec(`CREATE TABLE t (id INT, v TEXT)`);
209
+ return mustFail(`INSERT INTO t VALUES (1, 'x', 'extra')`);
210
+ });
211
+ await t('INSERT 已知列 → 正常', async ({ exec, scalar }) => {
212
+ await exec(`CREATE TABLE t (id INT, v TEXT)`);
213
+ await exec(`INSERT INTO t (id, v) VALUES (1, 'x')`);
214
+ return eq(await scalar(`SELECT v FROM t WHERE id = 1`), 'x');
215
+ });
216
+
201
217
  console.log(`\n${pass} passed, ${fail} failed`);
202
218
  process.exit(fail === 0 ? 0 : 1);
203
219
  })();