jsql-neo 6.4.0-beta.1 → 6.4.0-beta.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.
- package/README.md +1 -1
- package/index.js +1 -1
- package/lib/database.js +220 -85
- package/lib/native_client.js +75 -32
- package/lib/plugin.js +12 -10
- package/lib/sql.js +88 -22
- package/lib/sqlite_compat.js +2 -1
- package/lib/wasm_client.js +73 -30
- package/lib/web_ui.js +14 -8
- package/native/jsql-neo-native.node +0 -0
- package/package.json +2 -2
- package/test/coverage.js +3 -1
- package/test/dml-constraints.test.js +16 -0
- package/test/query-opt.test.js +8 -8
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.
|
|
6
|
+
> **v6.4.0-beta.10** — official release build · [github.com/vexify-org/JSQL-neo](https://github.com/vexify-org/JSQL-neo) · [jsql.vexify.top](https://jsql.vexify.top)
|
|
7
7
|
|
|
8
8
|

|
|
9
9
|

|
package/index.js
CHANGED
package/lib/database.js
CHANGED
|
@@ -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.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
714
|
-
if (!
|
|
715
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
// 无主键表没有稳定的行标识,
|
|
1871
|
-
//
|
|
1872
|
-
//
|
|
1873
|
-
//
|
|
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
|
|
2001
|
+
const targets = [];
|
|
1878
2002
|
for (const id of ids) {
|
|
1879
|
-
|
|
1880
|
-
|
|
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(
|
|
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.
|
|
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.
|
|
2303
|
+
this._createTableSync(name, schema);
|
|
2169
2304
|
}
|
|
2170
2305
|
}
|
|
2171
2306
|
}
|