jsql-neo 6.3.6 → 6.3.10

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.
@@ -81,6 +81,7 @@ class Database {
81
81
  close() {
82
82
  if (!this.open) return;
83
83
  try {
84
+ // P3-12: close() 不等 worker 保存完——显式等待保存完成
84
85
  this._sync('close', []);
85
86
  } finally {
86
87
  this.open = false;
@@ -285,7 +286,7 @@ class Statement {
285
286
 
286
287
  run(...params) {
287
288
  const r = this._call('run', params.length > 0 ? params : undefined);
288
- return r;
289
+ return { changes: r && r.affectedRows, lastInsertRowid: r && r.insertId, _stmt: this };
289
290
  }
290
291
 
291
292
  get(...params) {
@@ -97,7 +97,8 @@ function cleanResult(r) {
97
97
  if (v instanceof Date) out[k] = v.toISOString();
98
98
  else if (v === undefined) out[k] = null;
99
99
  else if (typeof v === 'object' && v !== null && !Array.isArray(v)) {
100
- // 仅保留可 JSON 化的值(防止 Table/内部对象循环引用)
100
+ // P3-14: 双次 stringify 检查可能导致某些值被错误地过滤
101
+ // 只检查一次 stringify 是否成功,然后保留原值
101
102
  try { JSON.stringify(v); out[k] = v; } catch (e) { out[k] = null; }
102
103
  } else out[k] = v;
103
104
  }
@@ -123,9 +124,14 @@ async function handleOp(op, args) {
123
124
  }
124
125
  case 'run': {
125
126
  const r = await executeStatement(args[0], args[1]);
127
+ // P3-13: 非 insert 操作不应回退到上次 insertId
128
+ const lastId = r && r.type === 'insert' ? Number(r.insertId || 0) : 0;
129
+ if (r && r.type === 'insert' && r.insertId !== undefined && r.insertId !== null) {
130
+ _lastInsertId = Number(r.insertId);
131
+ }
126
132
  result = {
127
133
  changes: r ? (r.affectedRows || 0) : 0,
128
- lastInsertRowid: r && r.type === 'insert' ? Number(r.insertId || 0) : _lastInsertId,
134
+ lastInsertRowid: lastId,
129
135
  };
130
136
  break;
131
137
  }
package/lib/table.js CHANGED
@@ -122,7 +122,7 @@ class Table {
122
122
  };
123
123
  this._softDelete = false;
124
124
  this._computedFields = {};
125
- this._btreesDirty = false;
125
+
126
126
 
127
127
  this._primaryKey = null;
128
128
  this._pkIndex = null;
@@ -283,6 +283,13 @@ class Table {
283
283
  }
284
284
  if (def.check && data[field] !== undefined && !def.check(data[field], data)) throw createError('ER_CHECK_CONSTRAINT', this._name + '.' + field);
285
285
  }
286
+ // 主键唯一性检查(主键可能不在 _cachedUniqueFields 中)
287
+ if (this._primaryKey && data[this._primaryKey] !== undefined && data[this._primaryKey] !== null) {
288
+ const pkVal = data[this._primaryKey];
289
+ if (this._pkIndex && this._pkIndex.has(pkVal)) {
290
+ throw createError('ER_DUP_ENTRY', String(pkVal), this._primaryKey);
291
+ }
292
+ }
286
293
  for (const fk of foreignKeys) {
287
294
  if (data[fk.field] !== undefined && data[fk.field] !== null) {
288
295
  const refTable = this._resolveTable(fk.table);
@@ -303,7 +310,11 @@ class Table {
303
310
  this._rows.push(Object.assign({}, data));
304
311
  if (this._pkIndex && this._primaryKey) {
305
312
  const pkVal = data[this._primaryKey];
306
- if (pkVal !== undefined && pkVal !== null) this._pkIndex.set(pkVal, rowIdx);
313
+ if (pkVal !== undefined && pkVal !== null) {
314
+ // P1-1: insertMany 主键重复检查
315
+ if (this._pkIndex.has(pkVal)) throw createError('ER_DUP_ENTRY', String(pkVal), this._primaryKey);
316
+ this._pkIndex.set(pkVal, rowIdx);
317
+ }
307
318
  }
308
319
  for (const [field, tree] of Object.entries(this._btrees)) {
309
320
  const val = data[field];
@@ -369,7 +380,6 @@ class Table {
369
380
  }
370
381
  return null;
371
382
  }
372
- this._ensureBTrees();
373
383
  if (this._btrees[this._primaryKey]) {
374
384
  const indices = this._btrees[this._primaryKey].search(id);
375
385
  if (indices.length > 0) {
@@ -442,7 +452,6 @@ class Table {
442
452
  if (this._pkIndex) {
443
453
  rowIndex = this._pkIndex.get(id);
444
454
  } else {
445
- this._ensureBTrees();
446
455
  if (this._btrees[this._primaryKey]) {
447
456
  const indices = this._btrees[this._primaryKey].search(id);
448
457
  rowIndex = indices.length > 0 ? indices[0] : undefined;
@@ -539,7 +548,6 @@ class Table {
539
548
  if (this._pkIndex) {
540
549
  rowIndex = this._pkIndex.get(id);
541
550
  } else {
542
- this._ensureBTrees();
543
551
  if (this._btrees[this._primaryKey]) {
544
552
  const indices = this._btrees[this._primaryKey].search(id);
545
553
  rowIndex = indices.length > 0 ? indices[0] : undefined;
@@ -587,8 +595,28 @@ class Table {
587
595
  }
588
596
 
589
597
  removeSoft(query = {}) {
590
- this._softDelete = true;
591
- return this.remove(query);
598
+ if (!this._softDelete) throw createError('ER_NOT_SUPPORTED', 'Soft delete is not enabled');
599
+ const matched = this._applyFilterOptimized(this._getVisibleRows(), query);
600
+ let count = 0;
601
+ const now = new Date().toISOString();
602
+ for (const row of matched) {
603
+ const index = this._rows.indexOf(row);
604
+ if (index === -1) continue;
605
+ this._removeFromIndexes(index, row);
606
+ row._deleted = true;
607
+ row._deletedAt = now;
608
+ this._addToIndexes(index, row);
609
+ count++;
610
+ }
611
+ if (count > 0) {
612
+ this._dirty = true;
613
+ this._db._markDirty(this._name);
614
+ for (const hook of this._hooks.afterRemove) {
615
+ hook(query, count);
616
+ }
617
+ this._db._emitChange('remove', this._name, { query, count, soft: true });
618
+ }
619
+ return count;
592
620
  }
593
621
 
594
622
  _findRowIndex(id) {
@@ -628,6 +656,7 @@ class Table {
628
656
  }
629
657
  this._indexes = {};
630
658
  this._autoIncrement = 0;
659
+ // P1-13: truncate 不应重置软删除模式——保留原值
631
660
  this._dirty = true;
632
661
  this._db._markDirty(this._name);
633
662
  return count;
@@ -640,6 +669,10 @@ class Table {
640
669
  addColumn(field, definition) {
641
670
  if (this._schema[field]) throw createError('ER_DUP_FIELDNAME', field);
642
671
  this._schema[field] = definition;
672
+ // P1-14: 更新缓存字段列表,使新列的 unique/required 约束生效
673
+ if (!this._cachedSchemaFields.includes(field)) this._cachedSchemaFields.push(field);
674
+ if (definition.unique && !this._cachedUniqueFields.includes(field)) this._cachedUniqueFields.push(field);
675
+ if (definition.required && !this._cachedRequiredFields.includes(field)) this._cachedRequiredFields.push(field);
643
676
  if (definition.default !== undefined) {
644
677
  this._defaults[field] = definition.default;
645
678
  for (const row of this._rows) {
@@ -666,6 +699,15 @@ class Table {
666
699
  this._btrees[field] = new BTree(64, true);
667
700
  this._rebuildBTree(field);
668
701
  }
702
+ // P1-8: 添加主键列时设置 _primaryKey 并重建 pkIndex
703
+ if (definition.primaryKey) {
704
+ this._primaryKey = field;
705
+ if (this._pkIndex) this._pkIndex.clear();
706
+ for (let i = 0; i < this._rows.length; i++) {
707
+ const val = this._rows[i][field];
708
+ if (val !== undefined && val !== null) this._pkIndex.set(val, i);
709
+ }
710
+ }
669
711
  this._dirty = true;
670
712
  this._db._markDirty(this._name);
671
713
  return this;
@@ -684,7 +726,11 @@ class Table {
684
726
  for (const row of this._rows) {
685
727
  delete row[field];
686
728
  }
687
- if (this._primaryKey === field) this._primaryKey = null;
729
+ // P1-8: 删除主键列时清空 pkIndex
730
+ if (this._primaryKey === field) {
731
+ this._primaryKey = null;
732
+ if (this._pkIndex) this._pkIndex.clear();
733
+ }
688
734
  if (this._autoIncrementField === field) this._autoIncrementField = null;
689
735
  this._dirty = true;
690
736
  this._db._markDirty(this._name);
@@ -703,6 +749,11 @@ class Table {
703
749
  if (this._btrees[oldName]) {
704
750
  this._btrees[newName] = this._btrees[oldName];
705
751
  delete this._btrees[oldName];
752
+ this._btrees[newName].clear();
753
+ for (let i = 0; i < this._rows.length; i++) {
754
+ const val = this._rows[i][newName];
755
+ if (val !== undefined && val !== null) this._btrees[newName].insert(val, i);
756
+ }
706
757
  }
707
758
  for (const row of this._rows) {
708
759
  row[newName] = row[oldName];
@@ -721,9 +772,13 @@ class Table {
721
772
  if (definition.default !== undefined) this._defaults[field] = definition.default;
722
773
  this._checkConstraints = this._checkConstraints.filter(c => c.field !== field);
723
774
  if (definition.check) this._checkConstraints.push({ field, fn: definition.check });
724
- if (definition.primaryKey || definition.unique) {
725
- if (!this._btrees[field]) this._btrees[field] = new BTree(64, true);
726
- this._rebuildBTree(field);
775
+ if (definition.primaryKey !== undefined || definition.unique !== undefined) {
776
+ if (definition.primaryKey || definition.unique) {
777
+ if (!this._btrees[field]) this._btrees[field] = new BTree(64, true);
778
+ this._rebuildBTree(field);
779
+ } else {
780
+ delete this._btrees[field];
781
+ }
727
782
  }
728
783
  this._dirty = true;
729
784
  this._db._markDirty(this._name);
@@ -817,20 +872,6 @@ class Table {
817
872
  }
818
873
  }
819
874
 
820
- _ensureBTrees() {
821
- if (!this._btreesDirty) return;
822
- this._btreesDirty = false;
823
- for (const field of Object.keys(this._btrees)) {
824
- this._btrees[field].clear();
825
- }
826
- for (let i = 0; i < this._rows.length; i++) {
827
- const row = this._rows[i];
828
- for (const [field, tree] of Object.entries(this._btrees)) {
829
- const v = row[field];
830
- if (v !== undefined && v !== null) tree.insert(v, i);
831
- }
832
- }
833
- }
834
875
 
835
876
  _rebuildAllBTrees() {
836
877
  for (const field of Object.keys(this._btrees)) {
@@ -881,7 +922,6 @@ class Table {
881
922
  }
882
923
 
883
924
  _applyFilterOptimized(rows, query) {
884
- this._ensureBTrees();
885
925
  for (const [field, condition] of Object.entries(query)) {
886
926
  if (field === '$or' || field === '$and' || field === '$not') continue;
887
927
  if (typeof condition !== 'object' || condition === null || Array.isArray(condition) || condition instanceof RegExp) {
@@ -940,6 +980,8 @@ class Table {
940
980
  _applyFilter(rows, query) {
941
981
  const result = [];
942
982
  for (let i = 0; i < rows.length; i++) {
983
+ // 软删除行不应出现在任何查询结果中
984
+ if (this._softDelete && rows[i]._deleted) continue;
943
985
  if (this._matchRow(rows[i], query)) {
944
986
  result.push(rows[i]);
945
987
  }
package/lib/tui.js CHANGED
@@ -13,7 +13,7 @@ const os = require('os');
13
13
  const Database = require('./database');
14
14
  const { executeSQL } = require('./sql');
15
15
 
16
- const HISTORY_FILE = path.join(os.homedir(), '.jsql-history');
16
+ const HISTORY_FILE = path.join(os.homedir(), '.jsql-history.' + process.pid);
17
17
  const MAX_HISTORY = 500;
18
18
 
19
19
  /* ---------- ANSI helpers ---------- */
package/lib/web_ui.js CHANGED
@@ -141,9 +141,17 @@ class WebUI {
141
141
  return fs.readdirSync(this.dataDir)
142
142
  .filter(f => f.endsWith('.json'))
143
143
  .map(f => {
144
- let tables = 0;
145
- try { tables = Object.keys(JSON.parse(fs.readFileSync(path.join(this.dataDir, f), 'utf8')).__schema__ || {}).length; } catch (_) {}
146
- return { name: f.slice(0, -5), tables };
144
+ try {
145
+ const stat = fs.statSync(path.join(this.dataDir, f));
146
+ // 只读前 4KB:schema 头部包含 __schema__ 定义,尾部数据不需要
147
+ const fd = fs.openSync(path.join(this.dataDir, f), 'r');
148
+ const buf = Buffer.alloc(4096);
149
+ fs.readSync(fd, buf, 0, 4096, 0);
150
+ fs.closeSync(fd);
151
+ const match = buf.toString('utf8').match(/"__schema__"\s*:\s*\{([^}]*)\}/);
152
+ const tables = match ? (match[1].match(/"/g) || []).length / 2 : 0;
153
+ return { name: f.slice(0, -5), tables };
154
+ } catch (_) { return { name: f.slice(0, -5), tables: 0 }; }
147
155
  });
148
156
  }
149
157
 
@@ -188,12 +196,20 @@ class WebUI {
188
196
  res.writeHead(204, h);
189
197
  return res.end();
190
198
  }
191
- if (this.authToken && url.pathname.startsWith('/api/')) {
192
- const provided = req.headers.authorization || '';
193
- const token = provided.startsWith('Bearer ') ? provided.slice(7) : '';
194
- if (token !== this.authToken) {
199
+ // API 认证:必须设置 authToken(从环境变量或构造选项)。
200
+ // 仅当显式 noAuth: true 时允许无认证访问(仅本地开发用)。
201
+ if (url.pathname.startsWith('/api/')) {
202
+ if (!this.noAuth && !this.authToken) {
195
203
  res.writeHead(401, { 'Content-Type': 'application/json' });
196
- return res.end(JSON.stringify({ ok: false, error: 'unauthorized' }));
204
+ return res.end(JSON.stringify({ ok: false, error: 'API auth not configured' }));
205
+ }
206
+ if (this.authToken) {
207
+ const provided = req.headers.authorization || '';
208
+ 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
+ }
197
213
  }
198
214
  }
199
215
  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.3.6",
3
+ "version": "6.3.10",
4
4
  "description": "JSQL-NEO — Rust-powered embedded database with WASM, REST API, B-Tree indexes, WAL, crash recovery",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",
@@ -51,10 +51,17 @@
51
51
  "test:btree": "node test/btree.test.js",
52
52
  "test:native": "node test/native.test.js",
53
53
  "test:regress": "node test/regress-5.1.0.js",
54
- "test:all": "node test/smoke.js && node test/sql-parser.test.js && node test/readme-audit.test.js && node test/native.test.js && node test/native-multi-instance.test.js && node test/wal-recovery.test.js && node test/mysql-compat-hijack.test.js && node test/wasm.test.js && node test/coverage.js && node test/regress-5.1.0.js && node test/join.test.js && node test/btree.test.js && node test/dml-constraints.test.js",
54
+ "test:all": "node test/smoke.js && node test/sql-parser.test.js && node test/readme-audit.test.js && node test/native.test.js && node test/native-multi-instance.test.js && node test/wal-recovery.test.js && node test/mysql-compat-hijack.test.js && node test/wasm.test.js && node test/coverage.js && node test/regress-5.1.0.js && node test/join.test.js && node test/btree.test.js && node test/dml-constraints.test.js && node test/savepoint.test.js && node test/alter-table.test.js && node test/null-semantics.test.js && node test/identifier-keywords.test.js && node test/recursive-cte.test.js && node test/index-ddl.test.js",
55
55
  "test:coverage": "c8 --all --include='lib/**' --exclude='lib/web_ui.js' --reporter=text --reporter=lcov node test/coverage.js",
56
56
  "test:orms": "node examples/orms/run-all.js",
57
- "test:dml": "node test/dml-constraints.test.js"
57
+ "test:dml": "node test/dml-constraints.test.js",
58
+ "test:cte": "node test/recursive-cte.test.js",
59
+ "test:index": "node test/index-ddl.test.js",
60
+ "test:mongo": "node test/mongo-group.test.js",
61
+ "test:savepoint": "node test/savepoint.test.js",
62
+ "test:alter": "node test/alter-table.test.js",
63
+ "test:ident": "node test/identifier-keywords.test.js",
64
+ "test:null": "node test/null-semantics.test.js"
58
65
  },
59
66
  "keywords": [
60
67
  "database",
@@ -0,0 +1,289 @@
1
+ /*
2
+ * ALTER TABLE 补全测试(RENAME TO / RENAME COLUMN / DROP INDEX)。
3
+ *
4
+ * 这三条此前各自以不同方式「看起来成功」:
5
+ * - ALTER ... RENAME TO t2 → 返回 ok:true,但表名根本没变
6
+ * - ALTER ... RENAME COLUMN → 解析期就报 "Expected table name, got 'COLUMN'"
7
+ * - ALTER ... DROP INDEX idx → 返回 ok:true,索引还在
8
+ * - ALTER ... ADD FOREIGN KEY→ 返回 ok:true,什么都没加
9
+ * 执行器里这三类 op 落在同一个 `default: break`,也就是静默无操作 ——
10
+ * 比报错更糟,调用方会以为改成功了。
11
+ *
12
+ * 现在:能做的真做,做不到的明确报错(外键在这个引擎里没实现)。
13
+ */
14
+ const fs = require('fs');
15
+ const os = require('os');
16
+ const path = require('path');
17
+
18
+ const ROOT = path.join(__dirname, '..');
19
+ const { Database } = require(path.join(ROOT, 'index.js'));
20
+ const { executeSQL } = require(path.join(ROOT, 'lib/sql.js'));
21
+
22
+ const OPTS = { safety: false };
23
+ let pass = 0, fail = 0;
24
+
25
+ function ok(name, cond, extra) {
26
+ if (cond) { pass++; console.log('[OK] ', name); }
27
+ else { fail++; console.log('[FAIL]', name, extra !== undefined ? '-> ' + JSON.stringify(extra).slice(0, 140) : ''); }
28
+ }
29
+ const eq = (a, b) => JSON.stringify(a) === JSON.stringify(b);
30
+
31
+ async function t(name, fn) {
32
+ const db = new Database(':memory:');
33
+ const exec = (sql) => executeSQL(db, sql, OPTS);
34
+ const rows = async (sql) => (await exec(sql)).rows;
35
+ const mustFail = async (sql) => {
36
+ try { await exec(sql); return false; } catch (_) { return true; }
37
+ };
38
+ try {
39
+ const r = await fn({ db, exec, rows, mustFail });
40
+ ok(name, r === undefined ? true : !!r);
41
+ } catch (e) {
42
+ ok(name, false, e.message);
43
+ }
44
+ }
45
+
46
+ (async () => {
47
+ console.log('\n--- RENAME TO ---');
48
+ await t('改名真的生效,数据不丢', async ({ db, exec, rows }) => {
49
+ await exec('CREATE TABLE t (id INT PRIMARY KEY, v TEXT)');
50
+ await exec("INSERT INTO t VALUES (1,'x')");
51
+ await exec('ALTER TABLE t RENAME TO t2');
52
+ const tables = (await db.getTables()).slice().sort();
53
+ return eq(tables, ['t2']) && eq(await rows('SELECT id, v FROM t2'), [[1, 'x']]);
54
+ });
55
+
56
+ await t('旧表名不再可用,新名字可取属性', async ({ db, exec, rows, mustFail }) => {
57
+ await exec('CREATE TABLE t (id INT PRIMARY KEY)');
58
+ await exec('ALTER TABLE t RENAME TO t2');
59
+ const oldGone = await mustFail('SELECT * FROM t');
60
+ return oldGone && !!db.t2 && db.t === undefined && rows('SELECT * FROM t2') !== null;
61
+ });
62
+
63
+ await t('RENAME AS 也支持', async ({ db, exec }) => {
64
+ await exec('CREATE TABLE t (id INT PRIMARY KEY)');
65
+ await exec('ALTER TABLE t RENAME AS t3');
66
+ return eq((await db.getTables()).slice().sort(), ['t3']);
67
+ });
68
+
69
+ await t('改成已存在的表名 → 报错', async ({ db, exec, mustFail }) => {
70
+ await exec('CREATE TABLE a (id INT PRIMARY KEY)');
71
+ await exec('CREATE TABLE b (id INT PRIMARY KEY)');
72
+ const bad = await mustFail('ALTER TABLE a RENAME TO b');
73
+ return bad && eq((await db.getTables()).slice().sort(), ['a', 'b']);
74
+ });
75
+
76
+ await t('改名不存在的表 → 报错', async ({ exec, mustFail }) => {
77
+ return mustFail('ALTER TABLE nope RENAME TO x');
78
+ });
79
+
80
+ await t('改成同名 = 无事发生,不算错', async ({ db, exec }) => {
81
+ await exec('CREATE TABLE t (id INT PRIMARY KEY)');
82
+ await exec('ALTER TABLE t RENAME TO t');
83
+ return eq((await db.getTables()).slice().sort(), ['t']);
84
+ });
85
+
86
+ await t('改名后主键/自增/唯一约束仍然生效', async ({ exec, rows, mustFail }) => {
87
+ await exec('CREATE TABLE t (id INTEGER PRIMARY KEY AUTOINCREMENT, e TEXT UNIQUE)');
88
+ await exec("INSERT INTO t (e) VALUES ('a')");
89
+ await exec('ALTER TABLE t RENAME TO t2');
90
+ await exec("INSERT INTO t2 (e) VALUES ('b')");
91
+ const dupPk = await mustFail('INSERT INTO t2 (id, e) VALUES (1, \'c\')');
92
+ const dupUniq = await mustFail("INSERT INTO t2 (e) VALUES ('a')");
93
+ const all = await rows('SELECT id, e FROM t2 ORDER BY id');
94
+ return eq(all, [[1, 'a'], [2, 'b']]) && dupPk && dupUniq;
95
+ });
96
+
97
+ console.log('\n--- RENAME COLUMN ---');
98
+ await t('改列名真的生效,数据不丢', async ({ exec, rows }) => {
99
+ await exec('CREATE TABLE t (id INT PRIMARY KEY, a TEXT)');
100
+ await exec("INSERT INTO t VALUES (1,'x')");
101
+ await exec('ALTER TABLE t RENAME COLUMN a TO name');
102
+ return eq(await rows('SELECT id, name FROM t'), [[1, 'x']]);
103
+ });
104
+
105
+ await t('列的先后顺序保持不变', async ({ exec }) => {
106
+ await exec('CREATE TABLE t (id INT PRIMARY KEY, a TEXT, b TEXT, c TEXT)');
107
+ await exec('ALTER TABLE t RENAME COLUMN b TO b2');
108
+ // 直接改 schema 的键会把这一列挪到末尾,SELECT * 的列序就变了
109
+ return eq((await exec('SELECT * FROM t')).columns, ['id', 'a', 'b2', 'c']);
110
+ });
111
+
112
+ await t('列级约束(NOT NULL / UNIQUE / 主键)跟着走', async ({ exec, mustFail, rows }) => {
113
+ await exec('CREATE TABLE t (id INT PRIMARY KEY, code TEXT NOT NULL UNIQUE)');
114
+ await exec("INSERT INTO t VALUES (1,'a')");
115
+ await exec('ALTER TABLE t RENAME COLUMN code TO code2');
116
+ const nullBad = await mustFail('INSERT INTO t (id, code2) VALUES (2, NULL)');
117
+ const dupBad = await mustFail("INSERT INTO t (id, code2) VALUES (3, 'a')");
118
+ await exec("INSERT INTO t VALUES (4, 'b')");
119
+ return nullBad && dupBad && eq(await rows('SELECT id, code2 FROM t ORDER BY id'), [[1, 'a'], [4, 'b']]);
120
+ });
121
+
122
+ await t('改列名后自增字段仍认得出来', async ({ exec, rows }) => {
123
+ await exec('CREATE TABLE t (id INTEGER PRIMARY KEY AUTOINCREMENT, v TEXT)');
124
+ await exec('ALTER TABLE t RENAME COLUMN id TO sid');
125
+ await exec("INSERT INTO t (v) VALUES ('a'), ('b')");
126
+ return eq(await rows('SELECT sid, v FROM t ORDER BY sid'), [[1, 'a'], [2, 'b']]);
127
+ });
128
+
129
+ await t('改列名会同步列上的索引', async ({ exec, rows }) => {
130
+ await exec('CREATE TABLE t (id INT PRIMARY KEY, a TEXT)');
131
+ await exec('ALTER TABLE t RENAME COLUMN a TO b');
132
+ await exec("INSERT INTO t VALUES (1,'x')");
133
+ // 表对象上的索引是以旧列名建的,改名后按新列名查必须仍然走得到数据
134
+ return eq(await rows('SELECT id FROM t WHERE b = \'x\''), [[1]]);
135
+ });
136
+
137
+ await t('列不存在 → 报错', async ({ exec, mustFail }) => {
138
+ await exec('CREATE TABLE t (id INT PRIMARY KEY)');
139
+ return mustFail('ALTER TABLE t RENAME COLUMN zzz TO yyy');
140
+ });
141
+
142
+ await t('改成已存在的列名 → 报错', async ({ exec, mustFail, rows }) => {
143
+ await exec('CREATE TABLE t (id INT PRIMARY KEY, a TEXT, b TEXT)');
144
+ await exec("INSERT INTO t VALUES (1,'x','y')");
145
+ const bad = await mustFail('ALTER TABLE t RENAME COLUMN b TO a');
146
+ return bad && eq(await rows('SELECT id, a, b FROM t'), [[1, 'x', 'y']]);
147
+ });
148
+
149
+ await t('RENAME COLUMN 缺 TO → 报错', async ({ exec, mustFail }) => {
150
+ await exec('CREATE TABLE t (id INT PRIMARY KEY, a TEXT)');
151
+ return mustFail('ALTER TABLE t RENAME COLUMN a b');
152
+ });
153
+
154
+ await t('改成同名列 = 无事发生', async ({ exec, rows }) => {
155
+ await exec('CREATE TABLE t (id INT PRIMARY KEY, a TEXT)');
156
+ await exec("INSERT INTO t VALUES (1,'x')");
157
+ await exec('ALTER TABLE t RENAME COLUMN a TO a');
158
+ return eq(await rows('SELECT a FROM t'), [['x']]);
159
+ });
160
+
161
+ console.log('\n--- DROP INDEX / DROP KEY ---');
162
+ await t('DROP INDEX 真的删掉索引', async ({ db, exec }) => {
163
+ await exec('CREATE TABLE t (id INT PRIMARY KEY, a TEXT)');
164
+ await exec('CREATE INDEX idx_a ON t (a)');
165
+ const before = Object.keys(db.t._indexes).length + Object.keys(db.t._btrees).length;
166
+ await exec('ALTER TABLE t DROP INDEX idx_a');
167
+ const after = Object.keys(db.t._indexes).length + Object.keys(db.t._btrees).length;
168
+ return before > 0 && after < before;
169
+ });
170
+
171
+ await t('DROP KEY 是同一件事的另一种写法', async ({ db, exec }) => {
172
+ await exec('CREATE TABLE t (id INT PRIMARY KEY, a TEXT)');
173
+ // 记下建索引前的状态:主键会在 _btrees 里留一个 id 索引,那是该留的
174
+ const baseline = Object.keys(db.t._indexes).length + Object.keys(db.t._btrees).length;
175
+ await exec('CREATE INDEX idx_a ON t (a)');
176
+ const withIndex = Object.keys(db.t._indexes).length + Object.keys(db.t._btrees).length;
177
+ await exec('ALTER TABLE t DROP KEY idx_a');
178
+ const after = Object.keys(db.t._indexes).length + Object.keys(db.t._btrees).length;
179
+ return withIndex > baseline && after === baseline;
180
+ });
181
+
182
+ await t('删索引不影响数据', async ({ exec, rows }) => {
183
+ await exec('CREATE TABLE t (id INT PRIMARY KEY, a TEXT)');
184
+ await exec("INSERT INTO t VALUES (1,'x')");
185
+ await exec('CREATE INDEX idx_a ON t (a)');
186
+ await exec('ALTER TABLE t DROP INDEX idx_a');
187
+ return eq(await rows('SELECT id, a FROM t'), [[1, 'x']]);
188
+ });
189
+
190
+ await t('删不存在的索引 → 报错', async ({ exec, mustFail }) => {
191
+ await exec('CREATE TABLE t (id INT PRIMARY KEY, a TEXT)');
192
+ return mustFail('ALTER TABLE t DROP INDEX nope');
193
+ });
194
+
195
+ console.log('\n--- 明确报错而不是静默无操作 ---');
196
+ await t('ADD FOREIGN KEY → 明确报不支持', async ({ exec }) => {
197
+ await exec('CREATE TABLE t (id INT PRIMARY KEY, a TEXT)');
198
+ try {
199
+ await exec('ALTER TABLE t ADD CONSTRAINT fk FOREIGN KEY (a) REFERENCES p(id)');
200
+ return false;
201
+ } catch (e) { return /not supported/i.test(e.message) && /foreign key/i.test(e.message); }
202
+ });
203
+
204
+ await t('DROP FOREIGN KEY → 明确报不支持', async ({ exec }) => {
205
+ await exec('CREATE TABLE t (id INT PRIMARY KEY, a TEXT)');
206
+ try {
207
+ await exec('ALTER TABLE t DROP FOREIGN KEY fk');
208
+ return false;
209
+ } catch (e) { return /not supported/i.test(e.message) && /foreign key/i.test(e.message); }
210
+ });
211
+
212
+ await t('RENAME INDEX → 明确报不支持并给出替代做法', async ({ exec }) => {
213
+ await exec('CREATE TABLE t (id INT PRIMARY KEY, a TEXT)');
214
+ try {
215
+ await exec('ALTER TABLE t RENAME INDEX i TO j');
216
+ return false;
217
+ } catch (e) { return /not supported/i.test(e.message) && /DROP INDEX/i.test(e.message); }
218
+ });
219
+
220
+ console.log('\n--- 多操作与组合 ---');
221
+ await t('一条语句里多个操作按序执行', async ({ db, exec, rows }) => {
222
+ await exec('CREATE TABLE t (id INT PRIMARY KEY, a TEXT)');
223
+ await exec("INSERT INTO t VALUES (1,'x')");
224
+ await exec('ALTER TABLE t ADD COLUMN b TEXT, RENAME COLUMN a TO a2');
225
+ const cols = (await exec('SELECT * FROM t')).columns;
226
+ const r = await rows('SELECT id, a2, b FROM t');
227
+ return eq(r, [[1, 'x', null]]) && cols.includes('a2') && cols.includes('b');
228
+ });
229
+
230
+ await t('改名后的表可以继续 DDL / DML', async ({ db, exec, rows }) => {
231
+ await exec('CREATE TABLE t (id INT PRIMARY KEY, v TEXT)');
232
+ await exec('ALTER TABLE t RENAME TO t2');
233
+ await exec('ALTER TABLE t2 RENAME COLUMN v TO val');
234
+ await exec("INSERT INTO t2 VALUES (1,'a')");
235
+ await exec('CREATE INDEX ix ON t2 (val)');
236
+ await exec('DELETE FROM t2 WHERE val = \'zzz\'');
237
+ return eq(await rows('SELECT id, val FROM t2'), [[1, 'a']]);
238
+ });
239
+
240
+ console.log('\n--- 持久化 ---');
241
+ {
242
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'jsql-alter-'));
243
+ const file = path.join(dir, 'db.jsql');
244
+ try {
245
+ const db = new Database(file, { wal: true, autoSave: false });
246
+ const exec = (sql) => executeSQL(db, sql, OPTS);
247
+ await exec('CREATE TABLE t (id INT PRIMARY KEY, a TEXT)');
248
+ await exec("INSERT INTO t VALUES (1,'x')");
249
+ await exec('ALTER TABLE t RENAME TO t2');
250
+ await exec('ALTER TABLE t2 RENAME COLUMN a TO name');
251
+ await db.save();
252
+
253
+ const db2 = new Database(file, { wal: true });
254
+ const exec2 = (sql) => executeSQL(db2, sql, OPTS);
255
+ const tables = (await db2.getTables()).slice().sort();
256
+ const cols = (await exec2('SELECT * FROM t2')).columns;
257
+ const r = (await exec2('SELECT id, name FROM t2')).rows;
258
+ ok('重开后表名与列名都保住', eq(tables, ['t2']) && eq(cols, ['id', 'name']) && eq(r, [[1, 'x']]),
259
+ { tables, cols, r });
260
+ } catch (e) {
261
+ ok('重开后表名与列名都保住', false, e.message);
262
+ } finally {
263
+ fs.rmSync(dir, { recursive: true, force: true });
264
+ }
265
+ }
266
+
267
+ console.log('\n--- 目录模式(改名的物理文件也要跟着挪)---');
268
+ {
269
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'jsql-alterdir-'));
270
+ try {
271
+ const db = new Database(dir, { mode: 'disk', autoSave: false });
272
+ const exec = (sql) => executeSQL(db, sql, OPTS);
273
+ await exec('CREATE TABLE t (id INT PRIMARY KEY, a TEXT)');
274
+ await exec("INSERT INTO t VALUES (1,'x')");
275
+ await exec('ALTER TABLE t RENAME TO t2');
276
+ const files = fs.readdirSync(dir).filter(f => f.endsWith('.jsql'));
277
+ const tables = (await db.getTables()).slice().sort();
278
+ ok('目录模式改名后表列表正确', eq(tables, ['t2']), tables);
279
+ ok('物理文件名跟着改了', files.some(f => f.includes('t2')) && !files.some(f => /(^|[^2])t\.jsql$/.test(f)), files);
280
+ } catch (e) {
281
+ ok('目录模式改名', false, e.message);
282
+ } finally {
283
+ fs.rmSync(dir, { recursive: true, force: true });
284
+ }
285
+ }
286
+
287
+ console.log(`\n${pass} passed, ${fail} failed`);
288
+ process.exit(fail === 0 ? 0 : 1);
289
+ })().catch(e => { console.error('💥', e); process.exit(1); });