jsql-neo 6.4.0-beta.1 → 6.4.0-beta.11

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.11** — 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.11 — 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 = []; // 响应式观察者
@@ -617,13 +652,13 @@ class Database {
617
652
  switch (op.op) {
618
653
  case 'createTable':
619
654
  if (op.table && !this._tables[op.table]) {
620
- this.createTable(op.table, op.schema || {});
655
+ this._createTableSync(op.table, op.schema || {});
621
656
  }
622
657
  return true;
623
658
 
624
659
  case 'dropTable':
625
660
  if (op.table && this._tables[op.table]) {
626
- this.dropTable(op.table);
661
+ this._dropTableSync(op.table);
627
662
  }
628
663
  return true;
629
664
 
@@ -661,22 +696,7 @@ class Database {
661
696
  // 表管理
662
697
  // ============================================================
663
698
 
664
- createTable(name, schema) {
665
- if (!name || name.length > 64 || name === '.' || name === '..' || /[\x00-\x1f/\\]|\.\./.test(name)) {
666
- throw createError('ER_BAD_TABLE_NAME', name);
667
- }
668
- if (typeof schema === 'string') {
669
- const parsed = {};
670
- for (const seg of schema.split(',')) {
671
- const trimmed = seg.trim();
672
- if (!trimmed) continue;
673
- const parts = trimmed.split(/\s+/);
674
- const field = parts.shift();
675
- parsed[field] = parts.join(' ').toLowerCase().trim() || 'string';
676
- }
677
- schema = parsed;
678
- }
679
- if (!this._runHooks('beforeCreateTable', [name, schema])) throw createError('ER_PLUGIN_ABORT', 'createTable aborted by plugin');
699
+ _createTableSync(name, schema) {
680
700
  if (this._tables[name] || (this._dirMode && this._meta.tables[name])) {
681
701
  throw createError('ER_TABLE_EXISTS_ERROR', name);
682
702
  }
@@ -706,13 +726,31 @@ class Database {
706
726
  this._markDirty(name);
707
727
  this._walWrite({ op: 'createTable', table: name, schema });
708
728
  this._emit('createTable', { name, schema });
709
- this._runHooks('afterCreateTable', [name, schema]);
710
729
  return table;
711
730
  }
712
731
 
713
- dropTable(name) {
714
- if (!this._tables[name] && !(this._dirMode && this._meta.tables[name])) throw createError('ER_NO_SUCH_TABLE', name);
715
- if (!this._runHooks('beforeDropTable', [name])) return;
732
+ async createTable(name, schema) {
733
+ if (!name || name.length > 64 || name === '.' || name === '..' || /[\x00-\x1f/\\]|\.\./.test(name)) {
734
+ throw createError('ER_BAD_TABLE_NAME', name);
735
+ }
736
+ if (typeof schema === 'string') {
737
+ const parsed = {};
738
+ for (const seg of schema.split(',')) {
739
+ const trimmed = seg.trim();
740
+ if (!trimmed) continue;
741
+ const parts = trimmed.split(/\s+/);
742
+ const field = parts.shift();
743
+ parsed[field] = parts.join(' ').toLowerCase().trim() || 'string';
744
+ }
745
+ schema = parsed;
746
+ }
747
+ if (!(await this._runHooks('beforeCreateTable', [name, schema]))) throw createError('ER_PLUGIN_ABORT', 'createTable aborted by plugin');
748
+ const table = this._createTableSync(name, schema);
749
+ await this._runHooks('afterCreateTable', [name, schema]);
750
+ return table;
751
+ }
752
+
753
+ _dropTableSync(name) {
716
754
  if (this._tables[name]) {
717
755
  delete this._tables[name];
718
756
  }
@@ -731,7 +769,13 @@ class Database {
731
769
  this._walWrite({ op: 'dropTable', table: name });
732
770
  this._markDirty(name);
733
771
  this._emit('dropTable', { name });
734
- this._runHooks('afterDropTable', [name]);
772
+ }
773
+
774
+ async dropTable(name) {
775
+ if (!this._tables[name] && !(this._dirMode && this._meta.tables[name])) throw createError('ER_NO_SUCH_TABLE', name);
776
+ if (!(await this._runHooks('beforeDropTable', [name]))) return;
777
+ this._dropTableSync(name);
778
+ await this._runHooks('afterDropTable', [name]);
735
779
  }
736
780
 
737
781
  /**
@@ -742,7 +786,7 @@ class Database {
742
786
  * 各自都存了一份。漏掉任何一个都会留下一张「半改名」的表:
743
787
  * 查得到数据却列不出来,或者反过来。
744
788
  */
745
- renameTable(oldName, newName, { ifExists = false } = {}) {
789
+ async renameTable(oldName, newName, { ifExists = false } = {}) {
746
790
  if (oldName === newName) {
747
791
  if (!this._tables[oldName]) {
748
792
  if (ifExists) return false;
@@ -761,7 +805,7 @@ class Database {
761
805
  /[\x00-\x1f/\\]|\.\./.test(newName)) {
762
806
  throw createError('ER_BAD_TABLE_NAME', newName);
763
807
  }
764
- if (!this._runHooks('beforeRenameTable', [oldName, newName])) return false;
808
+ if (!(await this._runHooks('beforeRenameTable', [oldName, newName]))) return false;
765
809
 
766
810
  const table = this._tables[oldName];
767
811
 
@@ -813,7 +857,7 @@ class Database {
813
857
  this._markDirty(newName);
814
858
  this._walWrite({ op: 'renameTable', from: oldName, to: newName });
815
859
  this._emit('renameTable', { from: oldName, to: newName });
816
- this._runHooks('afterRenameTable', [oldName, newName]);
860
+ await this._runHooks('afterRenameTable', [oldName, newName]);
817
861
  return true;
818
862
  }
819
863
 
@@ -1570,6 +1614,80 @@ class Database {
1570
1614
  return buildPluginContext(this, plugin);
1571
1615
  }
1572
1616
 
1617
+ /**
1618
+ * P1 修复:{modules:true} 自动加载插件。
1619
+ * 1. 通过 ModuleManager 读取 ~/.config/jsql/mod.config,应用已启用的模块
1620
+ * 2. 扫描 ~/.jsql/plugins/ 加载 registry 安装的插件
1621
+ */
1622
+ _loadAutoPlugins() {
1623
+ // 1. mod.config 中的模块
1624
+ try {
1625
+ const { ModuleManager } = require('./mod');
1626
+ new ModuleManager().applyTo(this);
1627
+ } catch (e) {
1628
+ if (process.env.JSQL_DEBUG) console.warn('[database] Failed to load mod.config:', e.message);
1629
+ }
1630
+ // 2. ~/.jsql/plugins/ 中的 registry 插件
1631
+ try {
1632
+ this._loadInstalledPlugins();
1633
+ } catch (e) {
1634
+ if (process.env.JSQL_DEBUG) console.warn('[database] Failed to load installed plugins:', e.message);
1635
+ }
1636
+ }
1637
+
1638
+ /**
1639
+ * 扫描 ~/.jsql/plugins/ 目录,加载已安装的 registry 插件。
1640
+ * 兼容两种布局:根级入口文件 + plugins/ 子目录入口文件。
1641
+ */
1642
+ _loadInstalledPlugins() {
1643
+ const os = require('os');
1644
+ const fs = require('fs');
1645
+ const PLUGIN_DIR = path.join(os.homedir(), '.jsql', 'plugins');
1646
+ if (!fs.existsSync(PLUGIN_DIR)) return;
1647
+ const entries = fs.readdirSync(PLUGIN_DIR, { withFileTypes: true });
1648
+ for (const entry of entries) {
1649
+ if (!entry.isDirectory()) continue;
1650
+ const dir = path.join(PLUGIN_DIR, entry.name);
1651
+ const manifestPath = path.join(dir, 'manifest.json');
1652
+ if (!fs.existsSync(manifestPath)) continue;
1653
+ let manifest;
1654
+ try {
1655
+ manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
1656
+ } catch { continue; }
1657
+ if (manifest.disabled) continue;
1658
+ const pluginName = manifest.name || entry.name;
1659
+ // 查找入口文件:manifest.main || manifest.entry || 'index.js'
1660
+ const entryFile = manifest.main || manifest.entry || 'index.js';
1661
+ // 兼容两种路径:根级 + plugins/ 子目录
1662
+ const candidates = [
1663
+ path.join(dir, entryFile),
1664
+ path.join(dir, entryFile + '.js'),
1665
+ path.join(dir, 'plugins', entryFile),
1666
+ path.join(dir, 'plugins', entryFile + '.js'),
1667
+ ];
1668
+ let entryPath = null;
1669
+ for (const c of candidates) {
1670
+ if (fs.existsSync(c) && fs.statSync(c).isFile()) {
1671
+ entryPath = c;
1672
+ break;
1673
+ }
1674
+ }
1675
+ if (!entryPath) continue;
1676
+ try {
1677
+ const resolved = require.resolve(entryPath);
1678
+ delete require.cache[resolved];
1679
+ const plugin = require(resolved);
1680
+ if (typeof plugin === 'function') {
1681
+ this.use({ name: pluginName, install: plugin });
1682
+ } else if (plugin && typeof plugin === 'object') {
1683
+ if (!plugin.name) plugin.name = pluginName;
1684
+ this.use(plugin);
1685
+ }
1686
+ } catch (e) {
1687
+ if (process.env.JSQL_DEBUG) console.warn('[database] Failed to load plugin', entry.name, e.message);
1688
+ }
1689
+ }
1690
+ }n
1573
1691
  on(event, fn) {
1574
1692
  return registerHook(this, event, fn);
1575
1693
  }
@@ -1614,7 +1732,7 @@ class Database {
1614
1732
  }
1615
1733
 
1616
1734
  async start() {
1617
- this._runHooks('onStart', []);
1735
+ await this._runHooks('onStart', []);
1618
1736
  this._emit('start', {});
1619
1737
  if (this._dirMode && !this._sigInstalled) {
1620
1738
  this._sigInstalled = true;
@@ -1663,11 +1781,11 @@ class Database {
1663
1781
  }
1664
1782
  }
1665
1783
 
1666
- insert(tableName, data) {
1784
+ async insert(tableName, data) {
1667
1785
  const table = this._ensureTable(tableName);
1668
1786
  if (!table) throw createError('ER_NO_SUCH_TABLE', tableName);
1669
1787
  const arr = Array.isArray(data) ? data : [data];
1670
- if (!this._runHooks('beforeInsert', [tableName, arr])) return [];
1788
+ if (!(await this._runHooks('beforeInsert', [tableName, arr]))) return [];
1671
1789
  const pk = this._pkOf(table);
1672
1790
  const ids = [];
1673
1791
  const logged = [];
@@ -1682,33 +1800,41 @@ class Database {
1682
1800
  // 有主键才可幂等回放;无主键表回放会重复插行,故不记录
1683
1801
  if (pk) this._walWrite({ op: 'insert', table: tableName, rows: logged });
1684
1802
  this._emit('insert', { table: tableName, count: arr.length, ids });
1685
- this._runHooks('afterInsert', [tableName, arr, ids]);
1803
+ await this._runHooks('afterInsert', [tableName, arr, ids]);
1686
1804
  this._markDirty(tableName);
1687
1805
  return ids;
1688
1806
  }
1689
1807
 
1690
1808
  async flush() {
1691
- if (!this._runHooks('beforeFlush', [])) return;
1809
+ if (!(await this._runHooks('beforeFlush', []))) return;
1692
1810
  if (this._dirMode) {
1693
1811
  this._flushDirty();
1694
1812
  this._saveMeta();
1695
1813
  } else if (!this._memoryMode) {
1696
1814
  this.save();
1697
1815
  }
1698
- this._runHooks('afterFlush', []);
1816
+ await this._runHooks('afterFlush', []);
1699
1817
  return this;
1700
1818
  }
1701
1819
 
1702
1820
  _resolveId(table, id) {
1703
1821
  const pk = this._pkOf(table);
1704
1822
  if (pk) return table._rows.find(r => r[pk] === id) || null;
1705
- return table._rows[id - 1] || null;
1823
+ // 无主键表:id 是行对象(可能是 find() 返回的浅拷贝)时,用内容匹配代替引用比较。
1824
+ // 做法:逐行比对所有字段值是否相等;是数字时当 1-based 行号。
1825
+ if (id && typeof id === 'object') {
1826
+ const keys = Object.keys(id);
1827
+ return table._rows.find(r => keys.every(k => r[k] === id[k])) || null;
1828
+ }
1829
+ const idx = Number(id);
1830
+ if (!isNaN(idx)) return table._rows[idx - 1] || null;
1831
+ return null;
1706
1832
  }
1707
1833
 
1708
- find(tableName, filter, opts = {}) {
1834
+ async find(tableName, filter, opts = {}) {
1709
1835
  const table = this._ensureTable(tableName);
1710
1836
  if (!table) throw createError('ER_NO_SUCH_TABLE', tableName);
1711
- if (!this._runHooks('beforeFind', [tableName, { filter, opts }])) return [];
1837
+ if (!(await this._runHooks('beforeFind', [tableName, { filter, opts }]))) return [];
1712
1838
  const { limit, offset = 0 } = opts;
1713
1839
  // P1-4: 软删除模式下不得泄露已删除行——只看可见行
1714
1840
  let rows = table._applyFilterOptimized(table._getVisibleRows(), filter || {});
@@ -1717,7 +1843,7 @@ class Database {
1717
1843
  // P1-11: 返回浅拷贝,防止调用方修改影响内部数据
1718
1844
  rows = rows.map(r => ({ ...r }));
1719
1845
  this._touchTable(tableName);
1720
- this._runHooks('afterFind', [tableName, { filter, opts }, rows]);
1846
+ await this._runHooks('afterFind', [tableName, { filter, opts }, rows]);
1721
1847
  return rows;
1722
1848
  }
1723
1849
 
@@ -1730,9 +1856,9 @@ class Database {
1730
1856
 
1731
1857
  async query(sql, paramsOrOpts, opts) {
1732
1858
  const { executeSQL } = require('./sql');
1733
- if (!this._runHooks('beforeQuery', [sql, paramsOrOpts, opts])) return null;
1859
+ if (!(await this._runHooks('beforeQuery', [sql, paramsOrOpts, opts]))) return null;
1734
1860
  const result = await executeSQL(this, sql, paramsOrOpts, opts);
1735
- this._runHooks('afterQuery', [sql, paramsOrOpts, opts, result]);
1861
+ await this._runHooks('afterQuery', [sql, paramsOrOpts, opts, result]);
1736
1862
  return result;
1737
1863
  }
1738
1864
 
@@ -1740,28 +1866,28 @@ class Database {
1740
1866
  return require('./prepared').createPrepared(this, sql);
1741
1867
  }
1742
1868
 
1743
- findById(tableName, id) {
1869
+ async findById(tableName, id) {
1744
1870
  const table = this._ensureTable(tableName);
1745
1871
  if (!table) throw createError('ER_NO_SUCH_TABLE', tableName);
1746
- if (!this._runHooks('beforeFind', [tableName, { id }])) return null;
1872
+ if (!(await this._runHooks('beforeFind', [tableName, { id }]))) return null;
1747
1873
  const row = this._resolveId(table, id);
1748
1874
  // P1-4: 软删除行对普通读取不可见
1749
1875
  if (row && row._deleted) return null;
1750
1876
  // P1-11: 返回浅拷贝,防止调用方修改影响内部数据
1751
1877
  const result = row ? { ...row } : null;
1752
1878
  this._touchTable(tableName);
1753
- this._runHooks('afterFind', [tableName, { id }, result]);
1879
+ await this._runHooks('afterFind', [tableName, { id }, result]);
1754
1880
  return result;
1755
1881
  }
1756
1882
 
1757
- findByIds(tableName, ids) {
1883
+ async findByIds(tableName, ids) {
1758
1884
  const table = this._ensureTable(tableName);
1759
1885
  if (!table) throw createError('ER_NO_SUCH_TABLE', tableName);
1760
- if (!this._runHooks('beforeFind', [tableName, { ids }])) return null;
1886
+ if (!(await this._runHooks('beforeFind', [tableName, { ids }]))) return null;
1761
1887
  // P1-4: 过滤掉软删除行;P1-11: 返回浅拷贝
1762
1888
  const rows = ids.map(id => this._resolveId(table, id)).filter(r => r !== null && !r._deleted).map(r => ({ ...r }));
1763
1889
  this._touchTable(tableName);
1764
- this._runHooks('afterFind', [tableName, { ids }, rows]);
1890
+ await this._runHooks('afterFind', [tableName, { ids }, rows]);
1765
1891
  return rows;
1766
1892
  }
1767
1893
 
@@ -1769,21 +1895,21 @@ class Database {
1769
1895
  return this.findByIds(tableName, ids);
1770
1896
  }
1771
1897
 
1772
- count(tableName) {
1898
+ async count(tableName) {
1773
1899
  const table = this._ensureTable(tableName);
1774
1900
  if (!table) throw createError('ER_NO_SUCH_TABLE', tableName);
1775
- this._runHooks('beforeCount', [tableName]);
1901
+ if (!(await this._runHooks('beforeCount', [tableName]))) return undefined;
1776
1902
  // P1-4: 软删除模式下只统计可见行
1777
1903
  const n = table._softDelete ? table._getVisibleRows().length : table._rows.length;
1778
1904
  this._touchTable(tableName);
1779
- this._runHooks('afterCount', [tableName, n]);
1905
+ await this._runHooks('afterCount', [tableName, n]);
1780
1906
  return n;
1781
1907
  }
1782
1908
 
1783
- updateById(tableName, id, data) {
1909
+ async updateById(tableName, id, data) {
1784
1910
  const table = this._ensureTable(tableName);
1785
1911
  if (!table) throw createError('ER_NO_SUCH_TABLE', tableName);
1786
- if (!this._runHooks('beforeUpdate', [tableName, id, data])) return;
1912
+ if (!(await this._runHooks('beforeUpdate', [tableName, id, data]))) return;
1787
1913
  // P1-9: updateById 绕过类型校验——先校验再更新
1788
1914
  const validated = table._validateDataTypes({ ...data }, true);
1789
1915
  const row = this._resolveId(table, id);
@@ -1808,16 +1934,16 @@ class Database {
1808
1934
  Object.assign(row, validated);
1809
1935
  if (pk) this._walWrite({ op: 'update', table: tableName, pk, id, row: { ...validated } });
1810
1936
  this._emit('update', { table: tableName, id, data: validated });
1811
- this._runHooks('afterUpdate', [tableName, id, validated, { ok: true }]);
1937
+ await this._runHooks('afterUpdate', [tableName, id, validated, { ok: true }]);
1812
1938
  this._markDirty(tableName);
1813
1939
  return { ok: true };
1814
1940
  }
1815
1941
 
1816
- updateByIds(tableName, entries) {
1942
+ async updateByIds(tableName, entries) {
1817
1943
  const table = this._ensureTable(tableName);
1818
1944
  if (!table) throw createError('ER_NO_SUCH_TABLE', tableName);
1819
1945
  const pairs = entries.map(([id, data]) => [id, data]);
1820
- if (!this._runHooks('beforeUpdate', [tableName, pairs])) return;
1946
+ if (!(await this._runHooks('beforeUpdate', [tableName, pairs]))) return;
1821
1947
  const pk = this._pkOf(table);
1822
1948
  let count = 0;
1823
1949
  for (const [id, data] of pairs) {
@@ -1829,21 +1955,21 @@ class Database {
1829
1955
  }
1830
1956
  }
1831
1957
  this._emit('update', { table: tableName, entries: pairs, result: { ok: true, count } });
1832
- this._runHooks('afterUpdate', [tableName, pairs, { ok: true, count }]);
1958
+ await this._runHooks('afterUpdate', [tableName, pairs, { ok: true, count }]);
1833
1959
  this._markDirty(tableName);
1834
1960
  return { ok: true, count };
1835
1961
  }
1836
1962
 
1837
- removeById(tableName, id) {
1963
+ async removeById(tableName, id) {
1838
1964
  const table = this._ensureTable(tableName);
1839
1965
  if (!table) throw createError('ER_NO_SUCH_TABLE', tableName);
1840
- if (!this._runHooks('beforeDelete', [tableName, id])) return;
1966
+ if (!(await this._runHooks('beforeDelete', [tableName, id]))) return;
1841
1967
  if (table._primaryKey) {
1842
1968
  const removed = table.removeById(id);
1843
1969
  if (removed === 0) return { ok: false, error: 'not found' };
1844
1970
  this._walWrite({ op: 'remove', table: tableName, pk: table._primaryKey, id });
1845
1971
  this._emit('delete', { table: tableName, id });
1846
- this._runHooks('afterDelete', [tableName, id, { ok: true }]);
1972
+ await this._runHooks('afterDelete', [tableName, id, { ok: true }]);
1847
1973
  this._markDirty(tableName);
1848
1974
  return { ok: true };
1849
1975
  }
@@ -1854,33 +1980,42 @@ class Database {
1854
1980
  table._rebuildAllBTrees();
1855
1981
  for (const field of Object.keys(table._indexes)) table.createIndex(field);
1856
1982
  this._emit('delete', { table: tableName, id });
1857
- this._runHooks('afterDelete', [tableName, id, { ok: true }]);
1983
+ await this._runHooks('afterDelete', [tableName, id, { ok: true }]);
1858
1984
  this._markDirty(tableName);
1859
1985
  return { ok: true };
1860
1986
  }
1861
1987
 
1862
- removeByIds(tableName, ids) {
1988
+ async removeByIds(tableName, ids) {
1863
1989
  const table = this._ensureTable(tableName);
1864
1990
  if (!table) throw createError('ER_NO_SUCH_TABLE', tableName);
1865
- if (!this._runHooks('beforeDelete', [tableName, ids])) return;
1991
+ if (!(await this._runHooks('beforeDelete', [tableName, ids]))) return;
1866
1992
  let removed = 0;
1867
1993
  if (table._primaryKey) {
1868
1994
  for (const id of ids) removed += table.removeById(id);
1869
1995
  } else {
1870
- // 无主键表没有稳定的行标识,_resolveId 只能把 id 当 1-based 行号,
1871
- // 而 _rowPkId 在无主键时回退到 row.id(业务列名)——
1872
- // 两者语义不一致,导致 DELETE ... WHERE id IN (3,5) 删错行。
1873
- // 这里先按「行对象 === id」尝试(SQL 层可以直接传行引用),
1874
- // 退化到行号解析;并一次性收集目标后统一移除,避免边删边移位。
1875
- const targets = [];
1996
+ // 无主键表没有稳定的行标识,P1-11 后 find() 返回浅拷贝({...r}),
1997
+ // 对象引用与 table._rows 里的原对象不同,不能用 Set.has(row) 做引用匹配。
1998
+ // _resolveId 对无主键表做 table._rows[id-1],传入对象时 id-1=NaN → undefined。
1999
+ // 所以这里对对象 id 做深度相等匹配,对值 id 走 _resolveId(1-based 行号)。
1876
2000
  const seen = new Set();
1877
- const push = (row) => { if (row && !seen.has(row)) { seen.add(row); targets.push(row); } };
2001
+ const targets = [];
1878
2002
  for (const id of ids) {
1879
- if (id && typeof id === 'object') { push(id); continue; }
1880
- push(this._resolveId(table, id));
2003
+ let rowIndex;
2004
+ if (id && typeof id === 'object') {
2005
+ // id 是行对象(find() 返回的浅拷贝),用深度相等匹配
2006
+ rowIndex = table._rows.findIndex(r => _rowDeepEqual(r, id));
2007
+ } else {
2008
+ // id 是值(1-based 行号),_resolveId 能处理
2009
+ const original = this._resolveId(table, id);
2010
+ if (original) rowIndex = table._rows.indexOf(original);
2011
+ }
2012
+ if (rowIndex >= 0 && !seen.has(rowIndex)) {
2013
+ seen.add(rowIndex);
2014
+ targets.push(rowIndex);
2015
+ }
1881
2016
  }
1882
2017
  if (targets.length) {
1883
- table._rows = table._rows.filter(r => !seen.has(r));
2018
+ table._rows = table._rows.filter((_, idx) => !seen.has(idx));
1884
2019
  removed = targets.length;
1885
2020
  }
1886
2021
  table._rebuildPKIndex();
@@ -1888,15 +2023,15 @@ class Database {
1888
2023
  for (const field of Object.keys(table._indexes)) table.createIndex(field);
1889
2024
  }
1890
2025
  this._emit('delete', { table: tableName, ids, result: { ok: true, count: removed } });
1891
- this._runHooks('afterDelete', [tableName, ids, { ok: true, count: removed }]);
2026
+ await this._runHooks('afterDelete', [tableName, ids, { ok: true, count: removed }]);
1892
2027
  this._markDirty(tableName);
1893
2028
  return { ok: true, count: removed };
1894
2029
  }
1895
2030
 
1896
- removeWhere(tableName, filter) {
2031
+ async removeWhere(tableName, filter) {
1897
2032
  const table = this._ensureTable(tableName);
1898
2033
  if (!table) throw createError('ER_NO_SUCH_TABLE', tableName);
1899
- if (!this._runHooks('beforeDelete', [tableName, [filter]])) return { ok: true, count: 0 };
2034
+ if (!(await this._runHooks('beforeDelete', [tableName, [filter]]))) return { ok: true, count: 0 };
1900
2035
  const before = table._rows.length;
1901
2036
  // P1-4: 只匹配可见行(软删除行已“不存在”,不应被硬删操作波及)
1902
2037
  const matched = table._applyFilter(table._getVisibleRows(), filter || {});
@@ -1907,31 +2042,31 @@ class Database {
1907
2042
  table._rebuildAllBTrees();
1908
2043
  for (const field of Object.keys(table._indexes)) table.createIndex(field);
1909
2044
  this._emit('delete', { table: tableName, ids: [], result: { ok: true, count: removed } });
1910
- this._runHooks('afterDelete', [tableName, [filter], { ok: true, count: removed }]);
2045
+ await this._runHooks('afterDelete', [tableName, [filter], { ok: true, count: removed }]);
1911
2046
  this._markDirty(tableName);
1912
2047
  return { ok: true, count: removed };
1913
2048
  }
1914
2049
 
1915
- insertMany(tableName, items) {
2050
+ async insertMany(tableName, items) {
1916
2051
  const table = this._ensureTable(tableName);
1917
2052
  if (!table) throw createError('ER_NO_SUCH_TABLE', tableName);
1918
2053
  const arr = Array.isArray(items) ? items : [items];
1919
- if (!this._runHooks('beforeInsert', [tableName, arr])) return [];
2054
+ if (!(await this._runHooks('beforeInsert', [tableName, arr]))) return [];
1920
2055
  const inserted = table.insertMany(arr);
1921
2056
  // P1-6: insertMany 不写 WAL——崩溃恢复时丢失这些操作
1922
2057
  if (table._primaryKey && inserted.length > 0) {
1923
2058
  this._walWrite({ op: 'insert', table: tableName, rows: inserted });
1924
2059
  }
1925
2060
  this._emit('insert', { table: tableName, count: arr.length });
1926
- this._runHooks('afterInsert', [tableName, arr, inserted]);
2061
+ await this._runHooks('afterInsert', [tableName, arr, inserted]);
1927
2062
  this._markDirty(tableName);
1928
2063
  return inserted;
1929
2064
  }
1930
2065
 
1931
- update(tableName, query, updates) {
2066
+ async update(tableName, query, updates) {
1932
2067
  const table = this._ensureTable(tableName);
1933
2068
  if (!table) throw createError('ER_NO_SUCH_TABLE', tableName);
1934
- if (!this._runHooks('beforeUpdate', [tableName, query, updates])) return 0;
2069
+ if (!(await this._runHooks('beforeUpdate', [tableName, query, updates]))) return 0;
1935
2070
  const count = table.update(query, updates);
1936
2071
  // P1-6: update 不写 WAL——崩溃恢复时丢失这些操作
1937
2072
  if (table._primaryKey && count > 0) {
@@ -1942,15 +2077,15 @@ class Database {
1942
2077
  }
1943
2078
  }
1944
2079
  this._emit('update', { table: tableName, query, count });
1945
- this._runHooks('afterUpdate', [tableName, query, updates, count]);
2080
+ await this._runHooks('afterUpdate', [tableName, query, updates, count]);
1946
2081
  this._markDirty(tableName);
1947
2082
  return count;
1948
2083
  }
1949
2084
 
1950
- delete(tableName, query = {}) {
2085
+ async delete(tableName, query = {}) {
1951
2086
  const table = this._ensureTable(tableName);
1952
2087
  if (!table) throw createError('ER_NO_SUCH_TABLE', tableName);
1953
- if (!this._runHooks('beforeDelete', [tableName, query])) return 0;
2088
+ if (!(await this._runHooks('beforeDelete', [tableName, query]))) return 0;
1954
2089
  // P1-6: delete 不写 WAL——崩溃恢复时丢失这些操作
1955
2090
  if (table._primaryKey) {
1956
2091
  const toDelete = table.find(query);
@@ -1961,7 +2096,7 @@ class Database {
1961
2096
  }
1962
2097
  const count = table.remove(query);
1963
2098
  this._emit('delete', { table: tableName, query, count });
1964
- this._runHooks('afterDelete', [tableName, query, count]);
2099
+ await this._runHooks('afterDelete', [tableName, query, count]);
1965
2100
  this._markDirty(tableName);
1966
2101
  return count;
1967
2102
  }
@@ -1984,7 +2119,7 @@ class Database {
1984
2119
  }
1985
2120
 
1986
2121
  async stop() {
1987
- this._runHooks('onStop', []);
2122
+ await this._runHooks('onStop', []);
1988
2123
  this._emit('stop', {});
1989
2124
  // 内存监控定时器:stop() 此前从不清理,重复创建/销毁实例会堆积定时器
1990
2125
  if (this._monitorTimer) { clearInterval(this._monitorTimer); this._monitorTimer = null; }
@@ -2142,7 +2277,7 @@ class Database {
2142
2277
  if (this._jsqlMode) {
2143
2278
  this._jsqlFormat._open();
2144
2279
  for (const [name, t] of this._jsqlFormat.tables) {
2145
- if (!this._tables[name]) this.createTable(name, t.schema);
2280
+ if (!this._tables[name]) this._createTableSync(name, t.schema);
2146
2281
  if (t.rowCount > 0) {
2147
2282
  const { rows } = this._jsqlFormat.readTableSync(name);
2148
2283
  this._tables[name]._loadRows(rows);
@@ -2165,7 +2300,7 @@ class Database {
2165
2300
  if (data.__schema__) {
2166
2301
  for (const [name, schema] of Object.entries(data.__schema__)) {
2167
2302
  if (!this._tables[name]) {
2168
- this.createTable(name, schema);
2303
+ this._createTableSync(name, schema);
2169
2304
  }
2170
2305
  }
2171
2306
  }