feat: v0.1.13 — 事务回滚 + 子查询 + 外键级联 + 连接池
This commit is contained in:
@@ -2,6 +2,38 @@
|
|||||||
|
|
||||||
All notable changes to MetonaSqlark will be documented in this file.
|
All notable changes to MetonaSqlark will be documented in this file.
|
||||||
|
|
||||||
|
## [0.1.13] - 2026-07-26
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- **事务回滚机制**: `IStorageEngine` 新增 `beginTransaction/commitTransaction/rollbackTransaction` 接口
|
||||||
|
- MemoryEngine: 快照式回滚(深拷贝 tables/schemas/indexes)
|
||||||
|
- IndexedDBEngine: 延迟写入策略(事务中仅写内存,commit 批量刷 IDB)
|
||||||
|
- OPFSEngine: 快照式回滚(commit 批量写文件)
|
||||||
|
- HybridEngine: 同时代理内存+磁盘引擎事务
|
||||||
|
- **子查询支持**: SQL Parser + Executor 支持 `IN (SELECT ...)` 和 `op (SELECT ...)` 子查询
|
||||||
|
- AST 新增 `SubqueryExpression` 类型
|
||||||
|
- Parser 在 IN 和比较运算符后检测子查询
|
||||||
|
- Executor 新增 `resolveSubqueries()` 递归解析,自动执行子查询并替换为具体值
|
||||||
|
- **外键级联操作**: ColumnDef 新增 `onDelete/onUpdate` 选项
|
||||||
|
- 支持 `CASCADE`(递归级联删除)、`SET NULL`、`RESTRICT`
|
||||||
|
- SQL Parser 解析 `REFERENCES table(col) ON DELETE CASCADE ON UPDATE CASCADE`
|
||||||
|
- MemoryEngine 内置 `cascadeDelete()` 递归级联逻辑
|
||||||
|
- **连接池管理**: `MetonaSqlark.connect()` 静态方法
|
||||||
|
- 同名数据库复用已打开实例,引用计数管理
|
||||||
|
- `db.disconnect()` 释放连接,归零自动 close
|
||||||
|
- `MetonaSqlark.disconnectAll()` 强制关闭所有连接
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- `TransactionManager.execute()` 调用引擎层 begin/commit/rollback 实现真正原子性
|
||||||
|
- IndexedDBEngine CRUD 事务感知:活跃事务中延迟 IDB 写入
|
||||||
|
- ASTColumnDef / astColumnToColumnDef 支持 references/onDelete/onUpdate 透传
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- IndexedDBEngine 事务中 find/count 从内存缓存读取(保证读到未提交变更)
|
||||||
|
- SQL Parser parseColumnDef 循环条件扩展,避免 REFERENCES 语法解析中断
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## [0.1.12] - 2026-07-26
|
## [0.1.12] - 2026-07-26
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
Vendored
+518
-39
@@ -31,7 +31,7 @@ class DatabaseError extends Error {
|
|||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// 版本
|
// 版本
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
const VERSION = '0.1.12';
|
const VERSION = '0.1.13';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* metona-sqlark Shared WHERE Matcher — 统一的条件匹配逻辑
|
* metona-sqlark Shared WHERE Matcher — 统一的条件匹配逻辑
|
||||||
@@ -200,6 +200,8 @@ class MemoryEngine {
|
|||||||
this.schemas = new Map();
|
this.schemas = new Map();
|
||||||
this.indexes = new Map();
|
this.indexes = new Map();
|
||||||
this.opened = false;
|
this.opened = false;
|
||||||
|
// ---- 事务快照 ----
|
||||||
|
this.snapshot = null;
|
||||||
}
|
}
|
||||||
// ---- 生命周期 ----
|
// ---- 生命周期 ----
|
||||||
async open(_dbName, _version) { this.opened = true; }
|
async open(_dbName, _version) { this.opened = true; }
|
||||||
@@ -293,9 +295,16 @@ class MemoryEngine {
|
|||||||
toDelete.push(pk);
|
toDelete.push(pk);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// 级联删除:检查引用此表的其他表
|
||||||
|
let cascadeCount = 0;
|
||||||
|
for (const pk of toDelete) {
|
||||||
|
const row = table.get(pk);
|
||||||
|
if (row)
|
||||||
|
cascadeCount += await this.cascadeDelete(tableName, pk, row);
|
||||||
|
}
|
||||||
for (const pk of toDelete)
|
for (const pk of toDelete)
|
||||||
table.delete(pk);
|
table.delete(pk);
|
||||||
return toDelete.length;
|
return toDelete.length + cascadeCount;
|
||||||
}
|
}
|
||||||
async count(tableName, query) {
|
async count(tableName, query) {
|
||||||
this.ensureTable(tableName);
|
this.ensureTable(tableName);
|
||||||
@@ -317,6 +326,54 @@ class MemoryEngine {
|
|||||||
for (const colIndex of tableIndexes.values())
|
for (const colIndex of tableIndexes.values())
|
||||||
colIndex.clear();
|
colIndex.clear();
|
||||||
}
|
}
|
||||||
|
// ---- 事务 ----
|
||||||
|
async beginTransaction() {
|
||||||
|
if (this.snapshot)
|
||||||
|
throw new DatabaseError('Transaction already in progress', 'TX_ACTIVE');
|
||||||
|
this.snapshot = {
|
||||||
|
tables: this.deepCloneMapMap(this.tables),
|
||||||
|
schemas: new Map(this.schemas),
|
||||||
|
indexes: this.deepCloneIndexes(this.indexes),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
async commitTransaction() {
|
||||||
|
if (!this.snapshot)
|
||||||
|
throw new DatabaseError('No active transaction', 'TX_NONE');
|
||||||
|
this.snapshot = null;
|
||||||
|
}
|
||||||
|
async rollbackTransaction() {
|
||||||
|
if (!this.snapshot)
|
||||||
|
throw new DatabaseError('No active transaction', 'TX_NONE');
|
||||||
|
this.tables = this.snapshot.tables;
|
||||||
|
this.schemas = this.snapshot.schemas;
|
||||||
|
this.indexes = this.snapshot.indexes;
|
||||||
|
this.snapshot = null;
|
||||||
|
}
|
||||||
|
// ---- 事务快照辅助 ----
|
||||||
|
deepCloneMapMap(source) {
|
||||||
|
const clone = new Map();
|
||||||
|
for (const [k, v] of source) {
|
||||||
|
const innerClone = new Map();
|
||||||
|
for (const [ik, iv] of v)
|
||||||
|
innerClone.set(ik, { ...iv });
|
||||||
|
clone.set(k, innerClone);
|
||||||
|
}
|
||||||
|
return clone;
|
||||||
|
}
|
||||||
|
deepCloneIndexes(source) {
|
||||||
|
const clone = new Map();
|
||||||
|
for (const [tableName, tableIndexes] of source) {
|
||||||
|
const tableClone = new Map();
|
||||||
|
for (const [col, colIndex] of tableIndexes) {
|
||||||
|
const colClone = new Map();
|
||||||
|
for (const [val, pkSet] of colIndex)
|
||||||
|
colClone.set(val, new Set(pkSet));
|
||||||
|
tableClone.set(col, colClone);
|
||||||
|
}
|
||||||
|
clone.set(tableName, tableClone);
|
||||||
|
}
|
||||||
|
return clone;
|
||||||
|
}
|
||||||
// ---- 内部辅助 ----
|
// ---- 内部辅助 ----
|
||||||
ensureTable(tableName) {
|
ensureTable(tableName) {
|
||||||
if (!this.tables.has(tableName))
|
if (!this.tables.has(tableName))
|
||||||
@@ -423,11 +480,62 @@ class MemoryEngine {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// ---- 外键级联 ----
|
||||||
|
/**
|
||||||
|
* 级联删除:查找引用 tableName.pkValue 的所有表的行并删除。
|
||||||
|
* @returns 级联删除的行数
|
||||||
|
*/
|
||||||
|
async cascadeDelete(tableName, pkValue, _row) {
|
||||||
|
let totalCascade = 0;
|
||||||
|
for (const [refTableName, refSchema] of this.schemas) {
|
||||||
|
if (refTableName === tableName)
|
||||||
|
continue;
|
||||||
|
for (const [colName, colDef] of Object.entries(refSchema.columns)) {
|
||||||
|
if (!colDef.references || !colDef.onDelete || colDef.onDelete === 'RESTRICT')
|
||||||
|
continue;
|
||||||
|
const [refTable, refCol] = colDef.references.split('.');
|
||||||
|
if (refTable !== tableName)
|
||||||
|
continue;
|
||||||
|
const refTableData = this.tables.get(refTableName);
|
||||||
|
if (!refTableData)
|
||||||
|
continue;
|
||||||
|
// 查找所有引用此主键的行
|
||||||
|
const toDelete = [];
|
||||||
|
for (const [refPk, refRow] of refTableData) {
|
||||||
|
if (String(refRow[colName]) === pkValue) {
|
||||||
|
toDelete.push(refPk);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (colDef.onDelete === 'CASCADE') {
|
||||||
|
// 递归级联
|
||||||
|
for (const refPk of toDelete) {
|
||||||
|
const refRow = refTableData.get(refPk);
|
||||||
|
if (refRow) {
|
||||||
|
totalCascade += await this.cascadeDelete(refTableName, refPk, refRow);
|
||||||
|
}
|
||||||
|
refTableData.delete(refPk);
|
||||||
|
totalCascade++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (colDef.onDelete === 'SET NULL') {
|
||||||
|
for (const refPk of toDelete) {
|
||||||
|
const refRow = refTableData.get(refPk);
|
||||||
|
if (refRow) {
|
||||||
|
refRow[colName] = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return totalCascade;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* metona-sqlark IndexedDB Engine — 基于 IndexedDB 的持久化存储引擎
|
* metona-sqlark IndexedDB Engine — 基于 IndexedDB 的持久化存储引擎
|
||||||
* @module engine/indexeddb
|
* @module engine/indexeddb
|
||||||
|
*
|
||||||
|
* v0.1.13: 支持事务 — beginTransaction 延迟 IDB 写入,commit 批量刷盘,rollback 恢复快照。
|
||||||
*/
|
*/
|
||||||
class IndexedDBEngine {
|
class IndexedDBEngine {
|
||||||
constructor() {
|
constructor() {
|
||||||
@@ -436,6 +544,8 @@ class IndexedDBEngine {
|
|||||||
this.dbName = '';
|
this.dbName = '';
|
||||||
this.version = 1;
|
this.version = 1;
|
||||||
this.memoryCache = new MemoryEngine();
|
this.memoryCache = new MemoryEngine();
|
||||||
|
// ---- 事务状态 ----
|
||||||
|
this.txActive = false;
|
||||||
}
|
}
|
||||||
async open(dbName, version) {
|
async open(dbName, version) {
|
||||||
this.dbName = dbName;
|
this.dbName = dbName;
|
||||||
@@ -459,6 +569,85 @@ class IndexedDBEngine {
|
|||||||
// ---- 表管理 ----
|
// ---- 表管理 ----
|
||||||
async createTable(schema) {
|
async createTable(schema) {
|
||||||
await this.memoryCache.createTable(schema);
|
await this.memoryCache.createTable(schema);
|
||||||
|
if (this.txActive)
|
||||||
|
return; // 事务中延迟 IDB 操作
|
||||||
|
await this.idbCreateTable(schema);
|
||||||
|
}
|
||||||
|
async dropTable(tableName) {
|
||||||
|
await this.memoryCache.dropTable(tableName);
|
||||||
|
if (this.txActive)
|
||||||
|
return;
|
||||||
|
await this.idbDropTable(tableName);
|
||||||
|
}
|
||||||
|
async hasTable(tableName) { return this.ensureDB().objectStoreNames.contains(tableName); }
|
||||||
|
async getTableNames() { return Array.from(this.ensureDB().objectStoreNames); }
|
||||||
|
async getTableSchema(tableName) { return this.memoryCache.getTableSchema(tableName); }
|
||||||
|
// ---- CRUD ----
|
||||||
|
async insert(tableName, rows) {
|
||||||
|
const pks = await this.memoryCache.insert(tableName, rows);
|
||||||
|
if (this.txActive)
|
||||||
|
return pks; // 事务中延迟写入
|
||||||
|
await this.idbInsert(tableName, rows);
|
||||||
|
return pks;
|
||||||
|
}
|
||||||
|
async find(tableName, query) {
|
||||||
|
// 事务中从内存缓存读取(保证读到未提交的变更),否则走 IDB
|
||||||
|
if (this.txActive)
|
||||||
|
return this.memoryCache.find(tableName, query);
|
||||||
|
return this.idbFind(tableName, query);
|
||||||
|
}
|
||||||
|
async update(tableName, query, updates) {
|
||||||
|
const count = await this.memoryCache.update(tableName, query, updates);
|
||||||
|
if (this.txActive)
|
||||||
|
return count;
|
||||||
|
await this.idbUpdate(tableName, query, updates);
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
async delete(tableName, query) {
|
||||||
|
const count = await this.memoryCache.delete(tableName, query);
|
||||||
|
if (this.txActive)
|
||||||
|
return count;
|
||||||
|
await this.idbDelete(tableName, query);
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
async count(tableName, query) {
|
||||||
|
if (this.txActive)
|
||||||
|
return this.memoryCache.count(tableName, query);
|
||||||
|
const results = await this.idbFind(tableName, { table: tableName, where: query?.where ?? {} });
|
||||||
|
return results.length;
|
||||||
|
}
|
||||||
|
async clear(tableName) {
|
||||||
|
await this.memoryCache.clear(tableName);
|
||||||
|
if (this.txActive)
|
||||||
|
return;
|
||||||
|
await this.idbClear(tableName);
|
||||||
|
}
|
||||||
|
// ---- 事务 ----
|
||||||
|
async beginTransaction() {
|
||||||
|
if (this.txActive)
|
||||||
|
throw new DatabaseError('Transaction already in progress', 'TX_ACTIVE');
|
||||||
|
this.txActive = true;
|
||||||
|
// 保存内存快照到 MemoryEngine 内部的 beginTransaction
|
||||||
|
await this.memoryCache.beginTransaction();
|
||||||
|
}
|
||||||
|
async commitTransaction() {
|
||||||
|
if (!this.txActive)
|
||||||
|
throw new DatabaseError('No active transaction', 'TX_NONE');
|
||||||
|
// 确认内存层的变更
|
||||||
|
await this.memoryCache.commitTransaction();
|
||||||
|
// 批量将内存数据刷到 IndexedDB
|
||||||
|
await this.flushToIDB();
|
||||||
|
this.txActive = false;
|
||||||
|
}
|
||||||
|
async rollbackTransaction() {
|
||||||
|
if (!this.txActive)
|
||||||
|
throw new DatabaseError('No active transaction', 'TX_NONE');
|
||||||
|
// 恢复内存层到快照状态
|
||||||
|
await this.memoryCache.rollbackTransaction();
|
||||||
|
this.txActive = false;
|
||||||
|
}
|
||||||
|
// ---- IDB 原生操作 ----
|
||||||
|
async idbCreateTable(schema) {
|
||||||
const db = this.ensureDB();
|
const db = this.ensureDB();
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const newVersion = db.version + 1;
|
const newVersion = db.version + 1;
|
||||||
@@ -478,8 +667,7 @@ class IndexedDBEngine {
|
|||||||
request.onerror = () => reject(new DatabaseError(`Failed to create table "${schema.name}"`, 'IDB_UPGRADE_ERROR', request.error));
|
request.onerror = () => reject(new DatabaseError(`Failed to create table "${schema.name}"`, 'IDB_UPGRADE_ERROR', request.error));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
async dropTable(tableName) {
|
async idbDropTable(tableName) {
|
||||||
await this.memoryCache.dropTable(tableName);
|
|
||||||
const db = this.ensureDB();
|
const db = this.ensureDB();
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const newVersion = db.version + 1;
|
const newVersion = db.version + 1;
|
||||||
@@ -494,27 +682,18 @@ class IndexedDBEngine {
|
|||||||
request.onerror = () => reject(new DatabaseError(`Failed to drop table "${tableName}"`, 'IDB_UPGRADE_ERROR', request.error));
|
request.onerror = () => reject(new DatabaseError(`Failed to drop table "${tableName}"`, 'IDB_UPGRADE_ERROR', request.error));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
async hasTable(tableName) { return this.ensureDB().objectStoreNames.contains(tableName); }
|
async idbInsert(tableName, rows) {
|
||||||
async getTableNames() { return Array.from(this.ensureDB().objectStoreNames); }
|
|
||||||
async getTableSchema(tableName) { return this.memoryCache.getTableSchema(tableName); }
|
|
||||||
// ---- CRUD ----
|
|
||||||
async insert(tableName, rows) {
|
|
||||||
await this.memoryCache.insert(tableName, rows);
|
|
||||||
const db = this.ensureDB();
|
const db = this.ensureDB();
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const tx = db.transaction(tableName, 'readwrite');
|
const tx = db.transaction(tableName, 'readwrite');
|
||||||
const store = tx.objectStore(tableName);
|
const store = tx.objectStore(tableName);
|
||||||
const pks = [];
|
for (const row of rows)
|
||||||
for (const row of rows) {
|
store.add(row);
|
||||||
const req = store.add(row);
|
tx.oncomplete = () => resolve();
|
||||||
req.onsuccess = () => pks.push(String(req.result));
|
|
||||||
req.onerror = () => { }; // 内存层已校验,忽略 IDB 重复键
|
|
||||||
}
|
|
||||||
tx.oncomplete = () => resolve(pks);
|
|
||||||
tx.onerror = () => reject(new DatabaseError(`Insert failed for "${tableName}"`, 'IDB_TX_ERROR', tx.error));
|
tx.onerror = () => reject(new DatabaseError(`Insert failed for "${tableName}"`, 'IDB_TX_ERROR', tx.error));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
async find(tableName, query) {
|
async idbFind(tableName, query) {
|
||||||
const db = this.ensureDB();
|
const db = this.ensureDB();
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const tx = db.transaction(tableName, 'readonly');
|
const tx = db.transaction(tableName, 'readonly');
|
||||||
@@ -538,29 +717,25 @@ class IndexedDBEngine {
|
|||||||
req.onerror = () => reject(new DatabaseError(`Find failed for "${tableName}"`, 'IDB_READ_ERROR', req.error));
|
req.onerror = () => reject(new DatabaseError(`Find failed for "${tableName}"`, 'IDB_READ_ERROR', req.error));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
async update(tableName, query, updates) {
|
async idbUpdate(tableName, query, updates) {
|
||||||
await this.memoryCache.update(tableName, query, updates);
|
|
||||||
const db = this.ensureDB();
|
const db = this.ensureDB();
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const tx = db.transaction(tableName, 'readwrite');
|
const tx = db.transaction(tableName, 'readwrite');
|
||||||
const store = tx.objectStore(tableName);
|
const store = tx.objectStore(tableName);
|
||||||
const getAllReq = store.getAll();
|
const getAllReq = store.getAll();
|
||||||
let count = 0;
|
|
||||||
getAllReq.onsuccess = () => {
|
getAllReq.onsuccess = () => {
|
||||||
for (const row of getAllReq.result ?? []) {
|
for (const row of getAllReq.result ?? []) {
|
||||||
if (!query.where || Object.keys(query.where).length === 0 || matchWhere(row, query.where)) {
|
if (!query.where || Object.keys(query.where).length === 0 || matchWhere(row, query.where)) {
|
||||||
Object.assign(row, updates);
|
Object.assign(row, updates);
|
||||||
store.put(row);
|
store.put(row);
|
||||||
count++;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
tx.oncomplete = () => resolve(count);
|
tx.oncomplete = () => resolve();
|
||||||
tx.onerror = () => reject(new DatabaseError(`Update failed for "${tableName}"`, 'IDB_TX_ERROR', tx.error));
|
tx.onerror = () => reject(new DatabaseError(`Update failed for "${tableName}"`, 'IDB_TX_ERROR', tx.error));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
async delete(tableName, query) {
|
async idbDelete(tableName, query) {
|
||||||
await this.memoryCache.delete(tableName, query);
|
|
||||||
const db = this.ensureDB();
|
const db = this.ensureDB();
|
||||||
const schema = await this.memoryCache.getTableSchema(tableName);
|
const schema = await this.memoryCache.getTableSchema(tableName);
|
||||||
if (!schema)
|
if (!schema)
|
||||||
@@ -570,25 +745,18 @@ class IndexedDBEngine {
|
|||||||
const tx = db.transaction(tableName, 'readwrite');
|
const tx = db.transaction(tableName, 'readwrite');
|
||||||
const store = tx.objectStore(tableName);
|
const store = tx.objectStore(tableName);
|
||||||
const getAllReq = store.getAll();
|
const getAllReq = store.getAll();
|
||||||
let count = 0;
|
|
||||||
getAllReq.onsuccess = () => {
|
getAllReq.onsuccess = () => {
|
||||||
for (const row of getAllReq.result ?? []) {
|
for (const row of getAllReq.result ?? []) {
|
||||||
if (!query.where || Object.keys(query.where).length === 0 || matchWhere(row, query.where)) {
|
if (!query.where || Object.keys(query.where).length === 0 || matchWhere(row, query.where)) {
|
||||||
store.delete(row[pkColumn]);
|
store.delete(row[pkColumn]);
|
||||||
count++;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
tx.oncomplete = () => resolve(count);
|
tx.oncomplete = () => resolve();
|
||||||
tx.onerror = () => reject(new DatabaseError(`Delete failed for "${tableName}"`, 'IDB_TX_ERROR', tx.error));
|
tx.onerror = () => reject(new DatabaseError(`Delete failed for "${tableName}"`, 'IDB_TX_ERROR', tx.error));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
async count(tableName, query) {
|
async idbClear(tableName) {
|
||||||
const results = await this.find(tableName, { table: tableName, where: query?.where ?? {} });
|
|
||||||
return results.length;
|
|
||||||
}
|
|
||||||
async clear(tableName) {
|
|
||||||
await this.memoryCache.clear(tableName);
|
|
||||||
const db = this.ensureDB();
|
const db = this.ensureDB();
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const tx = db.transaction(tableName, 'readwrite');
|
const tx = db.transaction(tableName, 'readwrite');
|
||||||
@@ -597,6 +765,18 @@ class IndexedDBEngine {
|
|||||||
tx.onerror = () => reject(new DatabaseError(`Clear failed for "${tableName}"`, 'IDB_TX_ERROR', tx.error));
|
tx.onerror = () => reject(new DatabaseError(`Clear failed for "${tableName}"`, 'IDB_TX_ERROR', tx.error));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
/** 将内存缓存中的所有表数据刷新到 IndexedDB */
|
||||||
|
async flushToIDB() {
|
||||||
|
const tableNames = await this.memoryCache.getTableNames();
|
||||||
|
for (const tableName of tableNames) {
|
||||||
|
const rows = await this.memoryCache.find(tableName, { table: tableName });
|
||||||
|
// Clear + bulk insert for simplicity
|
||||||
|
await this.idbClear(tableName);
|
||||||
|
if (rows.length > 0) {
|
||||||
|
await this.idbInsert(tableName, rows);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
ensureDB() {
|
ensureDB() {
|
||||||
if (!this.db)
|
if (!this.db)
|
||||||
throw new DatabaseError('Database not opened', 'DB_NOT_OPEN');
|
throw new DatabaseError('Database not opened', 'DB_NOT_OPEN');
|
||||||
@@ -712,6 +892,22 @@ class OPFSEngine {
|
|||||||
await this.memoryCache.clear(tableName);
|
await this.memoryCache.clear(tableName);
|
||||||
await this.writeTableData(tableName, []);
|
await this.writeTableData(tableName, []);
|
||||||
}
|
}
|
||||||
|
// ---- 事务 ----
|
||||||
|
async beginTransaction() {
|
||||||
|
await this.memoryCache.beginTransaction();
|
||||||
|
}
|
||||||
|
async commitTransaction() {
|
||||||
|
await this.memoryCache.commitTransaction();
|
||||||
|
// 将内存数据刷到 OPFS
|
||||||
|
const tableNames = await this.memoryCache.getTableNames();
|
||||||
|
for (const tableName of tableNames) {
|
||||||
|
const rows = await this.memoryCache.find(tableName, { table: tableName });
|
||||||
|
await this.writeTableData(tableName, rows);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async rollbackTransaction() {
|
||||||
|
await this.memoryCache.rollbackTransaction();
|
||||||
|
}
|
||||||
// ---- 内部辅助 ----
|
// ---- 内部辅助 ----
|
||||||
ensureDir() {
|
ensureDir() {
|
||||||
if (!this.tablesDir) {
|
if (!this.tablesDir) {
|
||||||
@@ -854,6 +1050,19 @@ class HybridEngine {
|
|||||||
await this.memoryEngine.clear(tableName);
|
await this.memoryEngine.clear(tableName);
|
||||||
await this.diskEngine.clear(tableName);
|
await this.diskEngine.clear(tableName);
|
||||||
}
|
}
|
||||||
|
// ---- 事务 ----
|
||||||
|
async beginTransaction() {
|
||||||
|
await this.memoryEngine.beginTransaction();
|
||||||
|
await this.diskEngine.beginTransaction();
|
||||||
|
}
|
||||||
|
async commitTransaction() {
|
||||||
|
await this.memoryEngine.commitTransaction();
|
||||||
|
await this.diskEngine.commitTransaction();
|
||||||
|
}
|
||||||
|
async rollbackTransaction() {
|
||||||
|
await this.memoryEngine.rollbackTransaction();
|
||||||
|
await this.diskEngine.rollbackTransaction();
|
||||||
|
}
|
||||||
// ---- 引擎信息 ----
|
// ---- 引擎信息 ----
|
||||||
/** 获取磁盘引擎类型 */
|
/** 获取磁盘引擎类型 */
|
||||||
getDiskEngineType() {
|
getDiskEngineType() {
|
||||||
@@ -1111,6 +1320,9 @@ function astColumnToColumnDef(astCol) {
|
|||||||
maxLength: astCol.maxLength,
|
maxLength: astCol.maxLength,
|
||||||
min: astCol.min,
|
min: astCol.min,
|
||||||
max: astCol.max,
|
max: astCol.max,
|
||||||
|
references: astCol.references,
|
||||||
|
onDelete: astCol.onDelete,
|
||||||
|
onUpdate: astCol.onUpdate,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1191,6 +1403,10 @@ class QueryExecutor {
|
|||||||
// SELECT
|
// SELECT
|
||||||
// ===================================================================
|
// ===================================================================
|
||||||
async executeSelect(stmt) {
|
async executeSelect(stmt) {
|
||||||
|
// 先解析子查询
|
||||||
|
if (stmt.where && Object.keys(stmt.where).length > 0) {
|
||||||
|
stmt.where = await this.resolveSubqueries(stmt.where);
|
||||||
|
}
|
||||||
const hasGroupBy = !!(stmt.groupBy && stmt.groupBy.length > 0);
|
const hasGroupBy = !!(stmt.groupBy && stmt.groupBy.length > 0);
|
||||||
let rows;
|
let rows;
|
||||||
if (!stmt.joins || stmt.joins.length === 0) {
|
if (!stmt.joins || stmt.joins.length === 0) {
|
||||||
@@ -1371,6 +1587,87 @@ class QueryExecutor {
|
|||||||
return this.engine.dropTable(stmt.name);
|
return this.engine.dropTable(stmt.name);
|
||||||
}
|
}
|
||||||
getEngine() { return this.engine; }
|
getEngine() { return this.engine; }
|
||||||
|
// ===================================================================
|
||||||
|
// 子查询解析
|
||||||
|
// ===================================================================
|
||||||
|
/**
|
||||||
|
* 递归扫描 WHERE 条件,找到 $subquery 标记并执行子查询,
|
||||||
|
* 将结果替换为具体值。
|
||||||
|
*/
|
||||||
|
async resolveSubqueries(where) {
|
||||||
|
const resolved = {};
|
||||||
|
for (const [key, value] of Object.entries(where)) {
|
||||||
|
// 逻辑组合操作符
|
||||||
|
if (key === '$and' && Array.isArray(value)) {
|
||||||
|
resolved.$and = await Promise.all(value.map((sub) => this.resolveSubqueries(sub)));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (key === '$or' && Array.isArray(value)) {
|
||||||
|
resolved.$or = await Promise.all(value.map((sub) => this.resolveSubqueries(sub)));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (key === '$not' && typeof value === 'object' && value !== null) {
|
||||||
|
resolved.$not = await this.resolveSubqueries(value);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// 字段条件
|
||||||
|
if (typeof value === 'object' && value !== null) {
|
||||||
|
resolved[key] = await this.resolveOperatorSubqueries(value);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
resolved[key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return resolved;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 解析操作符值中嵌套的子查询
|
||||||
|
*/
|
||||||
|
async resolveOperatorSubqueries(ops) {
|
||||||
|
const resolved = {};
|
||||||
|
for (const [op, operand] of Object.entries(ops)) {
|
||||||
|
// 处理嵌套 $and/$or(在字段级条件中)
|
||||||
|
if (op === '$and' && Array.isArray(operand)) {
|
||||||
|
resolved.$and = await Promise.all(operand.map((sub) => this.resolveSubqueries(sub)));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (op === '$or' && Array.isArray(operand)) {
|
||||||
|
resolved.$or = await Promise.all(operand.map((sub) => this.resolveSubqueries(sub)));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (op === '$not') {
|
||||||
|
resolved.$not = typeof operand === 'object' && operand !== null
|
||||||
|
? await this.resolveOperatorSubqueries(operand)
|
||||||
|
: operand;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// 子查询检测
|
||||||
|
if (typeof operand === 'object' && operand !== null && '$subquery' in operand) {
|
||||||
|
const subStmt = operand.$subquery;
|
||||||
|
const subResult = await this.executeSelect(subStmt);
|
||||||
|
if (op === '$in' || op === '$nin') {
|
||||||
|
// IN 子查询 → 提取第一列的值列表
|
||||||
|
const colName = Object.keys(subResult[0] || {})[0];
|
||||||
|
const values = subResult.map((row) => row[colName]);
|
||||||
|
resolved[op] = values;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// 标量子查询 → 取第一行第一列
|
||||||
|
if (subResult.length === 0) {
|
||||||
|
resolved[op] = null;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
const colName = Object.keys(subResult[0])[0];
|
||||||
|
resolved[op] = subResult[0][colName];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
resolved[op] = operand;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return resolved;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1412,6 +1709,8 @@ var TokenType;
|
|||||||
TokenType["DEFAULT"] = "DEFAULT";
|
TokenType["DEFAULT"] = "DEFAULT";
|
||||||
TokenType["NULL"] = "NULL";
|
TokenType["NULL"] = "NULL";
|
||||||
TokenType["TRUE"] = "TRUE";
|
TokenType["TRUE"] = "TRUE";
|
||||||
|
TokenType["REFERENCES"] = "REFERENCES";
|
||||||
|
TokenType["CASCADE"] = "CASCADE";
|
||||||
TokenType["FALSE"] = "FALSE";
|
TokenType["FALSE"] = "FALSE";
|
||||||
// JOIN 相关
|
// JOIN 相关
|
||||||
TokenType["INNER"] = "INNER";
|
TokenType["INNER"] = "INNER";
|
||||||
@@ -1486,6 +1785,8 @@ const KEYWORDS = {
|
|||||||
'NULL': TokenType.NULL,
|
'NULL': TokenType.NULL,
|
||||||
'TRUE': TokenType.TRUE,
|
'TRUE': TokenType.TRUE,
|
||||||
'FALSE': TokenType.FALSE,
|
'FALSE': TokenType.FALSE,
|
||||||
|
'REFERENCES': TokenType.REFERENCES,
|
||||||
|
'CASCADE': TokenType.CASCADE,
|
||||||
// JOIN
|
// JOIN
|
||||||
'INNER': TokenType.INNER,
|
'INNER': TokenType.INNER,
|
||||||
'LEFT': TokenType.LEFT,
|
'LEFT': TokenType.LEFT,
|
||||||
@@ -1994,7 +2295,8 @@ class Parser {
|
|||||||
while (this.curTokenIs(TokenType.PRIMARY) ||
|
while (this.curTokenIs(TokenType.PRIMARY) ||
|
||||||
this.curTokenIs(TokenType.UNIQUE) ||
|
this.curTokenIs(TokenType.UNIQUE) ||
|
||||||
this.curTokenIs(TokenType.NOT) ||
|
this.curTokenIs(TokenType.NOT) ||
|
||||||
this.curTokenIs(TokenType.DEFAULT)) {
|
this.curTokenIs(TokenType.DEFAULT) ||
|
||||||
|
this.curTokenIs(TokenType.REFERENCES)) {
|
||||||
if (this.curTokenIs(TokenType.PRIMARY)) {
|
if (this.curTokenIs(TokenType.PRIMARY)) {
|
||||||
this.nextToken();
|
this.nextToken();
|
||||||
this.expect(TokenType.KEY);
|
this.expect(TokenType.KEY);
|
||||||
@@ -2006,7 +2308,6 @@ class Parser {
|
|||||||
}
|
}
|
||||||
else if (this.curTokenIs(TokenType.NOT)) {
|
else if (this.curTokenIs(TokenType.NOT)) {
|
||||||
this.nextToken();
|
this.nextToken();
|
||||||
// 支持 NOT NULL → required
|
|
||||||
this.expect(TokenType.NULL);
|
this.expect(TokenType.NULL);
|
||||||
col.required = true;
|
col.required = true;
|
||||||
}
|
}
|
||||||
@@ -2014,12 +2315,53 @@ class Parser {
|
|||||||
this.nextToken();
|
this.nextToken();
|
||||||
col.default = this.parseValue();
|
col.default = this.parseValue();
|
||||||
}
|
}
|
||||||
|
else if (this.curTokenIs(TokenType.REFERENCES)) {
|
||||||
|
this.nextToken();
|
||||||
|
const refTable = this.expectIdentifier('referenced table');
|
||||||
|
this.expect(TokenType.LPAREN);
|
||||||
|
const refCol = this.expectIdentifier('referenced column');
|
||||||
|
this.expect(TokenType.RPAREN);
|
||||||
|
col.references = `${refTable}.${refCol}`;
|
||||||
|
// ON DELETE / ON UPDATE
|
||||||
|
while (this.curTokenIs(TokenType.ON)) {
|
||||||
|
this.nextToken();
|
||||||
|
if (this.curTokenIs(TokenType.DELETE)) {
|
||||||
|
this.nextToken();
|
||||||
|
col.onDelete = this.parseCascadeAction();
|
||||||
|
}
|
||||||
|
else if (this.curTokenIs(TokenType.UPDATE)) {
|
||||||
|
this.nextToken();
|
||||||
|
col.onUpdate = this.parseCascadeAction();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
else {
|
else {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return col;
|
return col;
|
||||||
}
|
}
|
||||||
|
/** 解析 CASCADE | SET NULL | RESTRICT */
|
||||||
|
parseCascadeAction() {
|
||||||
|
if (this.curTokenIs(TokenType.CASCADE)) {
|
||||||
|
this.nextToken();
|
||||||
|
return 'CASCADE';
|
||||||
|
}
|
||||||
|
if (this.curTokenIs(TokenType.SET)) {
|
||||||
|
this.nextToken();
|
||||||
|
this.expect(TokenType.NULL);
|
||||||
|
return 'SET NULL';
|
||||||
|
}
|
||||||
|
// RESTRICT 或默认
|
||||||
|
if (this.curToken.type === TokenType.IDENTIFIER && this.curToken.value.toUpperCase() === 'RESTRICT') {
|
||||||
|
this.nextToken();
|
||||||
|
return 'RESTRICT';
|
||||||
|
}
|
||||||
|
return 'RESTRICT';
|
||||||
|
}
|
||||||
// ===================================================================
|
// ===================================================================
|
||||||
// DROP TABLE
|
// DROP TABLE
|
||||||
// ===================================================================
|
// ===================================================================
|
||||||
@@ -2090,6 +2432,14 @@ class Parser {
|
|||||||
if (this.curTokenIs(TokenType.IN)) {
|
if (this.curTokenIs(TokenType.IN)) {
|
||||||
this.nextToken();
|
this.nextToken();
|
||||||
this.expect(TokenType.LPAREN);
|
this.expect(TokenType.LPAREN);
|
||||||
|
// 子查询: IN (SELECT ...)
|
||||||
|
if (this.curTokenIs(TokenType.SELECT)) {
|
||||||
|
const subquery = this.parseSelect();
|
||||||
|
this.expect(TokenType.RPAREN);
|
||||||
|
const result = {};
|
||||||
|
result[column] = { $in: { $subquery: subquery } };
|
||||||
|
return result;
|
||||||
|
}
|
||||||
const values = this.parseValueList();
|
const values = this.parseValueList();
|
||||||
this.expect(TokenType.RPAREN);
|
this.expect(TokenType.RPAREN);
|
||||||
const result = {};
|
const result = {};
|
||||||
@@ -2098,6 +2448,15 @@ class Parser {
|
|||||||
}
|
}
|
||||||
// 比较运算符
|
// 比较运算符
|
||||||
const op = this.parseComparisonOp();
|
const op = this.parseComparisonOp();
|
||||||
|
// 子查询: op (SELECT ...)
|
||||||
|
if (this.curTokenIs(TokenType.LPAREN) && this.peekTokenIs(TokenType.SELECT)) {
|
||||||
|
this.nextToken(); // skip (
|
||||||
|
const subquery = this.parseSelect();
|
||||||
|
this.expect(TokenType.RPAREN);
|
||||||
|
const result = {};
|
||||||
|
result[column] = { [op]: { $subquery: subquery } };
|
||||||
|
return result;
|
||||||
|
}
|
||||||
// 尝试解析列引用(identifier DOT identifier 格式)
|
// 尝试解析列引用(identifier DOT identifier 格式)
|
||||||
let value;
|
let value;
|
||||||
if ((this.curToken.type === TokenType.IDENTIFIER || this._isKeywordAsIdent()) &&
|
if ((this.curToken.type === TokenType.IDENTIFIER || this._isKeywordAsIdent()) &&
|
||||||
@@ -2336,8 +2695,7 @@ function parse(sql) {
|
|||||||
* metona-sqlark Transaction — 事务管理
|
* metona-sqlark Transaction — 事务管理
|
||||||
* @module transaction
|
* @module transaction
|
||||||
*
|
*
|
||||||
* 封装多操作事务,保证原子性。
|
* v0.1.13: 支持真正的回滚 — 利用引擎层 begin/commit/rollback 实现原子性。
|
||||||
* v0.0.1: 基于引擎层操作实现,IndexedDB 引擎利用原生事务。
|
|
||||||
*/
|
*/
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Transaction
|
// Transaction
|
||||||
@@ -2373,15 +2731,21 @@ class TransactionManager {
|
|||||||
constructor(engine) {
|
constructor(engine) {
|
||||||
this.engine = engine;
|
this.engine = engine;
|
||||||
}
|
}
|
||||||
/** 执行事务 */
|
/** 执行事务 — 支持自动回滚 */
|
||||||
async execute(fn) {
|
async execute(fn) {
|
||||||
const trx = new Transaction(this.engine);
|
const trx = new Transaction(this.engine);
|
||||||
|
// 开始引擎层事务
|
||||||
|
await this.engine.beginTransaction();
|
||||||
try {
|
try {
|
||||||
const result = await fn(trx);
|
const result = await fn(trx);
|
||||||
|
// 成功 → 提交
|
||||||
|
await this.engine.commitTransaction();
|
||||||
trx._markCompleted();
|
trx._markCompleted();
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
catch (error) {
|
catch (error) {
|
||||||
|
// 失败 → 回滚
|
||||||
|
await this.engine.rollbackTransaction();
|
||||||
if (error instanceof DatabaseError)
|
if (error instanceof DatabaseError)
|
||||||
throw error;
|
throw error;
|
||||||
throw new DatabaseError(`Transaction failed: ${error.message}`, 'TRANSACTION_ERROR', error);
|
throw new DatabaseError(`Transaction failed: ${error.message}`, 'TRANSACTION_ERROR', error);
|
||||||
@@ -2664,6 +3028,121 @@ class MetonaSqlark {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* metona-sqlark Connection Manager — 数据库实例连接池
|
||||||
|
* @module connection-manager
|
||||||
|
*
|
||||||
|
* v0.1.13: 避免重复创建同名数据库实例,通过 connect() 复用已有连接。
|
||||||
|
* 管理实例生命周期,防止重复 open IndexedDB。
|
||||||
|
*/
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// ConnectionManager
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
class ConnectionManager {
|
||||||
|
constructor() {
|
||||||
|
/** 活跃连接:dbName → MetonaSqlark */
|
||||||
|
this.connections = new Map();
|
||||||
|
/** 连接引用计数:dbName → count */
|
||||||
|
this.refCount = new Map();
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 获取或创建数据库实例
|
||||||
|
*
|
||||||
|
* 如果同名数据库已打开,复用已有实例并增加引用计数。
|
||||||
|
* 否则创建新实例。
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```ts
|
||||||
|
* const db = await MetonaSqlark.connect({ name: 'my-app', mode: 'hybrid' });
|
||||||
|
* // ... use db
|
||||||
|
* await db.disconnect(); // 引用计数 -1,归零时自动关闭
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
async connect(config) {
|
||||||
|
const name = config.name;
|
||||||
|
// 已有连接 → 复用
|
||||||
|
const existing = this.connections.get(name);
|
||||||
|
if (existing && existing.isReady()) {
|
||||||
|
const count = (this.refCount.get(name) ?? 0) + 1;
|
||||||
|
this.refCount.set(name, count);
|
||||||
|
return existing;
|
||||||
|
}
|
||||||
|
// 创建新连接
|
||||||
|
const db = new MetonaSqlark(config);
|
||||||
|
await db.init();
|
||||||
|
this.connections.set(name, db);
|
||||||
|
this.refCount.set(name, 1);
|
||||||
|
// 注入 disconnect 方法
|
||||||
|
db.disconnect = async () => {
|
||||||
|
await this.release(name);
|
||||||
|
};
|
||||||
|
return db;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 释放连接引用。引用计数归零时自动关闭数据库。
|
||||||
|
*/
|
||||||
|
async release(dbName) {
|
||||||
|
const count = (this.refCount.get(dbName) ?? 1) - 1;
|
||||||
|
if (count <= 0) {
|
||||||
|
const db = this.connections.get(dbName);
|
||||||
|
if (db) {
|
||||||
|
await db.close();
|
||||||
|
this.connections.delete(dbName);
|
||||||
|
}
|
||||||
|
this.refCount.delete(dbName);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this.refCount.set(dbName, count);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 强制关闭指定数据库(忽略引用计数)
|
||||||
|
*/
|
||||||
|
async forceClose(dbName) {
|
||||||
|
const db = this.connections.get(dbName);
|
||||||
|
if (db) {
|
||||||
|
await db.close();
|
||||||
|
this.connections.delete(dbName);
|
||||||
|
}
|
||||||
|
this.refCount.delete(dbName);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 强制关闭所有连接
|
||||||
|
*/
|
||||||
|
async closeAll() {
|
||||||
|
for (const [, db] of this.connections) {
|
||||||
|
try {
|
||||||
|
await db.close();
|
||||||
|
}
|
||||||
|
catch { /* ignore */ }
|
||||||
|
}
|
||||||
|
this.connections.clear();
|
||||||
|
this.refCount.clear();
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 获取所有活跃连接名
|
||||||
|
*/
|
||||||
|
getActiveConnections() {
|
||||||
|
return Array.from(this.connections.keys());
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 获取连接的引用计数
|
||||||
|
*/
|
||||||
|
getRefCount(dbName) {
|
||||||
|
return this.refCount.get(dbName) ?? 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// 全局单例
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
const manager = new ConnectionManager();
|
||||||
|
// 挂载到 MetonaSqlark 静态方法(通过 any 绕过 TS 类型检查)
|
||||||
|
const M = MetonaSqlark;
|
||||||
|
M.connect = (config) => manager.connect(config);
|
||||||
|
M.disconnect = (dbName) => manager.release(dbName);
|
||||||
|
M.disconnectAll = () => manager.closeAll();
|
||||||
|
M.getActiveConnections = () => manager.getActiveConnections();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* metona-sqlark — 入口文件
|
* metona-sqlark — 入口文件
|
||||||
* @module metona-sqlark
|
* @module metona-sqlark
|
||||||
|
|||||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+61
-3
@@ -24,6 +24,10 @@ interface ColumnDef {
|
|||||||
index?: boolean;
|
index?: boolean;
|
||||||
/** 外键引用: 'table.column' */
|
/** 外键引用: 'table.column' */
|
||||||
references?: string;
|
references?: string;
|
||||||
|
/** 删除级联: 'CASCADE' | 'SET NULL' | 'RESTRICT' */
|
||||||
|
onDelete?: 'CASCADE' | 'SET NULL' | 'RESTRICT';
|
||||||
|
/** 更新级联: 'CASCADE' | 'SET NULL' | 'RESTRICT' */
|
||||||
|
onUpdate?: 'CASCADE' | 'SET NULL' | 'RESTRICT';
|
||||||
/** 字符串最大长度 */
|
/** 字符串最大长度 */
|
||||||
maxLength?: number;
|
maxLength?: number;
|
||||||
/** 数字最小值 */
|
/** 数字最小值 */
|
||||||
@@ -106,7 +110,7 @@ interface MetonaPlugin {
|
|||||||
/** 销毁 */
|
/** 销毁 */
|
||||||
destroy(): void;
|
destroy(): void;
|
||||||
}
|
}
|
||||||
declare const VERSION = "0.1.12";
|
declare const VERSION = "0.1.13";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* metona-sqlark Plugin — 插件系统
|
* metona-sqlark Plugin — 插件系统
|
||||||
@@ -171,6 +175,12 @@ interface IStorageEngine {
|
|||||||
count(tableName: string, query?: QueryPlan): Promise<number>;
|
count(tableName: string, query?: QueryPlan): Promise<number>;
|
||||||
/** 清空表数据(保留结构) */
|
/** 清空表数据(保留结构) */
|
||||||
clear(tableName: string): Promise<void>;
|
clear(tableName: string): Promise<void>;
|
||||||
|
/** 开始事务 */
|
||||||
|
beginTransaction(): Promise<void>;
|
||||||
|
/** 提交事务 */
|
||||||
|
commitTransaction(): Promise<void>;
|
||||||
|
/** 回滚事务 */
|
||||||
|
rollbackTransaction(): Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -203,6 +213,12 @@ interface ASTColumnDef {
|
|||||||
maxLength?: number;
|
maxLength?: number;
|
||||||
min?: number;
|
min?: number;
|
||||||
max?: number;
|
max?: number;
|
||||||
|
/** 外键引用 */
|
||||||
|
references?: string;
|
||||||
|
/** 级联删除 */
|
||||||
|
onDelete?: 'CASCADE' | 'SET NULL' | 'RESTRICT';
|
||||||
|
/** 级联更新 */
|
||||||
|
onUpdate?: 'CASCADE' | 'SET NULL' | 'RESTRICT';
|
||||||
}
|
}
|
||||||
interface CreateTableStatement {
|
interface CreateTableStatement {
|
||||||
type: 'CREATE_TABLE';
|
type: 'CREATE_TABLE';
|
||||||
@@ -275,6 +291,15 @@ declare class QueryExecutor {
|
|||||||
private executeCreateTable;
|
private executeCreateTable;
|
||||||
private executeDropTable;
|
private executeDropTable;
|
||||||
getEngine(): IStorageEngine;
|
getEngine(): IStorageEngine;
|
||||||
|
/**
|
||||||
|
* 递归扫描 WHERE 条件,找到 $subquery 标记并执行子查询,
|
||||||
|
* 将结果替换为具体值。
|
||||||
|
*/
|
||||||
|
private resolveSubqueries;
|
||||||
|
/**
|
||||||
|
* 解析操作符值中嵌套的子查询
|
||||||
|
*/
|
||||||
|
private resolveOperatorSubqueries;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -369,8 +394,7 @@ declare class Table<T = Record<string, unknown>> {
|
|||||||
* metona-sqlark Transaction — 事务管理
|
* metona-sqlark Transaction — 事务管理
|
||||||
* @module transaction
|
* @module transaction
|
||||||
*
|
*
|
||||||
* 封装多操作事务,保证原子性。
|
* v0.1.13: 支持真正的回滚 — 利用引擎层 begin/commit/rollback 实现原子性。
|
||||||
* v0.0.1: 基于引擎层操作实现,IndexedDB 引擎利用原生事务。
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
declare class Transaction {
|
declare class Transaction {
|
||||||
@@ -464,6 +488,7 @@ declare class MemoryEngine implements IStorageEngine {
|
|||||||
private schemas;
|
private schemas;
|
||||||
private indexes;
|
private indexes;
|
||||||
private opened;
|
private opened;
|
||||||
|
private snapshot;
|
||||||
open(_dbName: string, _version: number): Promise<void>;
|
open(_dbName: string, _version: number): Promise<void>;
|
||||||
close(): Promise<void>;
|
close(): Promise<void>;
|
||||||
isOpen(): boolean;
|
isOpen(): boolean;
|
||||||
@@ -478,6 +503,11 @@ declare class MemoryEngine implements IStorageEngine {
|
|||||||
delete(tableName: string, query: QueryPlan): Promise<number>;
|
delete(tableName: string, query: QueryPlan): Promise<number>;
|
||||||
count(tableName: string, query?: QueryPlan): Promise<number>;
|
count(tableName: string, query?: QueryPlan): Promise<number>;
|
||||||
clear(tableName: string): Promise<void>;
|
clear(tableName: string): Promise<void>;
|
||||||
|
beginTransaction(): Promise<void>;
|
||||||
|
commitTransaction(): Promise<void>;
|
||||||
|
rollbackTransaction(): Promise<void>;
|
||||||
|
private deepCloneMapMap;
|
||||||
|
private deepCloneIndexes;
|
||||||
private ensureTable;
|
private ensureTable;
|
||||||
private getPrimaryKey;
|
private getPrimaryKey;
|
||||||
private validateRow;
|
private validateRow;
|
||||||
@@ -488,11 +518,18 @@ declare class MemoryEngine implements IStorageEngine {
|
|||||||
private tryIndexLookup;
|
private tryIndexLookup;
|
||||||
/** 更新索引 */
|
/** 更新索引 */
|
||||||
private updateIndexes;
|
private updateIndexes;
|
||||||
|
/**
|
||||||
|
* 级联删除:查找引用 tableName.pkValue 的所有表的行并删除。
|
||||||
|
* @returns 级联删除的行数
|
||||||
|
*/
|
||||||
|
private cascadeDelete;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* metona-sqlark IndexedDB Engine — 基于 IndexedDB 的持久化存储引擎
|
* metona-sqlark IndexedDB Engine — 基于 IndexedDB 的持久化存储引擎
|
||||||
* @module engine/indexeddb
|
* @module engine/indexeddb
|
||||||
|
*
|
||||||
|
* v0.1.13: 支持事务 — beginTransaction 延迟 IDB 写入,commit 批量刷盘,rollback 恢复快照。
|
||||||
*/
|
*/
|
||||||
|
|
||||||
declare class IndexedDBEngine implements IStorageEngine {
|
declare class IndexedDBEngine implements IStorageEngine {
|
||||||
@@ -501,6 +538,7 @@ declare class IndexedDBEngine implements IStorageEngine {
|
|||||||
private dbName;
|
private dbName;
|
||||||
private version;
|
private version;
|
||||||
private memoryCache;
|
private memoryCache;
|
||||||
|
private txActive;
|
||||||
open(dbName: string, version: number): Promise<void>;
|
open(dbName: string, version: number): Promise<void>;
|
||||||
close(): Promise<void>;
|
close(): Promise<void>;
|
||||||
isOpen(): boolean;
|
isOpen(): boolean;
|
||||||
@@ -515,6 +553,18 @@ declare class IndexedDBEngine implements IStorageEngine {
|
|||||||
delete(tableName: string, query: QueryPlan): Promise<number>;
|
delete(tableName: string, query: QueryPlan): Promise<number>;
|
||||||
count(tableName: string, query?: QueryPlan): Promise<number>;
|
count(tableName: string, query?: QueryPlan): Promise<number>;
|
||||||
clear(tableName: string): Promise<void>;
|
clear(tableName: string): Promise<void>;
|
||||||
|
beginTransaction(): Promise<void>;
|
||||||
|
commitTransaction(): Promise<void>;
|
||||||
|
rollbackTransaction(): Promise<void>;
|
||||||
|
private idbCreateTable;
|
||||||
|
private idbDropTable;
|
||||||
|
private idbInsert;
|
||||||
|
private idbFind;
|
||||||
|
private idbUpdate;
|
||||||
|
private idbDelete;
|
||||||
|
private idbClear;
|
||||||
|
/** 将内存缓存中的所有表数据刷新到 IndexedDB */
|
||||||
|
private flushToIDB;
|
||||||
private ensureDB;
|
private ensureDB;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -546,6 +596,9 @@ declare class OPFSEngine implements IStorageEngine {
|
|||||||
delete(tableName: string, query: QueryPlan): Promise<number>;
|
delete(tableName: string, query: QueryPlan): Promise<number>;
|
||||||
count(tableName: string, query?: QueryPlan): Promise<number>;
|
count(tableName: string, query?: QueryPlan): Promise<number>;
|
||||||
clear(tableName: string): Promise<void>;
|
clear(tableName: string): Promise<void>;
|
||||||
|
beginTransaction(): Promise<void>;
|
||||||
|
commitTransaction(): Promise<void>;
|
||||||
|
rollbackTransaction(): Promise<void>;
|
||||||
private ensureDir;
|
private ensureDir;
|
||||||
private writeTableData;
|
private writeTableData;
|
||||||
private readTableData;
|
private readTableData;
|
||||||
@@ -583,6 +636,9 @@ declare class HybridEngine implements IStorageEngine {
|
|||||||
delete(tableName: string, query: QueryPlan): Promise<number>;
|
delete(tableName: string, query: QueryPlan): Promise<number>;
|
||||||
count(tableName: string, query?: QueryPlan): Promise<number>;
|
count(tableName: string, query?: QueryPlan): Promise<number>;
|
||||||
clear(tableName: string): Promise<void>;
|
clear(tableName: string): Promise<void>;
|
||||||
|
beginTransaction(): Promise<void>;
|
||||||
|
commitTransaction(): Promise<void>;
|
||||||
|
rollbackTransaction(): Promise<void>;
|
||||||
/** 获取磁盘引擎类型 */
|
/** 获取磁盘引擎类型 */
|
||||||
getDiskEngineType(): DiskEngine;
|
getDiskEngineType(): DiskEngine;
|
||||||
/** 获取内存引擎(供内部使用) */
|
/** 获取内存引擎(供内部使用) */
|
||||||
@@ -634,6 +690,8 @@ declare enum TokenType {
|
|||||||
DEFAULT = "DEFAULT",
|
DEFAULT = "DEFAULT",
|
||||||
NULL = "NULL",
|
NULL = "NULL",
|
||||||
TRUE = "TRUE",
|
TRUE = "TRUE",
|
||||||
|
REFERENCES = "REFERENCES",
|
||||||
|
CASCADE = "CASCADE",
|
||||||
FALSE = "FALSE",
|
FALSE = "FALSE",
|
||||||
INNER = "INNER",
|
INNER = "INNER",
|
||||||
LEFT = "LEFT",
|
LEFT = "LEFT",
|
||||||
|
|||||||
Vendored
+518
-39
@@ -27,7 +27,7 @@ class DatabaseError extends Error {
|
|||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// 版本
|
// 版本
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
const VERSION = '0.1.12';
|
const VERSION = '0.1.13';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* metona-sqlark Shared WHERE Matcher — 统一的条件匹配逻辑
|
* metona-sqlark Shared WHERE Matcher — 统一的条件匹配逻辑
|
||||||
@@ -196,6 +196,8 @@ class MemoryEngine {
|
|||||||
this.schemas = new Map();
|
this.schemas = new Map();
|
||||||
this.indexes = new Map();
|
this.indexes = new Map();
|
||||||
this.opened = false;
|
this.opened = false;
|
||||||
|
// ---- 事务快照 ----
|
||||||
|
this.snapshot = null;
|
||||||
}
|
}
|
||||||
// ---- 生命周期 ----
|
// ---- 生命周期 ----
|
||||||
async open(_dbName, _version) { this.opened = true; }
|
async open(_dbName, _version) { this.opened = true; }
|
||||||
@@ -289,9 +291,16 @@ class MemoryEngine {
|
|||||||
toDelete.push(pk);
|
toDelete.push(pk);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// 级联删除:检查引用此表的其他表
|
||||||
|
let cascadeCount = 0;
|
||||||
|
for (const pk of toDelete) {
|
||||||
|
const row = table.get(pk);
|
||||||
|
if (row)
|
||||||
|
cascadeCount += await this.cascadeDelete(tableName, pk, row);
|
||||||
|
}
|
||||||
for (const pk of toDelete)
|
for (const pk of toDelete)
|
||||||
table.delete(pk);
|
table.delete(pk);
|
||||||
return toDelete.length;
|
return toDelete.length + cascadeCount;
|
||||||
}
|
}
|
||||||
async count(tableName, query) {
|
async count(tableName, query) {
|
||||||
this.ensureTable(tableName);
|
this.ensureTable(tableName);
|
||||||
@@ -313,6 +322,54 @@ class MemoryEngine {
|
|||||||
for (const colIndex of tableIndexes.values())
|
for (const colIndex of tableIndexes.values())
|
||||||
colIndex.clear();
|
colIndex.clear();
|
||||||
}
|
}
|
||||||
|
// ---- 事务 ----
|
||||||
|
async beginTransaction() {
|
||||||
|
if (this.snapshot)
|
||||||
|
throw new DatabaseError('Transaction already in progress', 'TX_ACTIVE');
|
||||||
|
this.snapshot = {
|
||||||
|
tables: this.deepCloneMapMap(this.tables),
|
||||||
|
schemas: new Map(this.schemas),
|
||||||
|
indexes: this.deepCloneIndexes(this.indexes),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
async commitTransaction() {
|
||||||
|
if (!this.snapshot)
|
||||||
|
throw new DatabaseError('No active transaction', 'TX_NONE');
|
||||||
|
this.snapshot = null;
|
||||||
|
}
|
||||||
|
async rollbackTransaction() {
|
||||||
|
if (!this.snapshot)
|
||||||
|
throw new DatabaseError('No active transaction', 'TX_NONE');
|
||||||
|
this.tables = this.snapshot.tables;
|
||||||
|
this.schemas = this.snapshot.schemas;
|
||||||
|
this.indexes = this.snapshot.indexes;
|
||||||
|
this.snapshot = null;
|
||||||
|
}
|
||||||
|
// ---- 事务快照辅助 ----
|
||||||
|
deepCloneMapMap(source) {
|
||||||
|
const clone = new Map();
|
||||||
|
for (const [k, v] of source) {
|
||||||
|
const innerClone = new Map();
|
||||||
|
for (const [ik, iv] of v)
|
||||||
|
innerClone.set(ik, { ...iv });
|
||||||
|
clone.set(k, innerClone);
|
||||||
|
}
|
||||||
|
return clone;
|
||||||
|
}
|
||||||
|
deepCloneIndexes(source) {
|
||||||
|
const clone = new Map();
|
||||||
|
for (const [tableName, tableIndexes] of source) {
|
||||||
|
const tableClone = new Map();
|
||||||
|
for (const [col, colIndex] of tableIndexes) {
|
||||||
|
const colClone = new Map();
|
||||||
|
for (const [val, pkSet] of colIndex)
|
||||||
|
colClone.set(val, new Set(pkSet));
|
||||||
|
tableClone.set(col, colClone);
|
||||||
|
}
|
||||||
|
clone.set(tableName, tableClone);
|
||||||
|
}
|
||||||
|
return clone;
|
||||||
|
}
|
||||||
// ---- 内部辅助 ----
|
// ---- 内部辅助 ----
|
||||||
ensureTable(tableName) {
|
ensureTable(tableName) {
|
||||||
if (!this.tables.has(tableName))
|
if (!this.tables.has(tableName))
|
||||||
@@ -419,11 +476,62 @@ class MemoryEngine {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// ---- 外键级联 ----
|
||||||
|
/**
|
||||||
|
* 级联删除:查找引用 tableName.pkValue 的所有表的行并删除。
|
||||||
|
* @returns 级联删除的行数
|
||||||
|
*/
|
||||||
|
async cascadeDelete(tableName, pkValue, _row) {
|
||||||
|
let totalCascade = 0;
|
||||||
|
for (const [refTableName, refSchema] of this.schemas) {
|
||||||
|
if (refTableName === tableName)
|
||||||
|
continue;
|
||||||
|
for (const [colName, colDef] of Object.entries(refSchema.columns)) {
|
||||||
|
if (!colDef.references || !colDef.onDelete || colDef.onDelete === 'RESTRICT')
|
||||||
|
continue;
|
||||||
|
const [refTable, refCol] = colDef.references.split('.');
|
||||||
|
if (refTable !== tableName)
|
||||||
|
continue;
|
||||||
|
const refTableData = this.tables.get(refTableName);
|
||||||
|
if (!refTableData)
|
||||||
|
continue;
|
||||||
|
// 查找所有引用此主键的行
|
||||||
|
const toDelete = [];
|
||||||
|
for (const [refPk, refRow] of refTableData) {
|
||||||
|
if (String(refRow[colName]) === pkValue) {
|
||||||
|
toDelete.push(refPk);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (colDef.onDelete === 'CASCADE') {
|
||||||
|
// 递归级联
|
||||||
|
for (const refPk of toDelete) {
|
||||||
|
const refRow = refTableData.get(refPk);
|
||||||
|
if (refRow) {
|
||||||
|
totalCascade += await this.cascadeDelete(refTableName, refPk, refRow);
|
||||||
|
}
|
||||||
|
refTableData.delete(refPk);
|
||||||
|
totalCascade++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (colDef.onDelete === 'SET NULL') {
|
||||||
|
for (const refPk of toDelete) {
|
||||||
|
const refRow = refTableData.get(refPk);
|
||||||
|
if (refRow) {
|
||||||
|
refRow[colName] = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return totalCascade;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* metona-sqlark IndexedDB Engine — 基于 IndexedDB 的持久化存储引擎
|
* metona-sqlark IndexedDB Engine — 基于 IndexedDB 的持久化存储引擎
|
||||||
* @module engine/indexeddb
|
* @module engine/indexeddb
|
||||||
|
*
|
||||||
|
* v0.1.13: 支持事务 — beginTransaction 延迟 IDB 写入,commit 批量刷盘,rollback 恢复快照。
|
||||||
*/
|
*/
|
||||||
class IndexedDBEngine {
|
class IndexedDBEngine {
|
||||||
constructor() {
|
constructor() {
|
||||||
@@ -432,6 +540,8 @@ class IndexedDBEngine {
|
|||||||
this.dbName = '';
|
this.dbName = '';
|
||||||
this.version = 1;
|
this.version = 1;
|
||||||
this.memoryCache = new MemoryEngine();
|
this.memoryCache = new MemoryEngine();
|
||||||
|
// ---- 事务状态 ----
|
||||||
|
this.txActive = false;
|
||||||
}
|
}
|
||||||
async open(dbName, version) {
|
async open(dbName, version) {
|
||||||
this.dbName = dbName;
|
this.dbName = dbName;
|
||||||
@@ -455,6 +565,85 @@ class IndexedDBEngine {
|
|||||||
// ---- 表管理 ----
|
// ---- 表管理 ----
|
||||||
async createTable(schema) {
|
async createTable(schema) {
|
||||||
await this.memoryCache.createTable(schema);
|
await this.memoryCache.createTable(schema);
|
||||||
|
if (this.txActive)
|
||||||
|
return; // 事务中延迟 IDB 操作
|
||||||
|
await this.idbCreateTable(schema);
|
||||||
|
}
|
||||||
|
async dropTable(tableName) {
|
||||||
|
await this.memoryCache.dropTable(tableName);
|
||||||
|
if (this.txActive)
|
||||||
|
return;
|
||||||
|
await this.idbDropTable(tableName);
|
||||||
|
}
|
||||||
|
async hasTable(tableName) { return this.ensureDB().objectStoreNames.contains(tableName); }
|
||||||
|
async getTableNames() { return Array.from(this.ensureDB().objectStoreNames); }
|
||||||
|
async getTableSchema(tableName) { return this.memoryCache.getTableSchema(tableName); }
|
||||||
|
// ---- CRUD ----
|
||||||
|
async insert(tableName, rows) {
|
||||||
|
const pks = await this.memoryCache.insert(tableName, rows);
|
||||||
|
if (this.txActive)
|
||||||
|
return pks; // 事务中延迟写入
|
||||||
|
await this.idbInsert(tableName, rows);
|
||||||
|
return pks;
|
||||||
|
}
|
||||||
|
async find(tableName, query) {
|
||||||
|
// 事务中从内存缓存读取(保证读到未提交的变更),否则走 IDB
|
||||||
|
if (this.txActive)
|
||||||
|
return this.memoryCache.find(tableName, query);
|
||||||
|
return this.idbFind(tableName, query);
|
||||||
|
}
|
||||||
|
async update(tableName, query, updates) {
|
||||||
|
const count = await this.memoryCache.update(tableName, query, updates);
|
||||||
|
if (this.txActive)
|
||||||
|
return count;
|
||||||
|
await this.idbUpdate(tableName, query, updates);
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
async delete(tableName, query) {
|
||||||
|
const count = await this.memoryCache.delete(tableName, query);
|
||||||
|
if (this.txActive)
|
||||||
|
return count;
|
||||||
|
await this.idbDelete(tableName, query);
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
async count(tableName, query) {
|
||||||
|
if (this.txActive)
|
||||||
|
return this.memoryCache.count(tableName, query);
|
||||||
|
const results = await this.idbFind(tableName, { table: tableName, where: query?.where ?? {} });
|
||||||
|
return results.length;
|
||||||
|
}
|
||||||
|
async clear(tableName) {
|
||||||
|
await this.memoryCache.clear(tableName);
|
||||||
|
if (this.txActive)
|
||||||
|
return;
|
||||||
|
await this.idbClear(tableName);
|
||||||
|
}
|
||||||
|
// ---- 事务 ----
|
||||||
|
async beginTransaction() {
|
||||||
|
if (this.txActive)
|
||||||
|
throw new DatabaseError('Transaction already in progress', 'TX_ACTIVE');
|
||||||
|
this.txActive = true;
|
||||||
|
// 保存内存快照到 MemoryEngine 内部的 beginTransaction
|
||||||
|
await this.memoryCache.beginTransaction();
|
||||||
|
}
|
||||||
|
async commitTransaction() {
|
||||||
|
if (!this.txActive)
|
||||||
|
throw new DatabaseError('No active transaction', 'TX_NONE');
|
||||||
|
// 确认内存层的变更
|
||||||
|
await this.memoryCache.commitTransaction();
|
||||||
|
// 批量将内存数据刷到 IndexedDB
|
||||||
|
await this.flushToIDB();
|
||||||
|
this.txActive = false;
|
||||||
|
}
|
||||||
|
async rollbackTransaction() {
|
||||||
|
if (!this.txActive)
|
||||||
|
throw new DatabaseError('No active transaction', 'TX_NONE');
|
||||||
|
// 恢复内存层到快照状态
|
||||||
|
await this.memoryCache.rollbackTransaction();
|
||||||
|
this.txActive = false;
|
||||||
|
}
|
||||||
|
// ---- IDB 原生操作 ----
|
||||||
|
async idbCreateTable(schema) {
|
||||||
const db = this.ensureDB();
|
const db = this.ensureDB();
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const newVersion = db.version + 1;
|
const newVersion = db.version + 1;
|
||||||
@@ -474,8 +663,7 @@ class IndexedDBEngine {
|
|||||||
request.onerror = () => reject(new DatabaseError(`Failed to create table "${schema.name}"`, 'IDB_UPGRADE_ERROR', request.error));
|
request.onerror = () => reject(new DatabaseError(`Failed to create table "${schema.name}"`, 'IDB_UPGRADE_ERROR', request.error));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
async dropTable(tableName) {
|
async idbDropTable(tableName) {
|
||||||
await this.memoryCache.dropTable(tableName);
|
|
||||||
const db = this.ensureDB();
|
const db = this.ensureDB();
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const newVersion = db.version + 1;
|
const newVersion = db.version + 1;
|
||||||
@@ -490,27 +678,18 @@ class IndexedDBEngine {
|
|||||||
request.onerror = () => reject(new DatabaseError(`Failed to drop table "${tableName}"`, 'IDB_UPGRADE_ERROR', request.error));
|
request.onerror = () => reject(new DatabaseError(`Failed to drop table "${tableName}"`, 'IDB_UPGRADE_ERROR', request.error));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
async hasTable(tableName) { return this.ensureDB().objectStoreNames.contains(tableName); }
|
async idbInsert(tableName, rows) {
|
||||||
async getTableNames() { return Array.from(this.ensureDB().objectStoreNames); }
|
|
||||||
async getTableSchema(tableName) { return this.memoryCache.getTableSchema(tableName); }
|
|
||||||
// ---- CRUD ----
|
|
||||||
async insert(tableName, rows) {
|
|
||||||
await this.memoryCache.insert(tableName, rows);
|
|
||||||
const db = this.ensureDB();
|
const db = this.ensureDB();
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const tx = db.transaction(tableName, 'readwrite');
|
const tx = db.transaction(tableName, 'readwrite');
|
||||||
const store = tx.objectStore(tableName);
|
const store = tx.objectStore(tableName);
|
||||||
const pks = [];
|
for (const row of rows)
|
||||||
for (const row of rows) {
|
store.add(row);
|
||||||
const req = store.add(row);
|
tx.oncomplete = () => resolve();
|
||||||
req.onsuccess = () => pks.push(String(req.result));
|
|
||||||
req.onerror = () => { }; // 内存层已校验,忽略 IDB 重复键
|
|
||||||
}
|
|
||||||
tx.oncomplete = () => resolve(pks);
|
|
||||||
tx.onerror = () => reject(new DatabaseError(`Insert failed for "${tableName}"`, 'IDB_TX_ERROR', tx.error));
|
tx.onerror = () => reject(new DatabaseError(`Insert failed for "${tableName}"`, 'IDB_TX_ERROR', tx.error));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
async find(tableName, query) {
|
async idbFind(tableName, query) {
|
||||||
const db = this.ensureDB();
|
const db = this.ensureDB();
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const tx = db.transaction(tableName, 'readonly');
|
const tx = db.transaction(tableName, 'readonly');
|
||||||
@@ -534,29 +713,25 @@ class IndexedDBEngine {
|
|||||||
req.onerror = () => reject(new DatabaseError(`Find failed for "${tableName}"`, 'IDB_READ_ERROR', req.error));
|
req.onerror = () => reject(new DatabaseError(`Find failed for "${tableName}"`, 'IDB_READ_ERROR', req.error));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
async update(tableName, query, updates) {
|
async idbUpdate(tableName, query, updates) {
|
||||||
await this.memoryCache.update(tableName, query, updates);
|
|
||||||
const db = this.ensureDB();
|
const db = this.ensureDB();
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const tx = db.transaction(tableName, 'readwrite');
|
const tx = db.transaction(tableName, 'readwrite');
|
||||||
const store = tx.objectStore(tableName);
|
const store = tx.objectStore(tableName);
|
||||||
const getAllReq = store.getAll();
|
const getAllReq = store.getAll();
|
||||||
let count = 0;
|
|
||||||
getAllReq.onsuccess = () => {
|
getAllReq.onsuccess = () => {
|
||||||
for (const row of getAllReq.result ?? []) {
|
for (const row of getAllReq.result ?? []) {
|
||||||
if (!query.where || Object.keys(query.where).length === 0 || matchWhere(row, query.where)) {
|
if (!query.where || Object.keys(query.where).length === 0 || matchWhere(row, query.where)) {
|
||||||
Object.assign(row, updates);
|
Object.assign(row, updates);
|
||||||
store.put(row);
|
store.put(row);
|
||||||
count++;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
tx.oncomplete = () => resolve(count);
|
tx.oncomplete = () => resolve();
|
||||||
tx.onerror = () => reject(new DatabaseError(`Update failed for "${tableName}"`, 'IDB_TX_ERROR', tx.error));
|
tx.onerror = () => reject(new DatabaseError(`Update failed for "${tableName}"`, 'IDB_TX_ERROR', tx.error));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
async delete(tableName, query) {
|
async idbDelete(tableName, query) {
|
||||||
await this.memoryCache.delete(tableName, query);
|
|
||||||
const db = this.ensureDB();
|
const db = this.ensureDB();
|
||||||
const schema = await this.memoryCache.getTableSchema(tableName);
|
const schema = await this.memoryCache.getTableSchema(tableName);
|
||||||
if (!schema)
|
if (!schema)
|
||||||
@@ -566,25 +741,18 @@ class IndexedDBEngine {
|
|||||||
const tx = db.transaction(tableName, 'readwrite');
|
const tx = db.transaction(tableName, 'readwrite');
|
||||||
const store = tx.objectStore(tableName);
|
const store = tx.objectStore(tableName);
|
||||||
const getAllReq = store.getAll();
|
const getAllReq = store.getAll();
|
||||||
let count = 0;
|
|
||||||
getAllReq.onsuccess = () => {
|
getAllReq.onsuccess = () => {
|
||||||
for (const row of getAllReq.result ?? []) {
|
for (const row of getAllReq.result ?? []) {
|
||||||
if (!query.where || Object.keys(query.where).length === 0 || matchWhere(row, query.where)) {
|
if (!query.where || Object.keys(query.where).length === 0 || matchWhere(row, query.where)) {
|
||||||
store.delete(row[pkColumn]);
|
store.delete(row[pkColumn]);
|
||||||
count++;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
tx.oncomplete = () => resolve(count);
|
tx.oncomplete = () => resolve();
|
||||||
tx.onerror = () => reject(new DatabaseError(`Delete failed for "${tableName}"`, 'IDB_TX_ERROR', tx.error));
|
tx.onerror = () => reject(new DatabaseError(`Delete failed for "${tableName}"`, 'IDB_TX_ERROR', tx.error));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
async count(tableName, query) {
|
async idbClear(tableName) {
|
||||||
const results = await this.find(tableName, { table: tableName, where: query?.where ?? {} });
|
|
||||||
return results.length;
|
|
||||||
}
|
|
||||||
async clear(tableName) {
|
|
||||||
await this.memoryCache.clear(tableName);
|
|
||||||
const db = this.ensureDB();
|
const db = this.ensureDB();
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const tx = db.transaction(tableName, 'readwrite');
|
const tx = db.transaction(tableName, 'readwrite');
|
||||||
@@ -593,6 +761,18 @@ class IndexedDBEngine {
|
|||||||
tx.onerror = () => reject(new DatabaseError(`Clear failed for "${tableName}"`, 'IDB_TX_ERROR', tx.error));
|
tx.onerror = () => reject(new DatabaseError(`Clear failed for "${tableName}"`, 'IDB_TX_ERROR', tx.error));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
/** 将内存缓存中的所有表数据刷新到 IndexedDB */
|
||||||
|
async flushToIDB() {
|
||||||
|
const tableNames = await this.memoryCache.getTableNames();
|
||||||
|
for (const tableName of tableNames) {
|
||||||
|
const rows = await this.memoryCache.find(tableName, { table: tableName });
|
||||||
|
// Clear + bulk insert for simplicity
|
||||||
|
await this.idbClear(tableName);
|
||||||
|
if (rows.length > 0) {
|
||||||
|
await this.idbInsert(tableName, rows);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
ensureDB() {
|
ensureDB() {
|
||||||
if (!this.db)
|
if (!this.db)
|
||||||
throw new DatabaseError('Database not opened', 'DB_NOT_OPEN');
|
throw new DatabaseError('Database not opened', 'DB_NOT_OPEN');
|
||||||
@@ -708,6 +888,22 @@ class OPFSEngine {
|
|||||||
await this.memoryCache.clear(tableName);
|
await this.memoryCache.clear(tableName);
|
||||||
await this.writeTableData(tableName, []);
|
await this.writeTableData(tableName, []);
|
||||||
}
|
}
|
||||||
|
// ---- 事务 ----
|
||||||
|
async beginTransaction() {
|
||||||
|
await this.memoryCache.beginTransaction();
|
||||||
|
}
|
||||||
|
async commitTransaction() {
|
||||||
|
await this.memoryCache.commitTransaction();
|
||||||
|
// 将内存数据刷到 OPFS
|
||||||
|
const tableNames = await this.memoryCache.getTableNames();
|
||||||
|
for (const tableName of tableNames) {
|
||||||
|
const rows = await this.memoryCache.find(tableName, { table: tableName });
|
||||||
|
await this.writeTableData(tableName, rows);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async rollbackTransaction() {
|
||||||
|
await this.memoryCache.rollbackTransaction();
|
||||||
|
}
|
||||||
// ---- 内部辅助 ----
|
// ---- 内部辅助 ----
|
||||||
ensureDir() {
|
ensureDir() {
|
||||||
if (!this.tablesDir) {
|
if (!this.tablesDir) {
|
||||||
@@ -850,6 +1046,19 @@ class HybridEngine {
|
|||||||
await this.memoryEngine.clear(tableName);
|
await this.memoryEngine.clear(tableName);
|
||||||
await this.diskEngine.clear(tableName);
|
await this.diskEngine.clear(tableName);
|
||||||
}
|
}
|
||||||
|
// ---- 事务 ----
|
||||||
|
async beginTransaction() {
|
||||||
|
await this.memoryEngine.beginTransaction();
|
||||||
|
await this.diskEngine.beginTransaction();
|
||||||
|
}
|
||||||
|
async commitTransaction() {
|
||||||
|
await this.memoryEngine.commitTransaction();
|
||||||
|
await this.diskEngine.commitTransaction();
|
||||||
|
}
|
||||||
|
async rollbackTransaction() {
|
||||||
|
await this.memoryEngine.rollbackTransaction();
|
||||||
|
await this.diskEngine.rollbackTransaction();
|
||||||
|
}
|
||||||
// ---- 引擎信息 ----
|
// ---- 引擎信息 ----
|
||||||
/** 获取磁盘引擎类型 */
|
/** 获取磁盘引擎类型 */
|
||||||
getDiskEngineType() {
|
getDiskEngineType() {
|
||||||
@@ -1107,6 +1316,9 @@ function astColumnToColumnDef(astCol) {
|
|||||||
maxLength: astCol.maxLength,
|
maxLength: astCol.maxLength,
|
||||||
min: astCol.min,
|
min: astCol.min,
|
||||||
max: astCol.max,
|
max: astCol.max,
|
||||||
|
references: astCol.references,
|
||||||
|
onDelete: astCol.onDelete,
|
||||||
|
onUpdate: astCol.onUpdate,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1187,6 +1399,10 @@ class QueryExecutor {
|
|||||||
// SELECT
|
// SELECT
|
||||||
// ===================================================================
|
// ===================================================================
|
||||||
async executeSelect(stmt) {
|
async executeSelect(stmt) {
|
||||||
|
// 先解析子查询
|
||||||
|
if (stmt.where && Object.keys(stmt.where).length > 0) {
|
||||||
|
stmt.where = await this.resolveSubqueries(stmt.where);
|
||||||
|
}
|
||||||
const hasGroupBy = !!(stmt.groupBy && stmt.groupBy.length > 0);
|
const hasGroupBy = !!(stmt.groupBy && stmt.groupBy.length > 0);
|
||||||
let rows;
|
let rows;
|
||||||
if (!stmt.joins || stmt.joins.length === 0) {
|
if (!stmt.joins || stmt.joins.length === 0) {
|
||||||
@@ -1367,6 +1583,87 @@ class QueryExecutor {
|
|||||||
return this.engine.dropTable(stmt.name);
|
return this.engine.dropTable(stmt.name);
|
||||||
}
|
}
|
||||||
getEngine() { return this.engine; }
|
getEngine() { return this.engine; }
|
||||||
|
// ===================================================================
|
||||||
|
// 子查询解析
|
||||||
|
// ===================================================================
|
||||||
|
/**
|
||||||
|
* 递归扫描 WHERE 条件,找到 $subquery 标记并执行子查询,
|
||||||
|
* 将结果替换为具体值。
|
||||||
|
*/
|
||||||
|
async resolveSubqueries(where) {
|
||||||
|
const resolved = {};
|
||||||
|
for (const [key, value] of Object.entries(where)) {
|
||||||
|
// 逻辑组合操作符
|
||||||
|
if (key === '$and' && Array.isArray(value)) {
|
||||||
|
resolved.$and = await Promise.all(value.map((sub) => this.resolveSubqueries(sub)));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (key === '$or' && Array.isArray(value)) {
|
||||||
|
resolved.$or = await Promise.all(value.map((sub) => this.resolveSubqueries(sub)));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (key === '$not' && typeof value === 'object' && value !== null) {
|
||||||
|
resolved.$not = await this.resolveSubqueries(value);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// 字段条件
|
||||||
|
if (typeof value === 'object' && value !== null) {
|
||||||
|
resolved[key] = await this.resolveOperatorSubqueries(value);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
resolved[key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return resolved;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 解析操作符值中嵌套的子查询
|
||||||
|
*/
|
||||||
|
async resolveOperatorSubqueries(ops) {
|
||||||
|
const resolved = {};
|
||||||
|
for (const [op, operand] of Object.entries(ops)) {
|
||||||
|
// 处理嵌套 $and/$or(在字段级条件中)
|
||||||
|
if (op === '$and' && Array.isArray(operand)) {
|
||||||
|
resolved.$and = await Promise.all(operand.map((sub) => this.resolveSubqueries(sub)));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (op === '$or' && Array.isArray(operand)) {
|
||||||
|
resolved.$or = await Promise.all(operand.map((sub) => this.resolveSubqueries(sub)));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (op === '$not') {
|
||||||
|
resolved.$not = typeof operand === 'object' && operand !== null
|
||||||
|
? await this.resolveOperatorSubqueries(operand)
|
||||||
|
: operand;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// 子查询检测
|
||||||
|
if (typeof operand === 'object' && operand !== null && '$subquery' in operand) {
|
||||||
|
const subStmt = operand.$subquery;
|
||||||
|
const subResult = await this.executeSelect(subStmt);
|
||||||
|
if (op === '$in' || op === '$nin') {
|
||||||
|
// IN 子查询 → 提取第一列的值列表
|
||||||
|
const colName = Object.keys(subResult[0] || {})[0];
|
||||||
|
const values = subResult.map((row) => row[colName]);
|
||||||
|
resolved[op] = values;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// 标量子查询 → 取第一行第一列
|
||||||
|
if (subResult.length === 0) {
|
||||||
|
resolved[op] = null;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
const colName = Object.keys(subResult[0])[0];
|
||||||
|
resolved[op] = subResult[0][colName];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
resolved[op] = operand;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return resolved;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1408,6 +1705,8 @@ var TokenType;
|
|||||||
TokenType["DEFAULT"] = "DEFAULT";
|
TokenType["DEFAULT"] = "DEFAULT";
|
||||||
TokenType["NULL"] = "NULL";
|
TokenType["NULL"] = "NULL";
|
||||||
TokenType["TRUE"] = "TRUE";
|
TokenType["TRUE"] = "TRUE";
|
||||||
|
TokenType["REFERENCES"] = "REFERENCES";
|
||||||
|
TokenType["CASCADE"] = "CASCADE";
|
||||||
TokenType["FALSE"] = "FALSE";
|
TokenType["FALSE"] = "FALSE";
|
||||||
// JOIN 相关
|
// JOIN 相关
|
||||||
TokenType["INNER"] = "INNER";
|
TokenType["INNER"] = "INNER";
|
||||||
@@ -1482,6 +1781,8 @@ const KEYWORDS = {
|
|||||||
'NULL': TokenType.NULL,
|
'NULL': TokenType.NULL,
|
||||||
'TRUE': TokenType.TRUE,
|
'TRUE': TokenType.TRUE,
|
||||||
'FALSE': TokenType.FALSE,
|
'FALSE': TokenType.FALSE,
|
||||||
|
'REFERENCES': TokenType.REFERENCES,
|
||||||
|
'CASCADE': TokenType.CASCADE,
|
||||||
// JOIN
|
// JOIN
|
||||||
'INNER': TokenType.INNER,
|
'INNER': TokenType.INNER,
|
||||||
'LEFT': TokenType.LEFT,
|
'LEFT': TokenType.LEFT,
|
||||||
@@ -1990,7 +2291,8 @@ class Parser {
|
|||||||
while (this.curTokenIs(TokenType.PRIMARY) ||
|
while (this.curTokenIs(TokenType.PRIMARY) ||
|
||||||
this.curTokenIs(TokenType.UNIQUE) ||
|
this.curTokenIs(TokenType.UNIQUE) ||
|
||||||
this.curTokenIs(TokenType.NOT) ||
|
this.curTokenIs(TokenType.NOT) ||
|
||||||
this.curTokenIs(TokenType.DEFAULT)) {
|
this.curTokenIs(TokenType.DEFAULT) ||
|
||||||
|
this.curTokenIs(TokenType.REFERENCES)) {
|
||||||
if (this.curTokenIs(TokenType.PRIMARY)) {
|
if (this.curTokenIs(TokenType.PRIMARY)) {
|
||||||
this.nextToken();
|
this.nextToken();
|
||||||
this.expect(TokenType.KEY);
|
this.expect(TokenType.KEY);
|
||||||
@@ -2002,7 +2304,6 @@ class Parser {
|
|||||||
}
|
}
|
||||||
else if (this.curTokenIs(TokenType.NOT)) {
|
else if (this.curTokenIs(TokenType.NOT)) {
|
||||||
this.nextToken();
|
this.nextToken();
|
||||||
// 支持 NOT NULL → required
|
|
||||||
this.expect(TokenType.NULL);
|
this.expect(TokenType.NULL);
|
||||||
col.required = true;
|
col.required = true;
|
||||||
}
|
}
|
||||||
@@ -2010,12 +2311,53 @@ class Parser {
|
|||||||
this.nextToken();
|
this.nextToken();
|
||||||
col.default = this.parseValue();
|
col.default = this.parseValue();
|
||||||
}
|
}
|
||||||
|
else if (this.curTokenIs(TokenType.REFERENCES)) {
|
||||||
|
this.nextToken();
|
||||||
|
const refTable = this.expectIdentifier('referenced table');
|
||||||
|
this.expect(TokenType.LPAREN);
|
||||||
|
const refCol = this.expectIdentifier('referenced column');
|
||||||
|
this.expect(TokenType.RPAREN);
|
||||||
|
col.references = `${refTable}.${refCol}`;
|
||||||
|
// ON DELETE / ON UPDATE
|
||||||
|
while (this.curTokenIs(TokenType.ON)) {
|
||||||
|
this.nextToken();
|
||||||
|
if (this.curTokenIs(TokenType.DELETE)) {
|
||||||
|
this.nextToken();
|
||||||
|
col.onDelete = this.parseCascadeAction();
|
||||||
|
}
|
||||||
|
else if (this.curTokenIs(TokenType.UPDATE)) {
|
||||||
|
this.nextToken();
|
||||||
|
col.onUpdate = this.parseCascadeAction();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
else {
|
else {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return col;
|
return col;
|
||||||
}
|
}
|
||||||
|
/** 解析 CASCADE | SET NULL | RESTRICT */
|
||||||
|
parseCascadeAction() {
|
||||||
|
if (this.curTokenIs(TokenType.CASCADE)) {
|
||||||
|
this.nextToken();
|
||||||
|
return 'CASCADE';
|
||||||
|
}
|
||||||
|
if (this.curTokenIs(TokenType.SET)) {
|
||||||
|
this.nextToken();
|
||||||
|
this.expect(TokenType.NULL);
|
||||||
|
return 'SET NULL';
|
||||||
|
}
|
||||||
|
// RESTRICT 或默认
|
||||||
|
if (this.curToken.type === TokenType.IDENTIFIER && this.curToken.value.toUpperCase() === 'RESTRICT') {
|
||||||
|
this.nextToken();
|
||||||
|
return 'RESTRICT';
|
||||||
|
}
|
||||||
|
return 'RESTRICT';
|
||||||
|
}
|
||||||
// ===================================================================
|
// ===================================================================
|
||||||
// DROP TABLE
|
// DROP TABLE
|
||||||
// ===================================================================
|
// ===================================================================
|
||||||
@@ -2086,6 +2428,14 @@ class Parser {
|
|||||||
if (this.curTokenIs(TokenType.IN)) {
|
if (this.curTokenIs(TokenType.IN)) {
|
||||||
this.nextToken();
|
this.nextToken();
|
||||||
this.expect(TokenType.LPAREN);
|
this.expect(TokenType.LPAREN);
|
||||||
|
// 子查询: IN (SELECT ...)
|
||||||
|
if (this.curTokenIs(TokenType.SELECT)) {
|
||||||
|
const subquery = this.parseSelect();
|
||||||
|
this.expect(TokenType.RPAREN);
|
||||||
|
const result = {};
|
||||||
|
result[column] = { $in: { $subquery: subquery } };
|
||||||
|
return result;
|
||||||
|
}
|
||||||
const values = this.parseValueList();
|
const values = this.parseValueList();
|
||||||
this.expect(TokenType.RPAREN);
|
this.expect(TokenType.RPAREN);
|
||||||
const result = {};
|
const result = {};
|
||||||
@@ -2094,6 +2444,15 @@ class Parser {
|
|||||||
}
|
}
|
||||||
// 比较运算符
|
// 比较运算符
|
||||||
const op = this.parseComparisonOp();
|
const op = this.parseComparisonOp();
|
||||||
|
// 子查询: op (SELECT ...)
|
||||||
|
if (this.curTokenIs(TokenType.LPAREN) && this.peekTokenIs(TokenType.SELECT)) {
|
||||||
|
this.nextToken(); // skip (
|
||||||
|
const subquery = this.parseSelect();
|
||||||
|
this.expect(TokenType.RPAREN);
|
||||||
|
const result = {};
|
||||||
|
result[column] = { [op]: { $subquery: subquery } };
|
||||||
|
return result;
|
||||||
|
}
|
||||||
// 尝试解析列引用(identifier DOT identifier 格式)
|
// 尝试解析列引用(identifier DOT identifier 格式)
|
||||||
let value;
|
let value;
|
||||||
if ((this.curToken.type === TokenType.IDENTIFIER || this._isKeywordAsIdent()) &&
|
if ((this.curToken.type === TokenType.IDENTIFIER || this._isKeywordAsIdent()) &&
|
||||||
@@ -2332,8 +2691,7 @@ function parse(sql) {
|
|||||||
* metona-sqlark Transaction — 事务管理
|
* metona-sqlark Transaction — 事务管理
|
||||||
* @module transaction
|
* @module transaction
|
||||||
*
|
*
|
||||||
* 封装多操作事务,保证原子性。
|
* v0.1.13: 支持真正的回滚 — 利用引擎层 begin/commit/rollback 实现原子性。
|
||||||
* v0.0.1: 基于引擎层操作实现,IndexedDB 引擎利用原生事务。
|
|
||||||
*/
|
*/
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Transaction
|
// Transaction
|
||||||
@@ -2369,15 +2727,21 @@ class TransactionManager {
|
|||||||
constructor(engine) {
|
constructor(engine) {
|
||||||
this.engine = engine;
|
this.engine = engine;
|
||||||
}
|
}
|
||||||
/** 执行事务 */
|
/** 执行事务 — 支持自动回滚 */
|
||||||
async execute(fn) {
|
async execute(fn) {
|
||||||
const trx = new Transaction(this.engine);
|
const trx = new Transaction(this.engine);
|
||||||
|
// 开始引擎层事务
|
||||||
|
await this.engine.beginTransaction();
|
||||||
try {
|
try {
|
||||||
const result = await fn(trx);
|
const result = await fn(trx);
|
||||||
|
// 成功 → 提交
|
||||||
|
await this.engine.commitTransaction();
|
||||||
trx._markCompleted();
|
trx._markCompleted();
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
catch (error) {
|
catch (error) {
|
||||||
|
// 失败 → 回滚
|
||||||
|
await this.engine.rollbackTransaction();
|
||||||
if (error instanceof DatabaseError)
|
if (error instanceof DatabaseError)
|
||||||
throw error;
|
throw error;
|
||||||
throw new DatabaseError(`Transaction failed: ${error.message}`, 'TRANSACTION_ERROR', error);
|
throw new DatabaseError(`Transaction failed: ${error.message}`, 'TRANSACTION_ERROR', error);
|
||||||
@@ -2660,6 +3024,121 @@ class MetonaSqlark {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* metona-sqlark Connection Manager — 数据库实例连接池
|
||||||
|
* @module connection-manager
|
||||||
|
*
|
||||||
|
* v0.1.13: 避免重复创建同名数据库实例,通过 connect() 复用已有连接。
|
||||||
|
* 管理实例生命周期,防止重复 open IndexedDB。
|
||||||
|
*/
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// ConnectionManager
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
class ConnectionManager {
|
||||||
|
constructor() {
|
||||||
|
/** 活跃连接:dbName → MetonaSqlark */
|
||||||
|
this.connections = new Map();
|
||||||
|
/** 连接引用计数:dbName → count */
|
||||||
|
this.refCount = new Map();
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 获取或创建数据库实例
|
||||||
|
*
|
||||||
|
* 如果同名数据库已打开,复用已有实例并增加引用计数。
|
||||||
|
* 否则创建新实例。
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```ts
|
||||||
|
* const db = await MetonaSqlark.connect({ name: 'my-app', mode: 'hybrid' });
|
||||||
|
* // ... use db
|
||||||
|
* await db.disconnect(); // 引用计数 -1,归零时自动关闭
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
async connect(config) {
|
||||||
|
const name = config.name;
|
||||||
|
// 已有连接 → 复用
|
||||||
|
const existing = this.connections.get(name);
|
||||||
|
if (existing && existing.isReady()) {
|
||||||
|
const count = (this.refCount.get(name) ?? 0) + 1;
|
||||||
|
this.refCount.set(name, count);
|
||||||
|
return existing;
|
||||||
|
}
|
||||||
|
// 创建新连接
|
||||||
|
const db = new MetonaSqlark(config);
|
||||||
|
await db.init();
|
||||||
|
this.connections.set(name, db);
|
||||||
|
this.refCount.set(name, 1);
|
||||||
|
// 注入 disconnect 方法
|
||||||
|
db.disconnect = async () => {
|
||||||
|
await this.release(name);
|
||||||
|
};
|
||||||
|
return db;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 释放连接引用。引用计数归零时自动关闭数据库。
|
||||||
|
*/
|
||||||
|
async release(dbName) {
|
||||||
|
const count = (this.refCount.get(dbName) ?? 1) - 1;
|
||||||
|
if (count <= 0) {
|
||||||
|
const db = this.connections.get(dbName);
|
||||||
|
if (db) {
|
||||||
|
await db.close();
|
||||||
|
this.connections.delete(dbName);
|
||||||
|
}
|
||||||
|
this.refCount.delete(dbName);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this.refCount.set(dbName, count);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 强制关闭指定数据库(忽略引用计数)
|
||||||
|
*/
|
||||||
|
async forceClose(dbName) {
|
||||||
|
const db = this.connections.get(dbName);
|
||||||
|
if (db) {
|
||||||
|
await db.close();
|
||||||
|
this.connections.delete(dbName);
|
||||||
|
}
|
||||||
|
this.refCount.delete(dbName);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 强制关闭所有连接
|
||||||
|
*/
|
||||||
|
async closeAll() {
|
||||||
|
for (const [, db] of this.connections) {
|
||||||
|
try {
|
||||||
|
await db.close();
|
||||||
|
}
|
||||||
|
catch { /* ignore */ }
|
||||||
|
}
|
||||||
|
this.connections.clear();
|
||||||
|
this.refCount.clear();
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 获取所有活跃连接名
|
||||||
|
*/
|
||||||
|
getActiveConnections() {
|
||||||
|
return Array.from(this.connections.keys());
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 获取连接的引用计数
|
||||||
|
*/
|
||||||
|
getRefCount(dbName) {
|
||||||
|
return this.refCount.get(dbName) ?? 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// 全局单例
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
const manager = new ConnectionManager();
|
||||||
|
// 挂载到 MetonaSqlark 静态方法(通过 any 绕过 TS 类型检查)
|
||||||
|
const M = MetonaSqlark;
|
||||||
|
M.connect = (config) => manager.connect(config);
|
||||||
|
M.disconnect = (dbName) => manager.release(dbName);
|
||||||
|
M.disconnectAll = () => manager.closeAll();
|
||||||
|
M.getActiveConnections = () => manager.getActiveConnections();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* metona-sqlark — 入口文件
|
* metona-sqlark — 入口文件
|
||||||
* @module metona-sqlark
|
* @module metona-sqlark
|
||||||
|
|||||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+518
-39
@@ -33,7 +33,7 @@
|
|||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// 版本
|
// 版本
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
const VERSION = '0.1.12';
|
const VERSION = '0.1.13';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* metona-sqlark Shared WHERE Matcher — 统一的条件匹配逻辑
|
* metona-sqlark Shared WHERE Matcher — 统一的条件匹配逻辑
|
||||||
@@ -202,6 +202,8 @@
|
|||||||
this.schemas = new Map();
|
this.schemas = new Map();
|
||||||
this.indexes = new Map();
|
this.indexes = new Map();
|
||||||
this.opened = false;
|
this.opened = false;
|
||||||
|
// ---- 事务快照 ----
|
||||||
|
this.snapshot = null;
|
||||||
}
|
}
|
||||||
// ---- 生命周期 ----
|
// ---- 生命周期 ----
|
||||||
async open(_dbName, _version) { this.opened = true; }
|
async open(_dbName, _version) { this.opened = true; }
|
||||||
@@ -295,9 +297,16 @@
|
|||||||
toDelete.push(pk);
|
toDelete.push(pk);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// 级联删除:检查引用此表的其他表
|
||||||
|
let cascadeCount = 0;
|
||||||
|
for (const pk of toDelete) {
|
||||||
|
const row = table.get(pk);
|
||||||
|
if (row)
|
||||||
|
cascadeCount += await this.cascadeDelete(tableName, pk, row);
|
||||||
|
}
|
||||||
for (const pk of toDelete)
|
for (const pk of toDelete)
|
||||||
table.delete(pk);
|
table.delete(pk);
|
||||||
return toDelete.length;
|
return toDelete.length + cascadeCount;
|
||||||
}
|
}
|
||||||
async count(tableName, query) {
|
async count(tableName, query) {
|
||||||
this.ensureTable(tableName);
|
this.ensureTable(tableName);
|
||||||
@@ -319,6 +328,54 @@
|
|||||||
for (const colIndex of tableIndexes.values())
|
for (const colIndex of tableIndexes.values())
|
||||||
colIndex.clear();
|
colIndex.clear();
|
||||||
}
|
}
|
||||||
|
// ---- 事务 ----
|
||||||
|
async beginTransaction() {
|
||||||
|
if (this.snapshot)
|
||||||
|
throw new DatabaseError('Transaction already in progress', 'TX_ACTIVE');
|
||||||
|
this.snapshot = {
|
||||||
|
tables: this.deepCloneMapMap(this.tables),
|
||||||
|
schemas: new Map(this.schemas),
|
||||||
|
indexes: this.deepCloneIndexes(this.indexes),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
async commitTransaction() {
|
||||||
|
if (!this.snapshot)
|
||||||
|
throw new DatabaseError('No active transaction', 'TX_NONE');
|
||||||
|
this.snapshot = null;
|
||||||
|
}
|
||||||
|
async rollbackTransaction() {
|
||||||
|
if (!this.snapshot)
|
||||||
|
throw new DatabaseError('No active transaction', 'TX_NONE');
|
||||||
|
this.tables = this.snapshot.tables;
|
||||||
|
this.schemas = this.snapshot.schemas;
|
||||||
|
this.indexes = this.snapshot.indexes;
|
||||||
|
this.snapshot = null;
|
||||||
|
}
|
||||||
|
// ---- 事务快照辅助 ----
|
||||||
|
deepCloneMapMap(source) {
|
||||||
|
const clone = new Map();
|
||||||
|
for (const [k, v] of source) {
|
||||||
|
const innerClone = new Map();
|
||||||
|
for (const [ik, iv] of v)
|
||||||
|
innerClone.set(ik, { ...iv });
|
||||||
|
clone.set(k, innerClone);
|
||||||
|
}
|
||||||
|
return clone;
|
||||||
|
}
|
||||||
|
deepCloneIndexes(source) {
|
||||||
|
const clone = new Map();
|
||||||
|
for (const [tableName, tableIndexes] of source) {
|
||||||
|
const tableClone = new Map();
|
||||||
|
for (const [col, colIndex] of tableIndexes) {
|
||||||
|
const colClone = new Map();
|
||||||
|
for (const [val, pkSet] of colIndex)
|
||||||
|
colClone.set(val, new Set(pkSet));
|
||||||
|
tableClone.set(col, colClone);
|
||||||
|
}
|
||||||
|
clone.set(tableName, tableClone);
|
||||||
|
}
|
||||||
|
return clone;
|
||||||
|
}
|
||||||
// ---- 内部辅助 ----
|
// ---- 内部辅助 ----
|
||||||
ensureTable(tableName) {
|
ensureTable(tableName) {
|
||||||
if (!this.tables.has(tableName))
|
if (!this.tables.has(tableName))
|
||||||
@@ -425,11 +482,62 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// ---- 外键级联 ----
|
||||||
|
/**
|
||||||
|
* 级联删除:查找引用 tableName.pkValue 的所有表的行并删除。
|
||||||
|
* @returns 级联删除的行数
|
||||||
|
*/
|
||||||
|
async cascadeDelete(tableName, pkValue, _row) {
|
||||||
|
let totalCascade = 0;
|
||||||
|
for (const [refTableName, refSchema] of this.schemas) {
|
||||||
|
if (refTableName === tableName)
|
||||||
|
continue;
|
||||||
|
for (const [colName, colDef] of Object.entries(refSchema.columns)) {
|
||||||
|
if (!colDef.references || !colDef.onDelete || colDef.onDelete === 'RESTRICT')
|
||||||
|
continue;
|
||||||
|
const [refTable, refCol] = colDef.references.split('.');
|
||||||
|
if (refTable !== tableName)
|
||||||
|
continue;
|
||||||
|
const refTableData = this.tables.get(refTableName);
|
||||||
|
if (!refTableData)
|
||||||
|
continue;
|
||||||
|
// 查找所有引用此主键的行
|
||||||
|
const toDelete = [];
|
||||||
|
for (const [refPk, refRow] of refTableData) {
|
||||||
|
if (String(refRow[colName]) === pkValue) {
|
||||||
|
toDelete.push(refPk);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (colDef.onDelete === 'CASCADE') {
|
||||||
|
// 递归级联
|
||||||
|
for (const refPk of toDelete) {
|
||||||
|
const refRow = refTableData.get(refPk);
|
||||||
|
if (refRow) {
|
||||||
|
totalCascade += await this.cascadeDelete(refTableName, refPk, refRow);
|
||||||
|
}
|
||||||
|
refTableData.delete(refPk);
|
||||||
|
totalCascade++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (colDef.onDelete === 'SET NULL') {
|
||||||
|
for (const refPk of toDelete) {
|
||||||
|
const refRow = refTableData.get(refPk);
|
||||||
|
if (refRow) {
|
||||||
|
refRow[colName] = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return totalCascade;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* metona-sqlark IndexedDB Engine — 基于 IndexedDB 的持久化存储引擎
|
* metona-sqlark IndexedDB Engine — 基于 IndexedDB 的持久化存储引擎
|
||||||
* @module engine/indexeddb
|
* @module engine/indexeddb
|
||||||
|
*
|
||||||
|
* v0.1.13: 支持事务 — beginTransaction 延迟 IDB 写入,commit 批量刷盘,rollback 恢复快照。
|
||||||
*/
|
*/
|
||||||
class IndexedDBEngine {
|
class IndexedDBEngine {
|
||||||
constructor() {
|
constructor() {
|
||||||
@@ -438,6 +546,8 @@
|
|||||||
this.dbName = '';
|
this.dbName = '';
|
||||||
this.version = 1;
|
this.version = 1;
|
||||||
this.memoryCache = new MemoryEngine();
|
this.memoryCache = new MemoryEngine();
|
||||||
|
// ---- 事务状态 ----
|
||||||
|
this.txActive = false;
|
||||||
}
|
}
|
||||||
async open(dbName, version) {
|
async open(dbName, version) {
|
||||||
this.dbName = dbName;
|
this.dbName = dbName;
|
||||||
@@ -461,6 +571,85 @@
|
|||||||
// ---- 表管理 ----
|
// ---- 表管理 ----
|
||||||
async createTable(schema) {
|
async createTable(schema) {
|
||||||
await this.memoryCache.createTable(schema);
|
await this.memoryCache.createTable(schema);
|
||||||
|
if (this.txActive)
|
||||||
|
return; // 事务中延迟 IDB 操作
|
||||||
|
await this.idbCreateTable(schema);
|
||||||
|
}
|
||||||
|
async dropTable(tableName) {
|
||||||
|
await this.memoryCache.dropTable(tableName);
|
||||||
|
if (this.txActive)
|
||||||
|
return;
|
||||||
|
await this.idbDropTable(tableName);
|
||||||
|
}
|
||||||
|
async hasTable(tableName) { return this.ensureDB().objectStoreNames.contains(tableName); }
|
||||||
|
async getTableNames() { return Array.from(this.ensureDB().objectStoreNames); }
|
||||||
|
async getTableSchema(tableName) { return this.memoryCache.getTableSchema(tableName); }
|
||||||
|
// ---- CRUD ----
|
||||||
|
async insert(tableName, rows) {
|
||||||
|
const pks = await this.memoryCache.insert(tableName, rows);
|
||||||
|
if (this.txActive)
|
||||||
|
return pks; // 事务中延迟写入
|
||||||
|
await this.idbInsert(tableName, rows);
|
||||||
|
return pks;
|
||||||
|
}
|
||||||
|
async find(tableName, query) {
|
||||||
|
// 事务中从内存缓存读取(保证读到未提交的变更),否则走 IDB
|
||||||
|
if (this.txActive)
|
||||||
|
return this.memoryCache.find(tableName, query);
|
||||||
|
return this.idbFind(tableName, query);
|
||||||
|
}
|
||||||
|
async update(tableName, query, updates) {
|
||||||
|
const count = await this.memoryCache.update(tableName, query, updates);
|
||||||
|
if (this.txActive)
|
||||||
|
return count;
|
||||||
|
await this.idbUpdate(tableName, query, updates);
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
async delete(tableName, query) {
|
||||||
|
const count = await this.memoryCache.delete(tableName, query);
|
||||||
|
if (this.txActive)
|
||||||
|
return count;
|
||||||
|
await this.idbDelete(tableName, query);
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
async count(tableName, query) {
|
||||||
|
if (this.txActive)
|
||||||
|
return this.memoryCache.count(tableName, query);
|
||||||
|
const results = await this.idbFind(tableName, { table: tableName, where: query?.where ?? {} });
|
||||||
|
return results.length;
|
||||||
|
}
|
||||||
|
async clear(tableName) {
|
||||||
|
await this.memoryCache.clear(tableName);
|
||||||
|
if (this.txActive)
|
||||||
|
return;
|
||||||
|
await this.idbClear(tableName);
|
||||||
|
}
|
||||||
|
// ---- 事务 ----
|
||||||
|
async beginTransaction() {
|
||||||
|
if (this.txActive)
|
||||||
|
throw new DatabaseError('Transaction already in progress', 'TX_ACTIVE');
|
||||||
|
this.txActive = true;
|
||||||
|
// 保存内存快照到 MemoryEngine 内部的 beginTransaction
|
||||||
|
await this.memoryCache.beginTransaction();
|
||||||
|
}
|
||||||
|
async commitTransaction() {
|
||||||
|
if (!this.txActive)
|
||||||
|
throw new DatabaseError('No active transaction', 'TX_NONE');
|
||||||
|
// 确认内存层的变更
|
||||||
|
await this.memoryCache.commitTransaction();
|
||||||
|
// 批量将内存数据刷到 IndexedDB
|
||||||
|
await this.flushToIDB();
|
||||||
|
this.txActive = false;
|
||||||
|
}
|
||||||
|
async rollbackTransaction() {
|
||||||
|
if (!this.txActive)
|
||||||
|
throw new DatabaseError('No active transaction', 'TX_NONE');
|
||||||
|
// 恢复内存层到快照状态
|
||||||
|
await this.memoryCache.rollbackTransaction();
|
||||||
|
this.txActive = false;
|
||||||
|
}
|
||||||
|
// ---- IDB 原生操作 ----
|
||||||
|
async idbCreateTable(schema) {
|
||||||
const db = this.ensureDB();
|
const db = this.ensureDB();
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const newVersion = db.version + 1;
|
const newVersion = db.version + 1;
|
||||||
@@ -480,8 +669,7 @@
|
|||||||
request.onerror = () => reject(new DatabaseError(`Failed to create table "${schema.name}"`, 'IDB_UPGRADE_ERROR', request.error));
|
request.onerror = () => reject(new DatabaseError(`Failed to create table "${schema.name}"`, 'IDB_UPGRADE_ERROR', request.error));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
async dropTable(tableName) {
|
async idbDropTable(tableName) {
|
||||||
await this.memoryCache.dropTable(tableName);
|
|
||||||
const db = this.ensureDB();
|
const db = this.ensureDB();
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const newVersion = db.version + 1;
|
const newVersion = db.version + 1;
|
||||||
@@ -496,27 +684,18 @@
|
|||||||
request.onerror = () => reject(new DatabaseError(`Failed to drop table "${tableName}"`, 'IDB_UPGRADE_ERROR', request.error));
|
request.onerror = () => reject(new DatabaseError(`Failed to drop table "${tableName}"`, 'IDB_UPGRADE_ERROR', request.error));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
async hasTable(tableName) { return this.ensureDB().objectStoreNames.contains(tableName); }
|
async idbInsert(tableName, rows) {
|
||||||
async getTableNames() { return Array.from(this.ensureDB().objectStoreNames); }
|
|
||||||
async getTableSchema(tableName) { return this.memoryCache.getTableSchema(tableName); }
|
|
||||||
// ---- CRUD ----
|
|
||||||
async insert(tableName, rows) {
|
|
||||||
await this.memoryCache.insert(tableName, rows);
|
|
||||||
const db = this.ensureDB();
|
const db = this.ensureDB();
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const tx = db.transaction(tableName, 'readwrite');
|
const tx = db.transaction(tableName, 'readwrite');
|
||||||
const store = tx.objectStore(tableName);
|
const store = tx.objectStore(tableName);
|
||||||
const pks = [];
|
for (const row of rows)
|
||||||
for (const row of rows) {
|
store.add(row);
|
||||||
const req = store.add(row);
|
tx.oncomplete = () => resolve();
|
||||||
req.onsuccess = () => pks.push(String(req.result));
|
|
||||||
req.onerror = () => { }; // 内存层已校验,忽略 IDB 重复键
|
|
||||||
}
|
|
||||||
tx.oncomplete = () => resolve(pks);
|
|
||||||
tx.onerror = () => reject(new DatabaseError(`Insert failed for "${tableName}"`, 'IDB_TX_ERROR', tx.error));
|
tx.onerror = () => reject(new DatabaseError(`Insert failed for "${tableName}"`, 'IDB_TX_ERROR', tx.error));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
async find(tableName, query) {
|
async idbFind(tableName, query) {
|
||||||
const db = this.ensureDB();
|
const db = this.ensureDB();
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const tx = db.transaction(tableName, 'readonly');
|
const tx = db.transaction(tableName, 'readonly');
|
||||||
@@ -540,29 +719,25 @@
|
|||||||
req.onerror = () => reject(new DatabaseError(`Find failed for "${tableName}"`, 'IDB_READ_ERROR', req.error));
|
req.onerror = () => reject(new DatabaseError(`Find failed for "${tableName}"`, 'IDB_READ_ERROR', req.error));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
async update(tableName, query, updates) {
|
async idbUpdate(tableName, query, updates) {
|
||||||
await this.memoryCache.update(tableName, query, updates);
|
|
||||||
const db = this.ensureDB();
|
const db = this.ensureDB();
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const tx = db.transaction(tableName, 'readwrite');
|
const tx = db.transaction(tableName, 'readwrite');
|
||||||
const store = tx.objectStore(tableName);
|
const store = tx.objectStore(tableName);
|
||||||
const getAllReq = store.getAll();
|
const getAllReq = store.getAll();
|
||||||
let count = 0;
|
|
||||||
getAllReq.onsuccess = () => {
|
getAllReq.onsuccess = () => {
|
||||||
for (const row of getAllReq.result ?? []) {
|
for (const row of getAllReq.result ?? []) {
|
||||||
if (!query.where || Object.keys(query.where).length === 0 || matchWhere(row, query.where)) {
|
if (!query.where || Object.keys(query.where).length === 0 || matchWhere(row, query.where)) {
|
||||||
Object.assign(row, updates);
|
Object.assign(row, updates);
|
||||||
store.put(row);
|
store.put(row);
|
||||||
count++;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
tx.oncomplete = () => resolve(count);
|
tx.oncomplete = () => resolve();
|
||||||
tx.onerror = () => reject(new DatabaseError(`Update failed for "${tableName}"`, 'IDB_TX_ERROR', tx.error));
|
tx.onerror = () => reject(new DatabaseError(`Update failed for "${tableName}"`, 'IDB_TX_ERROR', tx.error));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
async delete(tableName, query) {
|
async idbDelete(tableName, query) {
|
||||||
await this.memoryCache.delete(tableName, query);
|
|
||||||
const db = this.ensureDB();
|
const db = this.ensureDB();
|
||||||
const schema = await this.memoryCache.getTableSchema(tableName);
|
const schema = await this.memoryCache.getTableSchema(tableName);
|
||||||
if (!schema)
|
if (!schema)
|
||||||
@@ -572,25 +747,18 @@
|
|||||||
const tx = db.transaction(tableName, 'readwrite');
|
const tx = db.transaction(tableName, 'readwrite');
|
||||||
const store = tx.objectStore(tableName);
|
const store = tx.objectStore(tableName);
|
||||||
const getAllReq = store.getAll();
|
const getAllReq = store.getAll();
|
||||||
let count = 0;
|
|
||||||
getAllReq.onsuccess = () => {
|
getAllReq.onsuccess = () => {
|
||||||
for (const row of getAllReq.result ?? []) {
|
for (const row of getAllReq.result ?? []) {
|
||||||
if (!query.where || Object.keys(query.where).length === 0 || matchWhere(row, query.where)) {
|
if (!query.where || Object.keys(query.where).length === 0 || matchWhere(row, query.where)) {
|
||||||
store.delete(row[pkColumn]);
|
store.delete(row[pkColumn]);
|
||||||
count++;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
tx.oncomplete = () => resolve(count);
|
tx.oncomplete = () => resolve();
|
||||||
tx.onerror = () => reject(new DatabaseError(`Delete failed for "${tableName}"`, 'IDB_TX_ERROR', tx.error));
|
tx.onerror = () => reject(new DatabaseError(`Delete failed for "${tableName}"`, 'IDB_TX_ERROR', tx.error));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
async count(tableName, query) {
|
async idbClear(tableName) {
|
||||||
const results = await this.find(tableName, { table: tableName, where: query?.where ?? {} });
|
|
||||||
return results.length;
|
|
||||||
}
|
|
||||||
async clear(tableName) {
|
|
||||||
await this.memoryCache.clear(tableName);
|
|
||||||
const db = this.ensureDB();
|
const db = this.ensureDB();
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const tx = db.transaction(tableName, 'readwrite');
|
const tx = db.transaction(tableName, 'readwrite');
|
||||||
@@ -599,6 +767,18 @@
|
|||||||
tx.onerror = () => reject(new DatabaseError(`Clear failed for "${tableName}"`, 'IDB_TX_ERROR', tx.error));
|
tx.onerror = () => reject(new DatabaseError(`Clear failed for "${tableName}"`, 'IDB_TX_ERROR', tx.error));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
/** 将内存缓存中的所有表数据刷新到 IndexedDB */
|
||||||
|
async flushToIDB() {
|
||||||
|
const tableNames = await this.memoryCache.getTableNames();
|
||||||
|
for (const tableName of tableNames) {
|
||||||
|
const rows = await this.memoryCache.find(tableName, { table: tableName });
|
||||||
|
// Clear + bulk insert for simplicity
|
||||||
|
await this.idbClear(tableName);
|
||||||
|
if (rows.length > 0) {
|
||||||
|
await this.idbInsert(tableName, rows);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
ensureDB() {
|
ensureDB() {
|
||||||
if (!this.db)
|
if (!this.db)
|
||||||
throw new DatabaseError('Database not opened', 'DB_NOT_OPEN');
|
throw new DatabaseError('Database not opened', 'DB_NOT_OPEN');
|
||||||
@@ -714,6 +894,22 @@
|
|||||||
await this.memoryCache.clear(tableName);
|
await this.memoryCache.clear(tableName);
|
||||||
await this.writeTableData(tableName, []);
|
await this.writeTableData(tableName, []);
|
||||||
}
|
}
|
||||||
|
// ---- 事务 ----
|
||||||
|
async beginTransaction() {
|
||||||
|
await this.memoryCache.beginTransaction();
|
||||||
|
}
|
||||||
|
async commitTransaction() {
|
||||||
|
await this.memoryCache.commitTransaction();
|
||||||
|
// 将内存数据刷到 OPFS
|
||||||
|
const tableNames = await this.memoryCache.getTableNames();
|
||||||
|
for (const tableName of tableNames) {
|
||||||
|
const rows = await this.memoryCache.find(tableName, { table: tableName });
|
||||||
|
await this.writeTableData(tableName, rows);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async rollbackTransaction() {
|
||||||
|
await this.memoryCache.rollbackTransaction();
|
||||||
|
}
|
||||||
// ---- 内部辅助 ----
|
// ---- 内部辅助 ----
|
||||||
ensureDir() {
|
ensureDir() {
|
||||||
if (!this.tablesDir) {
|
if (!this.tablesDir) {
|
||||||
@@ -856,6 +1052,19 @@
|
|||||||
await this.memoryEngine.clear(tableName);
|
await this.memoryEngine.clear(tableName);
|
||||||
await this.diskEngine.clear(tableName);
|
await this.diskEngine.clear(tableName);
|
||||||
}
|
}
|
||||||
|
// ---- 事务 ----
|
||||||
|
async beginTransaction() {
|
||||||
|
await this.memoryEngine.beginTransaction();
|
||||||
|
await this.diskEngine.beginTransaction();
|
||||||
|
}
|
||||||
|
async commitTransaction() {
|
||||||
|
await this.memoryEngine.commitTransaction();
|
||||||
|
await this.diskEngine.commitTransaction();
|
||||||
|
}
|
||||||
|
async rollbackTransaction() {
|
||||||
|
await this.memoryEngine.rollbackTransaction();
|
||||||
|
await this.diskEngine.rollbackTransaction();
|
||||||
|
}
|
||||||
// ---- 引擎信息 ----
|
// ---- 引擎信息 ----
|
||||||
/** 获取磁盘引擎类型 */
|
/** 获取磁盘引擎类型 */
|
||||||
getDiskEngineType() {
|
getDiskEngineType() {
|
||||||
@@ -1113,6 +1322,9 @@
|
|||||||
maxLength: astCol.maxLength,
|
maxLength: astCol.maxLength,
|
||||||
min: astCol.min,
|
min: astCol.min,
|
||||||
max: astCol.max,
|
max: astCol.max,
|
||||||
|
references: astCol.references,
|
||||||
|
onDelete: astCol.onDelete,
|
||||||
|
onUpdate: astCol.onUpdate,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1193,6 +1405,10 @@
|
|||||||
// SELECT
|
// SELECT
|
||||||
// ===================================================================
|
// ===================================================================
|
||||||
async executeSelect(stmt) {
|
async executeSelect(stmt) {
|
||||||
|
// 先解析子查询
|
||||||
|
if (stmt.where && Object.keys(stmt.where).length > 0) {
|
||||||
|
stmt.where = await this.resolveSubqueries(stmt.where);
|
||||||
|
}
|
||||||
const hasGroupBy = !!(stmt.groupBy && stmt.groupBy.length > 0);
|
const hasGroupBy = !!(stmt.groupBy && stmt.groupBy.length > 0);
|
||||||
let rows;
|
let rows;
|
||||||
if (!stmt.joins || stmt.joins.length === 0) {
|
if (!stmt.joins || stmt.joins.length === 0) {
|
||||||
@@ -1373,6 +1589,87 @@
|
|||||||
return this.engine.dropTable(stmt.name);
|
return this.engine.dropTable(stmt.name);
|
||||||
}
|
}
|
||||||
getEngine() { return this.engine; }
|
getEngine() { return this.engine; }
|
||||||
|
// ===================================================================
|
||||||
|
// 子查询解析
|
||||||
|
// ===================================================================
|
||||||
|
/**
|
||||||
|
* 递归扫描 WHERE 条件,找到 $subquery 标记并执行子查询,
|
||||||
|
* 将结果替换为具体值。
|
||||||
|
*/
|
||||||
|
async resolveSubqueries(where) {
|
||||||
|
const resolved = {};
|
||||||
|
for (const [key, value] of Object.entries(where)) {
|
||||||
|
// 逻辑组合操作符
|
||||||
|
if (key === '$and' && Array.isArray(value)) {
|
||||||
|
resolved.$and = await Promise.all(value.map((sub) => this.resolveSubqueries(sub)));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (key === '$or' && Array.isArray(value)) {
|
||||||
|
resolved.$or = await Promise.all(value.map((sub) => this.resolveSubqueries(sub)));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (key === '$not' && typeof value === 'object' && value !== null) {
|
||||||
|
resolved.$not = await this.resolveSubqueries(value);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// 字段条件
|
||||||
|
if (typeof value === 'object' && value !== null) {
|
||||||
|
resolved[key] = await this.resolveOperatorSubqueries(value);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
resolved[key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return resolved;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 解析操作符值中嵌套的子查询
|
||||||
|
*/
|
||||||
|
async resolveOperatorSubqueries(ops) {
|
||||||
|
const resolved = {};
|
||||||
|
for (const [op, operand] of Object.entries(ops)) {
|
||||||
|
// 处理嵌套 $and/$or(在字段级条件中)
|
||||||
|
if (op === '$and' && Array.isArray(operand)) {
|
||||||
|
resolved.$and = await Promise.all(operand.map((sub) => this.resolveSubqueries(sub)));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (op === '$or' && Array.isArray(operand)) {
|
||||||
|
resolved.$or = await Promise.all(operand.map((sub) => this.resolveSubqueries(sub)));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (op === '$not') {
|
||||||
|
resolved.$not = typeof operand === 'object' && operand !== null
|
||||||
|
? await this.resolveOperatorSubqueries(operand)
|
||||||
|
: operand;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// 子查询检测
|
||||||
|
if (typeof operand === 'object' && operand !== null && '$subquery' in operand) {
|
||||||
|
const subStmt = operand.$subquery;
|
||||||
|
const subResult = await this.executeSelect(subStmt);
|
||||||
|
if (op === '$in' || op === '$nin') {
|
||||||
|
// IN 子查询 → 提取第一列的值列表
|
||||||
|
const colName = Object.keys(subResult[0] || {})[0];
|
||||||
|
const values = subResult.map((row) => row[colName]);
|
||||||
|
resolved[op] = values;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// 标量子查询 → 取第一行第一列
|
||||||
|
if (subResult.length === 0) {
|
||||||
|
resolved[op] = null;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
const colName = Object.keys(subResult[0])[0];
|
||||||
|
resolved[op] = subResult[0][colName];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
resolved[op] = operand;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return resolved;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1414,6 +1711,8 @@
|
|||||||
TokenType["DEFAULT"] = "DEFAULT";
|
TokenType["DEFAULT"] = "DEFAULT";
|
||||||
TokenType["NULL"] = "NULL";
|
TokenType["NULL"] = "NULL";
|
||||||
TokenType["TRUE"] = "TRUE";
|
TokenType["TRUE"] = "TRUE";
|
||||||
|
TokenType["REFERENCES"] = "REFERENCES";
|
||||||
|
TokenType["CASCADE"] = "CASCADE";
|
||||||
TokenType["FALSE"] = "FALSE";
|
TokenType["FALSE"] = "FALSE";
|
||||||
// JOIN 相关
|
// JOIN 相关
|
||||||
TokenType["INNER"] = "INNER";
|
TokenType["INNER"] = "INNER";
|
||||||
@@ -1488,6 +1787,8 @@
|
|||||||
'NULL': TokenType.NULL,
|
'NULL': TokenType.NULL,
|
||||||
'TRUE': TokenType.TRUE,
|
'TRUE': TokenType.TRUE,
|
||||||
'FALSE': TokenType.FALSE,
|
'FALSE': TokenType.FALSE,
|
||||||
|
'REFERENCES': TokenType.REFERENCES,
|
||||||
|
'CASCADE': TokenType.CASCADE,
|
||||||
// JOIN
|
// JOIN
|
||||||
'INNER': TokenType.INNER,
|
'INNER': TokenType.INNER,
|
||||||
'LEFT': TokenType.LEFT,
|
'LEFT': TokenType.LEFT,
|
||||||
@@ -1996,7 +2297,8 @@
|
|||||||
while (this.curTokenIs(TokenType.PRIMARY) ||
|
while (this.curTokenIs(TokenType.PRIMARY) ||
|
||||||
this.curTokenIs(TokenType.UNIQUE) ||
|
this.curTokenIs(TokenType.UNIQUE) ||
|
||||||
this.curTokenIs(TokenType.NOT) ||
|
this.curTokenIs(TokenType.NOT) ||
|
||||||
this.curTokenIs(TokenType.DEFAULT)) {
|
this.curTokenIs(TokenType.DEFAULT) ||
|
||||||
|
this.curTokenIs(TokenType.REFERENCES)) {
|
||||||
if (this.curTokenIs(TokenType.PRIMARY)) {
|
if (this.curTokenIs(TokenType.PRIMARY)) {
|
||||||
this.nextToken();
|
this.nextToken();
|
||||||
this.expect(TokenType.KEY);
|
this.expect(TokenType.KEY);
|
||||||
@@ -2008,7 +2310,6 @@
|
|||||||
}
|
}
|
||||||
else if (this.curTokenIs(TokenType.NOT)) {
|
else if (this.curTokenIs(TokenType.NOT)) {
|
||||||
this.nextToken();
|
this.nextToken();
|
||||||
// 支持 NOT NULL → required
|
|
||||||
this.expect(TokenType.NULL);
|
this.expect(TokenType.NULL);
|
||||||
col.required = true;
|
col.required = true;
|
||||||
}
|
}
|
||||||
@@ -2016,12 +2317,53 @@
|
|||||||
this.nextToken();
|
this.nextToken();
|
||||||
col.default = this.parseValue();
|
col.default = this.parseValue();
|
||||||
}
|
}
|
||||||
|
else if (this.curTokenIs(TokenType.REFERENCES)) {
|
||||||
|
this.nextToken();
|
||||||
|
const refTable = this.expectIdentifier('referenced table');
|
||||||
|
this.expect(TokenType.LPAREN);
|
||||||
|
const refCol = this.expectIdentifier('referenced column');
|
||||||
|
this.expect(TokenType.RPAREN);
|
||||||
|
col.references = `${refTable}.${refCol}`;
|
||||||
|
// ON DELETE / ON UPDATE
|
||||||
|
while (this.curTokenIs(TokenType.ON)) {
|
||||||
|
this.nextToken();
|
||||||
|
if (this.curTokenIs(TokenType.DELETE)) {
|
||||||
|
this.nextToken();
|
||||||
|
col.onDelete = this.parseCascadeAction();
|
||||||
|
}
|
||||||
|
else if (this.curTokenIs(TokenType.UPDATE)) {
|
||||||
|
this.nextToken();
|
||||||
|
col.onUpdate = this.parseCascadeAction();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
else {
|
else {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return col;
|
return col;
|
||||||
}
|
}
|
||||||
|
/** 解析 CASCADE | SET NULL | RESTRICT */
|
||||||
|
parseCascadeAction() {
|
||||||
|
if (this.curTokenIs(TokenType.CASCADE)) {
|
||||||
|
this.nextToken();
|
||||||
|
return 'CASCADE';
|
||||||
|
}
|
||||||
|
if (this.curTokenIs(TokenType.SET)) {
|
||||||
|
this.nextToken();
|
||||||
|
this.expect(TokenType.NULL);
|
||||||
|
return 'SET NULL';
|
||||||
|
}
|
||||||
|
// RESTRICT 或默认
|
||||||
|
if (this.curToken.type === TokenType.IDENTIFIER && this.curToken.value.toUpperCase() === 'RESTRICT') {
|
||||||
|
this.nextToken();
|
||||||
|
return 'RESTRICT';
|
||||||
|
}
|
||||||
|
return 'RESTRICT';
|
||||||
|
}
|
||||||
// ===================================================================
|
// ===================================================================
|
||||||
// DROP TABLE
|
// DROP TABLE
|
||||||
// ===================================================================
|
// ===================================================================
|
||||||
@@ -2092,6 +2434,14 @@
|
|||||||
if (this.curTokenIs(TokenType.IN)) {
|
if (this.curTokenIs(TokenType.IN)) {
|
||||||
this.nextToken();
|
this.nextToken();
|
||||||
this.expect(TokenType.LPAREN);
|
this.expect(TokenType.LPAREN);
|
||||||
|
// 子查询: IN (SELECT ...)
|
||||||
|
if (this.curTokenIs(TokenType.SELECT)) {
|
||||||
|
const subquery = this.parseSelect();
|
||||||
|
this.expect(TokenType.RPAREN);
|
||||||
|
const result = {};
|
||||||
|
result[column] = { $in: { $subquery: subquery } };
|
||||||
|
return result;
|
||||||
|
}
|
||||||
const values = this.parseValueList();
|
const values = this.parseValueList();
|
||||||
this.expect(TokenType.RPAREN);
|
this.expect(TokenType.RPAREN);
|
||||||
const result = {};
|
const result = {};
|
||||||
@@ -2100,6 +2450,15 @@
|
|||||||
}
|
}
|
||||||
// 比较运算符
|
// 比较运算符
|
||||||
const op = this.parseComparisonOp();
|
const op = this.parseComparisonOp();
|
||||||
|
// 子查询: op (SELECT ...)
|
||||||
|
if (this.curTokenIs(TokenType.LPAREN) && this.peekTokenIs(TokenType.SELECT)) {
|
||||||
|
this.nextToken(); // skip (
|
||||||
|
const subquery = this.parseSelect();
|
||||||
|
this.expect(TokenType.RPAREN);
|
||||||
|
const result = {};
|
||||||
|
result[column] = { [op]: { $subquery: subquery } };
|
||||||
|
return result;
|
||||||
|
}
|
||||||
// 尝试解析列引用(identifier DOT identifier 格式)
|
// 尝试解析列引用(identifier DOT identifier 格式)
|
||||||
let value;
|
let value;
|
||||||
if ((this.curToken.type === TokenType.IDENTIFIER || this._isKeywordAsIdent()) &&
|
if ((this.curToken.type === TokenType.IDENTIFIER || this._isKeywordAsIdent()) &&
|
||||||
@@ -2338,8 +2697,7 @@
|
|||||||
* metona-sqlark Transaction — 事务管理
|
* metona-sqlark Transaction — 事务管理
|
||||||
* @module transaction
|
* @module transaction
|
||||||
*
|
*
|
||||||
* 封装多操作事务,保证原子性。
|
* v0.1.13: 支持真正的回滚 — 利用引擎层 begin/commit/rollback 实现原子性。
|
||||||
* v0.0.1: 基于引擎层操作实现,IndexedDB 引擎利用原生事务。
|
|
||||||
*/
|
*/
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Transaction
|
// Transaction
|
||||||
@@ -2375,15 +2733,21 @@
|
|||||||
constructor(engine) {
|
constructor(engine) {
|
||||||
this.engine = engine;
|
this.engine = engine;
|
||||||
}
|
}
|
||||||
/** 执行事务 */
|
/** 执行事务 — 支持自动回滚 */
|
||||||
async execute(fn) {
|
async execute(fn) {
|
||||||
const trx = new Transaction(this.engine);
|
const trx = new Transaction(this.engine);
|
||||||
|
// 开始引擎层事务
|
||||||
|
await this.engine.beginTransaction();
|
||||||
try {
|
try {
|
||||||
const result = await fn(trx);
|
const result = await fn(trx);
|
||||||
|
// 成功 → 提交
|
||||||
|
await this.engine.commitTransaction();
|
||||||
trx._markCompleted();
|
trx._markCompleted();
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
catch (error) {
|
catch (error) {
|
||||||
|
// 失败 → 回滚
|
||||||
|
await this.engine.rollbackTransaction();
|
||||||
if (error instanceof DatabaseError)
|
if (error instanceof DatabaseError)
|
||||||
throw error;
|
throw error;
|
||||||
throw new DatabaseError(`Transaction failed: ${error.message}`, 'TRANSACTION_ERROR', error);
|
throw new DatabaseError(`Transaction failed: ${error.message}`, 'TRANSACTION_ERROR', error);
|
||||||
@@ -2666,6 +3030,121 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* metona-sqlark Connection Manager — 数据库实例连接池
|
||||||
|
* @module connection-manager
|
||||||
|
*
|
||||||
|
* v0.1.13: 避免重复创建同名数据库实例,通过 connect() 复用已有连接。
|
||||||
|
* 管理实例生命周期,防止重复 open IndexedDB。
|
||||||
|
*/
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// ConnectionManager
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
class ConnectionManager {
|
||||||
|
constructor() {
|
||||||
|
/** 活跃连接:dbName → MetonaSqlark */
|
||||||
|
this.connections = new Map();
|
||||||
|
/** 连接引用计数:dbName → count */
|
||||||
|
this.refCount = new Map();
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 获取或创建数据库实例
|
||||||
|
*
|
||||||
|
* 如果同名数据库已打开,复用已有实例并增加引用计数。
|
||||||
|
* 否则创建新实例。
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```ts
|
||||||
|
* const db = await MetonaSqlark.connect({ name: 'my-app', mode: 'hybrid' });
|
||||||
|
* // ... use db
|
||||||
|
* await db.disconnect(); // 引用计数 -1,归零时自动关闭
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
async connect(config) {
|
||||||
|
const name = config.name;
|
||||||
|
// 已有连接 → 复用
|
||||||
|
const existing = this.connections.get(name);
|
||||||
|
if (existing && existing.isReady()) {
|
||||||
|
const count = (this.refCount.get(name) ?? 0) + 1;
|
||||||
|
this.refCount.set(name, count);
|
||||||
|
return existing;
|
||||||
|
}
|
||||||
|
// 创建新连接
|
||||||
|
const db = new MetonaSqlark(config);
|
||||||
|
await db.init();
|
||||||
|
this.connections.set(name, db);
|
||||||
|
this.refCount.set(name, 1);
|
||||||
|
// 注入 disconnect 方法
|
||||||
|
db.disconnect = async () => {
|
||||||
|
await this.release(name);
|
||||||
|
};
|
||||||
|
return db;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 释放连接引用。引用计数归零时自动关闭数据库。
|
||||||
|
*/
|
||||||
|
async release(dbName) {
|
||||||
|
const count = (this.refCount.get(dbName) ?? 1) - 1;
|
||||||
|
if (count <= 0) {
|
||||||
|
const db = this.connections.get(dbName);
|
||||||
|
if (db) {
|
||||||
|
await db.close();
|
||||||
|
this.connections.delete(dbName);
|
||||||
|
}
|
||||||
|
this.refCount.delete(dbName);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this.refCount.set(dbName, count);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 强制关闭指定数据库(忽略引用计数)
|
||||||
|
*/
|
||||||
|
async forceClose(dbName) {
|
||||||
|
const db = this.connections.get(dbName);
|
||||||
|
if (db) {
|
||||||
|
await db.close();
|
||||||
|
this.connections.delete(dbName);
|
||||||
|
}
|
||||||
|
this.refCount.delete(dbName);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 强制关闭所有连接
|
||||||
|
*/
|
||||||
|
async closeAll() {
|
||||||
|
for (const [, db] of this.connections) {
|
||||||
|
try {
|
||||||
|
await db.close();
|
||||||
|
}
|
||||||
|
catch { /* ignore */ }
|
||||||
|
}
|
||||||
|
this.connections.clear();
|
||||||
|
this.refCount.clear();
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 获取所有活跃连接名
|
||||||
|
*/
|
||||||
|
getActiveConnections() {
|
||||||
|
return Array.from(this.connections.keys());
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 获取连接的引用计数
|
||||||
|
*/
|
||||||
|
getRefCount(dbName) {
|
||||||
|
return this.refCount.get(dbName) ?? 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// 全局单例
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
const manager = new ConnectionManager();
|
||||||
|
// 挂载到 MetonaSqlark 静态方法(通过 any 绕过 TS 类型检查)
|
||||||
|
const M = MetonaSqlark;
|
||||||
|
M.connect = (config) => manager.connect(config);
|
||||||
|
M.disconnect = (dbName) => manager.release(dbName);
|
||||||
|
M.disconnectAll = () => manager.closeAll();
|
||||||
|
M.getActiveConnections = () => manager.getActiveConnections();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* metona-sqlark — 入口文件
|
* metona-sqlark — 入口文件
|
||||||
* @module metona-sqlark
|
* @module metona-sqlark
|
||||||
|
|||||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@metona-team/metona-sqlark",
|
"name": "@metona-team/metona-sqlark",
|
||||||
"version": "0.1.12",
|
"version": "0.1.13",
|
||||||
"description": "Frontend SQL database with in-memory and disk dual-mode storage",
|
"description": "Frontend SQL database with in-memory and disk dual-mode storage",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "dist/metona-sqlark.js",
|
"main": "dist/metona-sqlark.js",
|
||||||
|
|||||||
@@ -0,0 +1,131 @@
|
|||||||
|
/**
|
||||||
|
* metona-sqlark Connection Manager — 数据库实例连接池
|
||||||
|
* @module connection-manager
|
||||||
|
*
|
||||||
|
* v0.1.13: 避免重复创建同名数据库实例,通过 connect() 复用已有连接。
|
||||||
|
* 管理实例生命周期,防止重复 open IndexedDB。
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { MetonaSqlark } from './core';
|
||||||
|
import type { DatabaseConfig } from './constants';
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// ConnectionManager
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
class ConnectionManager {
|
||||||
|
/** 活跃连接:dbName → MetonaSqlark */
|
||||||
|
private connections = new Map<string, MetonaSqlark>();
|
||||||
|
|
||||||
|
/** 连接引用计数:dbName → count */
|
||||||
|
private refCount = new Map<string, number>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取或创建数据库实例
|
||||||
|
*
|
||||||
|
* 如果同名数据库已打开,复用已有实例并增加引用计数。
|
||||||
|
* 否则创建新实例。
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```ts
|
||||||
|
* const db = await MetonaSqlark.connect({ name: 'my-app', mode: 'hybrid' });
|
||||||
|
* // ... use db
|
||||||
|
* await db.disconnect(); // 引用计数 -1,归零时自动关闭
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
async connect(config: DatabaseConfig): Promise<MetonaSqlark> {
|
||||||
|
const name = config.name;
|
||||||
|
|
||||||
|
// 已有连接 → 复用
|
||||||
|
const existing = this.connections.get(name);
|
||||||
|
if (existing && existing.isReady()) {
|
||||||
|
const count = (this.refCount.get(name) ?? 0) + 1;
|
||||||
|
this.refCount.set(name, count);
|
||||||
|
return existing;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建新连接
|
||||||
|
const db = new MetonaSqlark(config);
|
||||||
|
await db.init();
|
||||||
|
|
||||||
|
this.connections.set(name, db);
|
||||||
|
this.refCount.set(name, 1);
|
||||||
|
|
||||||
|
// 注入 disconnect 方法
|
||||||
|
(db as MetonaSqlark & { disconnect: () => Promise<void> }).disconnect = async () => {
|
||||||
|
await this.release(name);
|
||||||
|
};
|
||||||
|
|
||||||
|
return db;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 释放连接引用。引用计数归零时自动关闭数据库。
|
||||||
|
*/
|
||||||
|
async release(dbName: string): Promise<void> {
|
||||||
|
const count = (this.refCount.get(dbName) ?? 1) - 1;
|
||||||
|
if (count <= 0) {
|
||||||
|
const db = this.connections.get(dbName);
|
||||||
|
if (db) {
|
||||||
|
await db.close();
|
||||||
|
this.connections.delete(dbName);
|
||||||
|
}
|
||||||
|
this.refCount.delete(dbName);
|
||||||
|
} else {
|
||||||
|
this.refCount.set(dbName, count);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 强制关闭指定数据库(忽略引用计数)
|
||||||
|
*/
|
||||||
|
async forceClose(dbName: string): Promise<void> {
|
||||||
|
const db = this.connections.get(dbName);
|
||||||
|
if (db) {
|
||||||
|
await db.close();
|
||||||
|
this.connections.delete(dbName);
|
||||||
|
}
|
||||||
|
this.refCount.delete(dbName);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 强制关闭所有连接
|
||||||
|
*/
|
||||||
|
async closeAll(): Promise<void> {
|
||||||
|
for (const [, db] of this.connections) {
|
||||||
|
try { await db.close(); } catch { /* ignore */ }
|
||||||
|
}
|
||||||
|
this.connections.clear();
|
||||||
|
this.refCount.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取所有活跃连接名
|
||||||
|
*/
|
||||||
|
getActiveConnections(): string[] {
|
||||||
|
return Array.from(this.connections.keys());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取连接的引用计数
|
||||||
|
*/
|
||||||
|
getRefCount(dbName: string): number {
|
||||||
|
return this.refCount.get(dbName) ?? 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// 全局单例
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
const manager = new ConnectionManager();
|
||||||
|
|
||||||
|
// 挂载到 MetonaSqlark 静态方法(通过 any 绕过 TS 类型检查)
|
||||||
|
const M = MetonaSqlark as unknown as Record<string, unknown>;
|
||||||
|
M.connect = (config: DatabaseConfig) => manager.connect(config);
|
||||||
|
M.disconnect = (dbName: string) => manager.release(dbName);
|
||||||
|
M.disconnectAll = () => manager.closeAll();
|
||||||
|
M.getActiveConnections = () => manager.getActiveConnections();
|
||||||
|
|
||||||
|
export { manager as connectionManager };
|
||||||
|
export default manager;
|
||||||
+5
-1
@@ -49,6 +49,10 @@ export interface ColumnDef {
|
|||||||
index?: boolean;
|
index?: boolean;
|
||||||
/** 外键引用: 'table.column' */
|
/** 外键引用: 'table.column' */
|
||||||
references?: string;
|
references?: string;
|
||||||
|
/** 删除级联: 'CASCADE' | 'SET NULL' | 'RESTRICT' */
|
||||||
|
onDelete?: 'CASCADE' | 'SET NULL' | 'RESTRICT';
|
||||||
|
/** 更新级联: 'CASCADE' | 'SET NULL' | 'RESTRICT' */
|
||||||
|
onUpdate?: 'CASCADE' | 'SET NULL' | 'RESTRICT';
|
||||||
/** 字符串最大长度 */
|
/** 字符串最大长度 */
|
||||||
maxLength?: number;
|
maxLength?: number;
|
||||||
/** 数字最小值 */
|
/** 数字最小值 */
|
||||||
@@ -199,4 +203,4 @@ export class DatabaseError extends Error {
|
|||||||
// 版本
|
// 版本
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
export const VERSION = '0.1.12';
|
export const VERSION = '0.1.13';
|
||||||
|
|||||||
+112
-34
@@ -1,6 +1,8 @@
|
|||||||
/**
|
/**
|
||||||
* metona-sqlark IndexedDB Engine — 基于 IndexedDB 的持久化存储引擎
|
* metona-sqlark IndexedDB Engine — 基于 IndexedDB 的持久化存储引擎
|
||||||
* @module engine/indexeddb
|
* @module engine/indexeddb
|
||||||
|
*
|
||||||
|
* v0.1.13: 支持事务 — beginTransaction 延迟 IDB 写入,commit 批量刷盘,rollback 恢复快照。
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { IStorageEngine } from './interface';
|
import type { IStorageEngine } from './interface';
|
||||||
@@ -17,6 +19,9 @@ export class IndexedDBEngine implements IStorageEngine {
|
|||||||
private version = 1;
|
private version = 1;
|
||||||
private memoryCache: MemoryEngine = new MemoryEngine();
|
private memoryCache: MemoryEngine = new MemoryEngine();
|
||||||
|
|
||||||
|
// ---- 事务状态 ----
|
||||||
|
private txActive = false;
|
||||||
|
|
||||||
async open(dbName: string, version: number): Promise<void> {
|
async open(dbName: string, version: number): Promise<void> {
|
||||||
this.dbName = dbName; this.version = version;
|
this.dbName = dbName; this.version = version;
|
||||||
await this.memoryCache.open(dbName, version);
|
await this.memoryCache.open(dbName, version);
|
||||||
@@ -38,6 +43,88 @@ export class IndexedDBEngine implements IStorageEngine {
|
|||||||
// ---- 表管理 ----
|
// ---- 表管理 ----
|
||||||
async createTable(schema: TableSchema): Promise<void> {
|
async createTable(schema: TableSchema): Promise<void> {
|
||||||
await this.memoryCache.createTable(schema);
|
await this.memoryCache.createTable(schema);
|
||||||
|
if (this.txActive) return; // 事务中延迟 IDB 操作
|
||||||
|
await this.idbCreateTable(schema);
|
||||||
|
}
|
||||||
|
|
||||||
|
async dropTable(tableName: string): Promise<void> {
|
||||||
|
await this.memoryCache.dropTable(tableName);
|
||||||
|
if (this.txActive) return;
|
||||||
|
await this.idbDropTable(tableName);
|
||||||
|
}
|
||||||
|
|
||||||
|
async hasTable(tableName: string): Promise<boolean> { return this.ensureDB().objectStoreNames.contains(tableName); }
|
||||||
|
async getTableNames(): Promise<string[]> { return Array.from(this.ensureDB().objectStoreNames); }
|
||||||
|
async getTableSchema(tableName: string): Promise<TableSchema | null> { return this.memoryCache.getTableSchema(tableName); }
|
||||||
|
|
||||||
|
// ---- CRUD ----
|
||||||
|
async insert(tableName: string, rows: Record<string, unknown>[]): Promise<string[]> {
|
||||||
|
const pks = await this.memoryCache.insert(tableName, rows);
|
||||||
|
if (this.txActive) return pks; // 事务中延迟写入
|
||||||
|
await this.idbInsert(tableName, rows);
|
||||||
|
return pks;
|
||||||
|
}
|
||||||
|
|
||||||
|
async find(tableName: string, query: QueryPlan): Promise<Record<string, unknown>[]> {
|
||||||
|
// 事务中从内存缓存读取(保证读到未提交的变更),否则走 IDB
|
||||||
|
if (this.txActive) return this.memoryCache.find(tableName, query);
|
||||||
|
return this.idbFind(tableName, query);
|
||||||
|
}
|
||||||
|
|
||||||
|
async update(tableName: string, query: QueryPlan, updates: Record<string, unknown>): Promise<number> {
|
||||||
|
const count = await this.memoryCache.update(tableName, query, updates);
|
||||||
|
if (this.txActive) return count;
|
||||||
|
await this.idbUpdate(tableName, query, updates);
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
async delete(tableName: string, query: QueryPlan): Promise<number> {
|
||||||
|
const count = await this.memoryCache.delete(tableName, query);
|
||||||
|
if (this.txActive) return count;
|
||||||
|
await this.idbDelete(tableName, query);
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
async count(tableName: string, query?: QueryPlan): Promise<number> {
|
||||||
|
if (this.txActive) return this.memoryCache.count(tableName, query);
|
||||||
|
const results = await this.idbFind(tableName, { table: tableName, where: query?.where ?? {} });
|
||||||
|
return results.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
async clear(tableName: string): Promise<void> {
|
||||||
|
await this.memoryCache.clear(tableName);
|
||||||
|
if (this.txActive) return;
|
||||||
|
await this.idbClear(tableName);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- 事务 ----
|
||||||
|
|
||||||
|
async beginTransaction(): Promise<void> {
|
||||||
|
if (this.txActive) throw new DatabaseError('Transaction already in progress', 'TX_ACTIVE');
|
||||||
|
this.txActive = true;
|
||||||
|
// 保存内存快照到 MemoryEngine 内部的 beginTransaction
|
||||||
|
await this.memoryCache.beginTransaction();
|
||||||
|
}
|
||||||
|
|
||||||
|
async commitTransaction(): Promise<void> {
|
||||||
|
if (!this.txActive) throw new DatabaseError('No active transaction', 'TX_NONE');
|
||||||
|
// 确认内存层的变更
|
||||||
|
await this.memoryCache.commitTransaction();
|
||||||
|
// 批量将内存数据刷到 IndexedDB
|
||||||
|
await this.flushToIDB();
|
||||||
|
this.txActive = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
async rollbackTransaction(): Promise<void> {
|
||||||
|
if (!this.txActive) throw new DatabaseError('No active transaction', 'TX_NONE');
|
||||||
|
// 恢复内存层到快照状态
|
||||||
|
await this.memoryCache.rollbackTransaction();
|
||||||
|
this.txActive = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- IDB 原生操作 ----
|
||||||
|
|
||||||
|
private async idbCreateTable(schema: TableSchema): Promise<void> {
|
||||||
const db = this.ensureDB();
|
const db = this.ensureDB();
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const newVersion = db.version + 1; db.close();
|
const newVersion = db.version + 1; db.close();
|
||||||
@@ -57,8 +144,7 @@ export class IndexedDBEngine implements IStorageEngine {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async dropTable(tableName: string): Promise<void> {
|
private async idbDropTable(tableName: string): Promise<void> {
|
||||||
await this.memoryCache.dropTable(tableName);
|
|
||||||
const db = this.ensureDB();
|
const db = this.ensureDB();
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const newVersion = db.version + 1; db.close();
|
const newVersion = db.version + 1; db.close();
|
||||||
@@ -72,29 +158,18 @@ export class IndexedDBEngine implements IStorageEngine {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async hasTable(tableName: string): Promise<boolean> { return this.ensureDB().objectStoreNames.contains(tableName); }
|
private async idbInsert(tableName: string, rows: Record<string, unknown>[]): Promise<void> {
|
||||||
async getTableNames(): Promise<string[]> { return Array.from(this.ensureDB().objectStoreNames); }
|
|
||||||
async getTableSchema(tableName: string): Promise<TableSchema | null> { return this.memoryCache.getTableSchema(tableName); }
|
|
||||||
|
|
||||||
// ---- CRUD ----
|
|
||||||
async insert(tableName: string, rows: Record<string, unknown>[]): Promise<string[]> {
|
|
||||||
await this.memoryCache.insert(tableName, rows);
|
|
||||||
const db = this.ensureDB();
|
const db = this.ensureDB();
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const tx = db.transaction(tableName, 'readwrite');
|
const tx = db.transaction(tableName, 'readwrite');
|
||||||
const store = tx.objectStore(tableName);
|
const store = tx.objectStore(tableName);
|
||||||
const pks: string[] = [];
|
for (const row of rows) store.add(row);
|
||||||
for (const row of rows) {
|
tx.oncomplete = () => resolve();
|
||||||
const req = store.add(row);
|
|
||||||
req.onsuccess = () => pks.push(String(req.result));
|
|
||||||
req.onerror = () => {}; // 内存层已校验,忽略 IDB 重复键
|
|
||||||
}
|
|
||||||
tx.oncomplete = () => resolve(pks);
|
|
||||||
tx.onerror = () => reject(new DatabaseError(`Insert failed for "${tableName}"`, 'IDB_TX_ERROR', tx.error));
|
tx.onerror = () => reject(new DatabaseError(`Insert failed for "${tableName}"`, 'IDB_TX_ERROR', tx.error));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async find(tableName: string, query: QueryPlan): Promise<Record<string, unknown>[]> {
|
private async idbFind(tableName: string, query: QueryPlan): Promise<Record<string, unknown>[]> {
|
||||||
const db = this.ensureDB();
|
const db = this.ensureDB();
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const tx = db.transaction(tableName, 'readonly');
|
const tx = db.transaction(tableName, 'readonly');
|
||||||
@@ -119,28 +194,25 @@ export class IndexedDBEngine implements IStorageEngine {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async update(tableName: string, query: QueryPlan, updates: Record<string, unknown>): Promise<number> {
|
private async idbUpdate(tableName: string, query: QueryPlan, updates: Record<string, unknown>): Promise<void> {
|
||||||
await this.memoryCache.update(tableName, query, updates);
|
|
||||||
const db = this.ensureDB();
|
const db = this.ensureDB();
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const tx = db.transaction(tableName, 'readwrite');
|
const tx = db.transaction(tableName, 'readwrite');
|
||||||
const store = tx.objectStore(tableName);
|
const store = tx.objectStore(tableName);
|
||||||
const getAllReq = store.getAll();
|
const getAllReq = store.getAll();
|
||||||
let count = 0;
|
|
||||||
getAllReq.onsuccess = () => {
|
getAllReq.onsuccess = () => {
|
||||||
for (const row of getAllReq.result ?? []) {
|
for (const row of getAllReq.result ?? []) {
|
||||||
if (!query.where || Object.keys(query.where).length === 0 || matchWhere(row, query.where)) {
|
if (!query.where || Object.keys(query.where).length === 0 || matchWhere(row, query.where)) {
|
||||||
Object.assign(row, updates); store.put(row); count++;
|
Object.assign(row, updates); store.put(row);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
tx.oncomplete = () => resolve(count);
|
tx.oncomplete = () => resolve();
|
||||||
tx.onerror = () => reject(new DatabaseError(`Update failed for "${tableName}"`, 'IDB_TX_ERROR', tx.error));
|
tx.onerror = () => reject(new DatabaseError(`Update failed for "${tableName}"`, 'IDB_TX_ERROR', tx.error));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async delete(tableName: string, query: QueryPlan): Promise<number> {
|
private async idbDelete(tableName: string, query: QueryPlan): Promise<void> {
|
||||||
await this.memoryCache.delete(tableName, query);
|
|
||||||
const db = this.ensureDB();
|
const db = this.ensureDB();
|
||||||
const schema = await this.memoryCache.getTableSchema(tableName);
|
const schema = await this.memoryCache.getTableSchema(tableName);
|
||||||
if (!schema) throw new DatabaseError(`Table "${tableName}" not found`, 'TABLE_NOT_FOUND');
|
if (!schema) throw new DatabaseError(`Table "${tableName}" not found`, 'TABLE_NOT_FOUND');
|
||||||
@@ -149,26 +221,19 @@ export class IndexedDBEngine implements IStorageEngine {
|
|||||||
const tx = db.transaction(tableName, 'readwrite');
|
const tx = db.transaction(tableName, 'readwrite');
|
||||||
const store = tx.objectStore(tableName);
|
const store = tx.objectStore(tableName);
|
||||||
const getAllReq = store.getAll();
|
const getAllReq = store.getAll();
|
||||||
let count = 0;
|
|
||||||
getAllReq.onsuccess = () => {
|
getAllReq.onsuccess = () => {
|
||||||
for (const row of getAllReq.result ?? []) {
|
for (const row of getAllReq.result ?? []) {
|
||||||
if (!query.where || Object.keys(query.where).length === 0 || matchWhere(row, query.where)) {
|
if (!query.where || Object.keys(query.where).length === 0 || matchWhere(row, query.where)) {
|
||||||
store.delete(row[pkColumn] as IDBValidKey); count++;
|
store.delete(row[pkColumn] as IDBValidKey);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
tx.oncomplete = () => resolve(count);
|
tx.oncomplete = () => resolve();
|
||||||
tx.onerror = () => reject(new DatabaseError(`Delete failed for "${tableName}"`, 'IDB_TX_ERROR', tx.error));
|
tx.onerror = () => reject(new DatabaseError(`Delete failed for "${tableName}"`, 'IDB_TX_ERROR', tx.error));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async count(tableName: string, query?: QueryPlan): Promise<number> {
|
private async idbClear(tableName: string): Promise<void> {
|
||||||
const results = await this.find(tableName, { table: tableName, where: query?.where ?? {} });
|
|
||||||
return results.length;
|
|
||||||
}
|
|
||||||
|
|
||||||
async clear(tableName: string): Promise<void> {
|
|
||||||
await this.memoryCache.clear(tableName);
|
|
||||||
const db = this.ensureDB();
|
const db = this.ensureDB();
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const tx = db.transaction(tableName, 'readwrite');
|
const tx = db.transaction(tableName, 'readwrite');
|
||||||
@@ -178,6 +243,19 @@ export class IndexedDBEngine implements IStorageEngine {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 将内存缓存中的所有表数据刷新到 IndexedDB */
|
||||||
|
private async flushToIDB(): Promise<void> {
|
||||||
|
const tableNames = await this.memoryCache.getTableNames();
|
||||||
|
for (const tableName of tableNames) {
|
||||||
|
const rows = await this.memoryCache.find(tableName, { table: tableName });
|
||||||
|
// Clear + bulk insert for simplicity
|
||||||
|
await this.idbClear(tableName);
|
||||||
|
if (rows.length > 0) {
|
||||||
|
await this.idbInsert(tableName, rows);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private ensureDB(): IDBDatabase {
|
private ensureDB(): IDBDatabase {
|
||||||
if (!this.db) throw new DatabaseError('Database not opened', 'DB_NOT_OPEN');
|
if (!this.db) throw new DatabaseError('Database not opened', 'DB_NOT_OPEN');
|
||||||
return this.db;
|
return this.db;
|
||||||
|
|||||||
@@ -54,4 +54,15 @@ export interface IStorageEngine {
|
|||||||
|
|
||||||
/** 清空表数据(保留结构) */
|
/** 清空表数据(保留结构) */
|
||||||
clear(tableName: string): Promise<void>;
|
clear(tableName: string): Promise<void>;
|
||||||
|
|
||||||
|
// ---- 事务 ----
|
||||||
|
|
||||||
|
/** 开始事务 */
|
||||||
|
beginTransaction(): Promise<void>;
|
||||||
|
|
||||||
|
/** 提交事务 */
|
||||||
|
commitTransaction(): Promise<void>;
|
||||||
|
|
||||||
|
/** 回滚事务 */
|
||||||
|
rollbackTransaction(): Promise<void>;
|
||||||
}
|
}
|
||||||
|
|||||||
+118
-1
@@ -16,6 +16,13 @@ export class MemoryEngine implements IStorageEngine {
|
|||||||
private indexes: Map<string, Map<string, Map<unknown, Set<string>>>> = new Map();
|
private indexes: Map<string, Map<string, Map<unknown, Set<string>>>> = new Map();
|
||||||
private opened = false;
|
private opened = false;
|
||||||
|
|
||||||
|
// ---- 事务快照 ----
|
||||||
|
private snapshot: {
|
||||||
|
tables: Map<string, Map<string, Record<string, unknown>>>;
|
||||||
|
schemas: Map<string, TableSchema>;
|
||||||
|
indexes: Map<string, Map<string, Map<unknown, Set<string>>>>;
|
||||||
|
} | null = null;
|
||||||
|
|
||||||
// ---- 生命周期 ----
|
// ---- 生命周期 ----
|
||||||
async open(_dbName: string, _version: number): Promise<void> { this.opened = true; }
|
async open(_dbName: string, _version: number): Promise<void> { this.opened = true; }
|
||||||
async close(): Promise<void> {
|
async close(): Promise<void> {
|
||||||
@@ -109,8 +116,14 @@ export class MemoryEngine implements IStorageEngine {
|
|||||||
toDelete.push(pk);
|
toDelete.push(pk);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// 级联删除:检查引用此表的其他表
|
||||||
|
let cascadeCount = 0;
|
||||||
|
for (const pk of toDelete) {
|
||||||
|
const row = table.get(pk);
|
||||||
|
if (row) cascadeCount += await this.cascadeDelete(tableName, pk, row);
|
||||||
|
}
|
||||||
for (const pk of toDelete) table.delete(pk);
|
for (const pk of toDelete) table.delete(pk);
|
||||||
return toDelete.length;
|
return toDelete.length + cascadeCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
async count(tableName: string, query?: QueryPlan): Promise<number> {
|
async count(tableName: string, query?: QueryPlan): Promise<number> {
|
||||||
@@ -129,6 +142,56 @@ export class MemoryEngine implements IStorageEngine {
|
|||||||
if (tableIndexes) for (const colIndex of tableIndexes.values()) colIndex.clear();
|
if (tableIndexes) for (const colIndex of tableIndexes.values()) colIndex.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---- 事务 ----
|
||||||
|
|
||||||
|
async beginTransaction(): Promise<void> {
|
||||||
|
if (this.snapshot) throw new DatabaseError('Transaction already in progress', 'TX_ACTIVE');
|
||||||
|
this.snapshot = {
|
||||||
|
tables: this.deepCloneMapMap(this.tables),
|
||||||
|
schemas: new Map(this.schemas),
|
||||||
|
indexes: this.deepCloneIndexes(this.indexes),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async commitTransaction(): Promise<void> {
|
||||||
|
if (!this.snapshot) throw new DatabaseError('No active transaction', 'TX_NONE');
|
||||||
|
this.snapshot = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async rollbackTransaction(): Promise<void> {
|
||||||
|
if (!this.snapshot) throw new DatabaseError('No active transaction', 'TX_NONE');
|
||||||
|
this.tables = this.snapshot.tables;
|
||||||
|
this.schemas = this.snapshot.schemas;
|
||||||
|
this.indexes = this.snapshot.indexes;
|
||||||
|
this.snapshot = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- 事务快照辅助 ----
|
||||||
|
|
||||||
|
private deepCloneMapMap(source: Map<string, Map<string, Record<string, unknown>>>): Map<string, Map<string, Record<string, unknown>>> {
|
||||||
|
const clone = new Map<string, Map<string, Record<string, unknown>>>();
|
||||||
|
for (const [k, v] of source) {
|
||||||
|
const innerClone = new Map<string, Record<string, unknown>>();
|
||||||
|
for (const [ik, iv] of v) innerClone.set(ik, { ...iv });
|
||||||
|
clone.set(k, innerClone);
|
||||||
|
}
|
||||||
|
return clone;
|
||||||
|
}
|
||||||
|
|
||||||
|
private deepCloneIndexes(source: Map<string, Map<string, Map<unknown, Set<string>>>>): Map<string, Map<string, Map<unknown, Set<string>>>> {
|
||||||
|
const clone = new Map<string, Map<string, Map<unknown, Set<string>>>>();
|
||||||
|
for (const [tableName, tableIndexes] of source) {
|
||||||
|
const tableClone = new Map<string, Map<unknown, Set<string>>>();
|
||||||
|
for (const [col, colIndex] of tableIndexes) {
|
||||||
|
const colClone = new Map<unknown, Set<string>>();
|
||||||
|
for (const [val, pkSet] of colIndex) colClone.set(val, new Set(pkSet));
|
||||||
|
tableClone.set(col, colClone);
|
||||||
|
}
|
||||||
|
clone.set(tableName, tableClone);
|
||||||
|
}
|
||||||
|
return clone;
|
||||||
|
}
|
||||||
|
|
||||||
// ---- 内部辅助 ----
|
// ---- 内部辅助 ----
|
||||||
private ensureTable(tableName: string): void {
|
private ensureTable(tableName: string): void {
|
||||||
if (!this.tables.has(tableName)) throw new DatabaseError(`Table "${tableName}" does not exist`, 'TABLE_NOT_FOUND');
|
if (!this.tables.has(tableName)) throw new DatabaseError(`Table "${tableName}" does not exist`, 'TABLE_NOT_FOUND');
|
||||||
@@ -212,4 +275,58 @@ export class MemoryEngine implements IStorageEngine {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---- 外键级联 ----
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 级联删除:查找引用 tableName.pkValue 的所有表的行并删除。
|
||||||
|
* @returns 级联删除的行数
|
||||||
|
*/
|
||||||
|
private async cascadeDelete(tableName: string, pkValue: string, _row: Record<string, unknown>): Promise<number> {
|
||||||
|
let totalCascade = 0;
|
||||||
|
|
||||||
|
for (const [refTableName, refSchema] of this.schemas) {
|
||||||
|
if (refTableName === tableName) continue;
|
||||||
|
|
||||||
|
for (const [colName, colDef] of Object.entries(refSchema.columns)) {
|
||||||
|
if (!colDef.references || !colDef.onDelete || colDef.onDelete === 'RESTRICT') continue;
|
||||||
|
|
||||||
|
const [refTable, refCol] = colDef.references.split('.');
|
||||||
|
if (refTable !== tableName) continue;
|
||||||
|
|
||||||
|
const refPkCol = refCol;
|
||||||
|
const refTableData = this.tables.get(refTableName);
|
||||||
|
if (!refTableData) continue;
|
||||||
|
|
||||||
|
// 查找所有引用此主键的行
|
||||||
|
const toDelete: string[] = [];
|
||||||
|
for (const [refPk, refRow] of refTableData) {
|
||||||
|
if (String(refRow[colName]) === pkValue) {
|
||||||
|
toDelete.push(refPk);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (colDef.onDelete === 'CASCADE') {
|
||||||
|
// 递归级联
|
||||||
|
for (const refPk of toDelete) {
|
||||||
|
const refRow = refTableData.get(refPk);
|
||||||
|
if (refRow) {
|
||||||
|
totalCascade += await this.cascadeDelete(refTableName, refPk, refRow);
|
||||||
|
}
|
||||||
|
refTableData.delete(refPk);
|
||||||
|
totalCascade++;
|
||||||
|
}
|
||||||
|
} else if (colDef.onDelete === 'SET NULL') {
|
||||||
|
for (const refPk of toDelete) {
|
||||||
|
const refRow = refTableData.get(refPk);
|
||||||
|
if (refRow) {
|
||||||
|
refRow[colName] = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return totalCascade;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -129,6 +129,26 @@ export class OPFSEngine implements IStorageEngine {
|
|||||||
await this.writeTableData(tableName, []);
|
await this.writeTableData(tableName, []);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---- 事务 ----
|
||||||
|
|
||||||
|
async beginTransaction(): Promise<void> {
|
||||||
|
await this.memoryCache.beginTransaction();
|
||||||
|
}
|
||||||
|
|
||||||
|
async commitTransaction(): Promise<void> {
|
||||||
|
await this.memoryCache.commitTransaction();
|
||||||
|
// 将内存数据刷到 OPFS
|
||||||
|
const tableNames = await this.memoryCache.getTableNames();
|
||||||
|
for (const tableName of tableNames) {
|
||||||
|
const rows = await this.memoryCache.find(tableName, { table: tableName });
|
||||||
|
await this.writeTableData(tableName, rows);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async rollbackTransaction(): Promise<void> {
|
||||||
|
await this.memoryCache.rollbackTransaction();
|
||||||
|
}
|
||||||
|
|
||||||
// ---- 内部辅助 ----
|
// ---- 内部辅助 ----
|
||||||
|
|
||||||
private ensureDir(): FileSystemDirectoryHandle {
|
private ensureDir(): FileSystemDirectoryHandle {
|
||||||
|
|||||||
@@ -132,6 +132,23 @@ export class HybridEngine implements IStorageEngine {
|
|||||||
await this.diskEngine.clear(tableName);
|
await this.diskEngine.clear(tableName);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---- 事务 ----
|
||||||
|
|
||||||
|
async beginTransaction(): Promise<void> {
|
||||||
|
await this.memoryEngine.beginTransaction();
|
||||||
|
await this.diskEngine.beginTransaction();
|
||||||
|
}
|
||||||
|
|
||||||
|
async commitTransaction(): Promise<void> {
|
||||||
|
await this.memoryEngine.commitTransaction();
|
||||||
|
await this.diskEngine.commitTransaction();
|
||||||
|
}
|
||||||
|
|
||||||
|
async rollbackTransaction(): Promise<void> {
|
||||||
|
await this.memoryEngine.rollbackTransaction();
|
||||||
|
await this.diskEngine.rollbackTransaction();
|
||||||
|
}
|
||||||
|
|
||||||
// ---- 引擎信息 ----
|
// ---- 引擎信息 ----
|
||||||
|
|
||||||
/** 获取磁盘引擎类型 */
|
/** 获取磁盘引擎类型 */
|
||||||
|
|||||||
@@ -11,6 +11,9 @@ import { MetonaSqlark } from './core';
|
|||||||
import type { DatabaseConfig } from './constants';
|
import type { DatabaseConfig } from './constants';
|
||||||
import { VERSION } from './constants';
|
import { VERSION } from './constants';
|
||||||
|
|
||||||
|
// 连接池管理器(side-effect: 注入 MetonaSqlark.connect / disconnect 等静态方法)
|
||||||
|
import './connection-manager';
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// 工厂函数
|
// 工厂函数
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -57,6 +57,16 @@ export interface AggregateExpression {
|
|||||||
alias?: string;
|
alias?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// 子查询
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/** 子查询表达式 */
|
||||||
|
export interface SubqueryExpression {
|
||||||
|
type: 'SUBQUERY';
|
||||||
|
statement: SelectStatement;
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// DDL: CREATE TABLE
|
// DDL: CREATE TABLE
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -72,6 +82,12 @@ export interface ASTColumnDef {
|
|||||||
maxLength?: number;
|
maxLength?: number;
|
||||||
min?: number;
|
min?: number;
|
||||||
max?: number;
|
max?: number;
|
||||||
|
/** 外键引用 */
|
||||||
|
references?: string;
|
||||||
|
/** 级联删除 */
|
||||||
|
onDelete?: 'CASCADE' | 'SET NULL' | 'RESTRICT';
|
||||||
|
/** 级联更新 */
|
||||||
|
onUpdate?: 'CASCADE' | 'SET NULL' | 'RESTRICT';
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CreateTableStatement {
|
export interface CreateTableStatement {
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import { DatabaseError } from '../constants';
|
|||||||
import { compileStatement } from './compiler';
|
import { compileStatement } from './compiler';
|
||||||
import { createSchema, astColumnToColumnDef } from '../table/schema';
|
import { createSchema, astColumnToColumnDef } from '../table/schema';
|
||||||
import { matchWhere, applyOrderBy, projectColumns } from './where-matcher';
|
import { matchWhere, applyOrderBy, projectColumns } from './where-matcher';
|
||||||
|
import type { WhereCondition } from '../constants';
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Executor
|
// Executor
|
||||||
@@ -39,6 +40,11 @@ export class QueryExecutor {
|
|||||||
// ===================================================================
|
// ===================================================================
|
||||||
|
|
||||||
private async executeSelect(stmt: SelectStatement): Promise<Record<string, unknown>[]> {
|
private async executeSelect(stmt: SelectStatement): Promise<Record<string, unknown>[]> {
|
||||||
|
// 先解析子查询
|
||||||
|
if (stmt.where && Object.keys(stmt.where).length > 0) {
|
||||||
|
stmt.where = await this.resolveSubqueries(stmt.where);
|
||||||
|
}
|
||||||
|
|
||||||
const hasGroupBy = !!(stmt.groupBy && stmt.groupBy.length > 0);
|
const hasGroupBy = !!(stmt.groupBy && stmt.groupBy.length > 0);
|
||||||
let rows: Record<string, unknown>[];
|
let rows: Record<string, unknown>[];
|
||||||
|
|
||||||
@@ -226,4 +232,99 @@ export class QueryExecutor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getEngine(): IStorageEngine { return this.engine; }
|
getEngine(): IStorageEngine { return this.engine; }
|
||||||
|
|
||||||
|
// ===================================================================
|
||||||
|
// 子查询解析
|
||||||
|
// ===================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 递归扫描 WHERE 条件,找到 $subquery 标记并执行子查询,
|
||||||
|
* 将结果替换为具体值。
|
||||||
|
*/
|
||||||
|
private async resolveSubqueries(where: WhereCondition): Promise<WhereCondition> {
|
||||||
|
const resolved: WhereCondition = {};
|
||||||
|
|
||||||
|
for (const [key, value] of Object.entries(where)) {
|
||||||
|
// 逻辑组合操作符
|
||||||
|
if (key === '$and' && Array.isArray(value)) {
|
||||||
|
resolved.$and = await Promise.all(
|
||||||
|
(value as WhereCondition[]).map((sub) => this.resolveSubqueries(sub)),
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (key === '$or' && Array.isArray(value)) {
|
||||||
|
resolved.$or = await Promise.all(
|
||||||
|
(value as WhereCondition[]).map((sub) => this.resolveSubqueries(sub)),
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (key === '$not' && typeof value === 'object' && value !== null) {
|
||||||
|
resolved.$not = await this.resolveSubqueries(value as WhereCondition);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 字段条件
|
||||||
|
if (typeof value === 'object' && value !== null) {
|
||||||
|
resolved[key] = await this.resolveOperatorSubqueries(value as Record<string, unknown>);
|
||||||
|
} else {
|
||||||
|
resolved[key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return resolved;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析操作符值中嵌套的子查询
|
||||||
|
*/
|
||||||
|
private async resolveOperatorSubqueries(ops: Record<string, unknown>): Promise<Record<string, unknown>> {
|
||||||
|
const resolved: Record<string, unknown> = {};
|
||||||
|
|
||||||
|
for (const [op, operand] of Object.entries(ops)) {
|
||||||
|
// 处理嵌套 $and/$or(在字段级条件中)
|
||||||
|
if (op === '$and' && Array.isArray(operand)) {
|
||||||
|
resolved.$and = await Promise.all(
|
||||||
|
(operand as WhereCondition[]).map((sub) => this.resolveSubqueries(sub)),
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (op === '$or' && Array.isArray(operand)) {
|
||||||
|
resolved.$or = await Promise.all(
|
||||||
|
(operand as WhereCondition[]).map((sub) => this.resolveSubqueries(sub)),
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (op === '$not') {
|
||||||
|
resolved.$not = typeof operand === 'object' && operand !== null
|
||||||
|
? await this.resolveOperatorSubqueries(operand as Record<string, unknown>)
|
||||||
|
: operand;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 子查询检测
|
||||||
|
if (typeof operand === 'object' && operand !== null && '$subquery' in (operand as Record<string, unknown>)) {
|
||||||
|
const subStmt = (operand as Record<string, unknown>).$subquery as SelectStatement;
|
||||||
|
const subResult = await this.executeSelect(subStmt);
|
||||||
|
|
||||||
|
if (op === '$in' || op === '$nin') {
|
||||||
|
// IN 子查询 → 提取第一列的值列表
|
||||||
|
const colName = Object.keys(subResult[0] || {})[0];
|
||||||
|
const values = subResult.map((row) => row[colName]);
|
||||||
|
resolved[op] = values;
|
||||||
|
} else {
|
||||||
|
// 标量子查询 → 取第一行第一列
|
||||||
|
if (subResult.length === 0) {
|
||||||
|
resolved[op] = null;
|
||||||
|
} else {
|
||||||
|
const colName = Object.keys(subResult[0])[0];
|
||||||
|
resolved[op] = subResult[0][colName];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
resolved[op] = operand;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return resolved;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+59
-2
@@ -352,7 +352,8 @@ export class Parser {
|
|||||||
this.curTokenIs(TokenType.PRIMARY) ||
|
this.curTokenIs(TokenType.PRIMARY) ||
|
||||||
this.curTokenIs(TokenType.UNIQUE) ||
|
this.curTokenIs(TokenType.UNIQUE) ||
|
||||||
this.curTokenIs(TokenType.NOT) ||
|
this.curTokenIs(TokenType.NOT) ||
|
||||||
this.curTokenIs(TokenType.DEFAULT)
|
this.curTokenIs(TokenType.DEFAULT) ||
|
||||||
|
this.curTokenIs(TokenType.REFERENCES)
|
||||||
) {
|
) {
|
||||||
if (this.curTokenIs(TokenType.PRIMARY)) {
|
if (this.curTokenIs(TokenType.PRIMARY)) {
|
||||||
this.nextToken();
|
this.nextToken();
|
||||||
@@ -363,12 +364,31 @@ export class Parser {
|
|||||||
col.unique = true;
|
col.unique = true;
|
||||||
} else if (this.curTokenIs(TokenType.NOT)) {
|
} else if (this.curTokenIs(TokenType.NOT)) {
|
||||||
this.nextToken();
|
this.nextToken();
|
||||||
// 支持 NOT NULL → required
|
|
||||||
this.expect(TokenType.NULL);
|
this.expect(TokenType.NULL);
|
||||||
col.required = true;
|
col.required = true;
|
||||||
} else if (this.curTokenIs(TokenType.DEFAULT)) {
|
} else if (this.curTokenIs(TokenType.DEFAULT)) {
|
||||||
this.nextToken();
|
this.nextToken();
|
||||||
col.default = this.parseValue();
|
col.default = this.parseValue();
|
||||||
|
} else if (this.curTokenIs(TokenType.REFERENCES)) {
|
||||||
|
this.nextToken();
|
||||||
|
const refTable = this.expectIdentifier('referenced table');
|
||||||
|
this.expect(TokenType.LPAREN);
|
||||||
|
const refCol = this.expectIdentifier('referenced column');
|
||||||
|
this.expect(TokenType.RPAREN);
|
||||||
|
col.references = `${refTable}.${refCol}`;
|
||||||
|
// ON DELETE / ON UPDATE
|
||||||
|
while (this.curTokenIs(TokenType.ON)) {
|
||||||
|
this.nextToken();
|
||||||
|
if (this.curTokenIs(TokenType.DELETE)) {
|
||||||
|
this.nextToken();
|
||||||
|
col.onDelete = this.parseCascadeAction();
|
||||||
|
} else if (this.curTokenIs(TokenType.UPDATE)) {
|
||||||
|
this.nextToken();
|
||||||
|
col.onUpdate = this.parseCascadeAction();
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -377,6 +397,25 @@ export class Parser {
|
|||||||
return col;
|
return col;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 解析 CASCADE | SET NULL | RESTRICT */
|
||||||
|
private parseCascadeAction(): 'CASCADE' | 'SET NULL' | 'RESTRICT' {
|
||||||
|
if (this.curTokenIs(TokenType.CASCADE)) {
|
||||||
|
this.nextToken();
|
||||||
|
return 'CASCADE';
|
||||||
|
}
|
||||||
|
if (this.curTokenIs(TokenType.SET)) {
|
||||||
|
this.nextToken();
|
||||||
|
this.expect(TokenType.NULL);
|
||||||
|
return 'SET NULL';
|
||||||
|
}
|
||||||
|
// RESTRICT 或默认
|
||||||
|
if (this.curToken.type === TokenType.IDENTIFIER && this.curToken.value.toUpperCase() === 'RESTRICT') {
|
||||||
|
this.nextToken();
|
||||||
|
return 'RESTRICT';
|
||||||
|
}
|
||||||
|
return 'RESTRICT';
|
||||||
|
}
|
||||||
|
|
||||||
// ===================================================================
|
// ===================================================================
|
||||||
// DROP TABLE
|
// DROP TABLE
|
||||||
// ===================================================================
|
// ===================================================================
|
||||||
@@ -457,6 +496,14 @@ export class Parser {
|
|||||||
if (this.curTokenIs(TokenType.IN)) {
|
if (this.curTokenIs(TokenType.IN)) {
|
||||||
this.nextToken();
|
this.nextToken();
|
||||||
this.expect(TokenType.LPAREN);
|
this.expect(TokenType.LPAREN);
|
||||||
|
// 子查询: IN (SELECT ...)
|
||||||
|
if (this.curTokenIs(TokenType.SELECT)) {
|
||||||
|
const subquery = this.parseSelect();
|
||||||
|
this.expect(TokenType.RPAREN);
|
||||||
|
const result: WhereCondition = {};
|
||||||
|
result[column] = { $in: { $subquery: subquery } };
|
||||||
|
return result;
|
||||||
|
}
|
||||||
const values = this.parseValueList();
|
const values = this.parseValueList();
|
||||||
this.expect(TokenType.RPAREN);
|
this.expect(TokenType.RPAREN);
|
||||||
const result: WhereCondition = {};
|
const result: WhereCondition = {};
|
||||||
@@ -467,6 +514,16 @@ export class Parser {
|
|||||||
// 比较运算符
|
// 比较运算符
|
||||||
const op = this.parseComparisonOp();
|
const op = this.parseComparisonOp();
|
||||||
|
|
||||||
|
// 子查询: op (SELECT ...)
|
||||||
|
if (this.curTokenIs(TokenType.LPAREN) && this.peekTokenIs(TokenType.SELECT)) {
|
||||||
|
this.nextToken(); // skip (
|
||||||
|
const subquery = this.parseSelect();
|
||||||
|
this.expect(TokenType.RPAREN);
|
||||||
|
const result: WhereCondition = {};
|
||||||
|
result[column] = { [op]: { $subquery: subquery } };
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
// 尝试解析列引用(identifier DOT identifier 格式)
|
// 尝试解析列引用(identifier DOT identifier 格式)
|
||||||
let value: unknown;
|
let value: unknown;
|
||||||
if (
|
if (
|
||||||
|
|||||||
@@ -38,6 +38,8 @@ export enum TokenType {
|
|||||||
DEFAULT = 'DEFAULT',
|
DEFAULT = 'DEFAULT',
|
||||||
NULL = 'NULL',
|
NULL = 'NULL',
|
||||||
TRUE = 'TRUE',
|
TRUE = 'TRUE',
|
||||||
|
REFERENCES = 'REFERENCES',
|
||||||
|
CASCADE = 'CASCADE',
|
||||||
FALSE = 'FALSE',
|
FALSE = 'FALSE',
|
||||||
|
|
||||||
// JOIN 相关
|
// JOIN 相关
|
||||||
@@ -129,6 +131,8 @@ export const KEYWORDS: Record<string, TokenType> = {
|
|||||||
'NULL': TokenType.NULL,
|
'NULL': TokenType.NULL,
|
||||||
'TRUE': TokenType.TRUE,
|
'TRUE': TokenType.TRUE,
|
||||||
'FALSE': TokenType.FALSE,
|
'FALSE': TokenType.FALSE,
|
||||||
|
'REFERENCES': TokenType.REFERENCES,
|
||||||
|
'CASCADE': TokenType.CASCADE,
|
||||||
|
|
||||||
// JOIN
|
// JOIN
|
||||||
'INNER': TokenType.INNER,
|
'INNER': TokenType.INNER,
|
||||||
|
|||||||
@@ -157,6 +157,9 @@ export function astColumnToColumnDef(astCol: {
|
|||||||
maxLength?: number;
|
maxLength?: number;
|
||||||
min?: number;
|
min?: number;
|
||||||
max?: number;
|
max?: number;
|
||||||
|
references?: string;
|
||||||
|
onDelete?: 'CASCADE' | 'SET NULL' | 'RESTRICT';
|
||||||
|
onUpdate?: 'CASCADE' | 'SET NULL' | 'RESTRICT';
|
||||||
}): ColumnDef {
|
}): ColumnDef {
|
||||||
return {
|
return {
|
||||||
type: astCol.type as FieldType,
|
type: astCol.type as FieldType,
|
||||||
@@ -168,5 +171,8 @@ export function astColumnToColumnDef(astCol: {
|
|||||||
maxLength: astCol.maxLength,
|
maxLength: astCol.maxLength,
|
||||||
min: astCol.min,
|
min: astCol.min,
|
||||||
max: astCol.max,
|
max: astCol.max,
|
||||||
|
references: astCol.references,
|
||||||
|
onDelete: astCol.onDelete,
|
||||||
|
onUpdate: astCol.onUpdate,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,8 +2,7 @@
|
|||||||
* metona-sqlark Transaction — 事务管理
|
* metona-sqlark Transaction — 事务管理
|
||||||
* @module transaction
|
* @module transaction
|
||||||
*
|
*
|
||||||
* 封装多操作事务,保证原子性。
|
* v0.1.13: 支持真正的回滚 — 利用引擎层 begin/commit/rollback 实现原子性。
|
||||||
* v0.0.1: 基于引擎层操作实现,IndexedDB 引擎利用原生事务。
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { IStorageEngine } from '../engine/interface';
|
import type { IStorageEngine } from '../engine/interface';
|
||||||
@@ -51,15 +50,22 @@ export class Transaction {
|
|||||||
export class TransactionManager {
|
export class TransactionManager {
|
||||||
constructor(private engine: IStorageEngine) {}
|
constructor(private engine: IStorageEngine) {}
|
||||||
|
|
||||||
/** 执行事务 */
|
/** 执行事务 — 支持自动回滚 */
|
||||||
async execute<T>(fn: (trx: Transaction) => Promise<T>): Promise<T> {
|
async execute<T>(fn: (trx: Transaction) => Promise<T>): Promise<T> {
|
||||||
const trx = new Transaction(this.engine);
|
const trx = new Transaction(this.engine);
|
||||||
|
|
||||||
|
// 开始引擎层事务
|
||||||
|
await this.engine.beginTransaction();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await fn(trx);
|
const result = await fn(trx);
|
||||||
|
// 成功 → 提交
|
||||||
|
await this.engine.commitTransaction();
|
||||||
trx._markCompleted();
|
trx._markCompleted();
|
||||||
return result;
|
return result;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
// 失败 → 回滚
|
||||||
|
await this.engine.rollbackTransaction();
|
||||||
if (error instanceof DatabaseError) throw error;
|
if (error instanceof DatabaseError) throw error;
|
||||||
throw new DatabaseError(`Transaction failed: ${(error as Error).message}`, 'TRANSACTION_ERROR', error);
|
throw new DatabaseError(`Transaction failed: ${(error as Error).message}`, 'TRANSACTION_ERROR', error);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user