feat: v0.1.13 — 事务回滚 + 子查询 + 外键级联 + 连接池
CI / test (18.x) (push) Successful in 9m54s
CI / test (20.x) (push) Successful in 9m52s
CI / test (22.x) (push) Successful in 9m54s
CI / test (24.x) (push) Successful in 9m47s

This commit is contained in:
thzxx
2026-07-26 16:31:15 +08:00
parent 7a53cfd530
commit 0f128da34a
24 changed files with 2264 additions and 166 deletions
+131
View File
@@ -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
View File
@@ -49,6 +49,10 @@ export interface ColumnDef {
index?: boolean;
/** 外键引用: 'table.column' */
references?: string;
/** 删除级联: 'CASCADE' | 'SET NULL' | 'RESTRICT' */
onDelete?: 'CASCADE' | 'SET NULL' | 'RESTRICT';
/** 更新级联: 'CASCADE' | 'SET NULL' | 'RESTRICT' */
onUpdate?: 'CASCADE' | 'SET NULL' | 'RESTRICT';
/** 字符串最大长度 */
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
View File
@@ -1,6 +1,8 @@
/**
* metona-sqlark IndexedDB Engine — 基于 IndexedDB 的持久化存储引擎
* @module engine/indexeddb
*
* v0.1.13: 支持事务 — beginTransaction 延迟 IDB 写入,commit 批量刷盘,rollback 恢复快照。
*/
import type { IStorageEngine } from './interface';
@@ -17,6 +19,9 @@ export class IndexedDBEngine implements IStorageEngine {
private version = 1;
private memoryCache: MemoryEngine = new MemoryEngine();
// ---- 事务状态 ----
private txActive = false;
async open(dbName: string, version: number): Promise<void> {
this.dbName = dbName; this.version = version;
await this.memoryCache.open(dbName, version);
@@ -38,6 +43,88 @@ export class IndexedDBEngine implements IStorageEngine {
// ---- 表管理 ----
async createTable(schema: TableSchema): Promise<void> {
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();
return new Promise((resolve, reject) => {
const newVersion = db.version + 1; db.close();
@@ -57,8 +144,7 @@ export class IndexedDBEngine implements IStorageEngine {
});
}
async dropTable(tableName: string): Promise<void> {
await this.memoryCache.dropTable(tableName);
private async idbDropTable(tableName: string): Promise<void> {
const db = this.ensureDB();
return new Promise((resolve, reject) => {
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); }
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);
private async idbInsert(tableName: string, rows: Record<string, unknown>[]): Promise<void> {
const db = this.ensureDB();
return new Promise((resolve, reject) => {
const tx = db.transaction(tableName, 'readwrite');
const store = tx.objectStore(tableName);
const pks: string[] = [];
for (const row of rows) {
const req = store.add(row);
req.onsuccess = () => pks.push(String(req.result));
req.onerror = () => {}; // 内存层已校验,忽略 IDB 重复键
}
tx.oncomplete = () => resolve(pks);
for (const row of rows) store.add(row);
tx.oncomplete = () => resolve();
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();
return new Promise((resolve, reject) => {
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> {
await this.memoryCache.update(tableName, query, updates);
private async idbUpdate(tableName: string, query: QueryPlan, updates: Record<string, unknown>): Promise<void> {
const db = this.ensureDB();
return new Promise((resolve, reject) => {
const tx = db.transaction(tableName, 'readwrite');
const store = tx.objectStore(tableName);
const getAllReq = store.getAll();
let count = 0;
getAllReq.onsuccess = () => {
for (const row of getAllReq.result ?? []) {
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));
});
}
async delete(tableName: string, query: QueryPlan): Promise<number> {
await this.memoryCache.delete(tableName, query);
private async idbDelete(tableName: string, query: QueryPlan): Promise<void> {
const db = this.ensureDB();
const schema = await this.memoryCache.getTableSchema(tableName);
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 store = tx.objectStore(tableName);
const getAllReq = store.getAll();
let count = 0;
getAllReq.onsuccess = () => {
for (const row of getAllReq.result ?? []) {
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));
});
}
async count(tableName: string, query?: QueryPlan): Promise<number> {
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);
private async idbClear(tableName: string): Promise<void> {
const db = this.ensureDB();
return new Promise((resolve, reject) => {
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 {
if (!this.db) throw new DatabaseError('Database not opened', 'DB_NOT_OPEN');
return this.db;
+11
View File
@@ -54,4 +54,15 @@ export interface IStorageEngine {
/** 清空表数据(保留结构) */
clear(tableName: string): Promise<void>;
// ---- 事务 ----
/** 开始事务 */
beginTransaction(): Promise<void>;
/** 提交事务 */
commitTransaction(): Promise<void>;
/** 回滚事务 */
rollbackTransaction(): Promise<void>;
}
+118 -1
View File
@@ -16,6 +16,13 @@ export class MemoryEngine implements IStorageEngine {
private indexes: Map<string, Map<string, Map<unknown, Set<string>>>> = new Map();
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 close(): Promise<void> {
@@ -109,8 +116,14 @@ export class MemoryEngine implements IStorageEngine {
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);
return toDelete.length;
return toDelete.length + cascadeCount;
}
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();
}
// ---- 事务 ----
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 {
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;
}
}
+20
View File
@@ -129,6 +129,26 @@ export class OPFSEngine implements IStorageEngine {
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 {
+17
View File
@@ -132,6 +132,23 @@ export class HybridEngine implements IStorageEngine {
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();
}
// ---- 引擎信息 ----
/** 获取磁盘引擎类型 */
+3
View File
@@ -11,6 +11,9 @@ import { MetonaSqlark } from './core';
import type { DatabaseConfig } from './constants';
import { VERSION } from './constants';
// 连接池管理器(side-effect: 注入 MetonaSqlark.connect / disconnect 等静态方法)
import './connection-manager';
// ---------------------------------------------------------------------------
// 工厂函数
// ---------------------------------------------------------------------------
+16
View File
@@ -57,6 +57,16 @@ export interface AggregateExpression {
alias?: string;
}
// ---------------------------------------------------------------------------
// 子查询
// ---------------------------------------------------------------------------
/** 子查询表达式 */
export interface SubqueryExpression {
type: 'SUBQUERY';
statement: SelectStatement;
}
// ---------------------------------------------------------------------------
// DDL: CREATE TABLE
// ---------------------------------------------------------------------------
@@ -72,6 +82,12 @@ export interface ASTColumnDef {
maxLength?: number;
min?: number;
max?: number;
/** 外键引用 */
references?: string;
/** 级联删除 */
onDelete?: 'CASCADE' | 'SET NULL' | 'RESTRICT';
/** 级联更新 */
onUpdate?: 'CASCADE' | 'SET NULL' | 'RESTRICT';
}
export interface CreateTableStatement {
+101
View File
@@ -14,6 +14,7 @@ import { DatabaseError } from '../constants';
import { compileStatement } from './compiler';
import { createSchema, astColumnToColumnDef } from '../table/schema';
import { matchWhere, applyOrderBy, projectColumns } from './where-matcher';
import type { WhereCondition } from '../constants';
// ---------------------------------------------------------------------------
// Executor
@@ -39,6 +40,11 @@ export class QueryExecutor {
// ===================================================================
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);
let rows: Record<string, unknown>[];
@@ -226,4 +232,99 @@ export class QueryExecutor {
}
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
View File
@@ -352,7 +352,8 @@ export class Parser {
this.curTokenIs(TokenType.PRIMARY) ||
this.curTokenIs(TokenType.UNIQUE) ||
this.curTokenIs(TokenType.NOT) ||
this.curTokenIs(TokenType.DEFAULT)
this.curTokenIs(TokenType.DEFAULT) ||
this.curTokenIs(TokenType.REFERENCES)
) {
if (this.curTokenIs(TokenType.PRIMARY)) {
this.nextToken();
@@ -363,12 +364,31 @@ export class Parser {
col.unique = true;
} else if (this.curTokenIs(TokenType.NOT)) {
this.nextToken();
// 支持 NOT NULL → required
this.expect(TokenType.NULL);
col.required = true;
} else if (this.curTokenIs(TokenType.DEFAULT)) {
this.nextToken();
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 {
break;
}
@@ -377,6 +397,25 @@ export class Parser {
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
// ===================================================================
@@ -457,6 +496,14 @@ export class Parser {
if (this.curTokenIs(TokenType.IN)) {
this.nextToken();
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();
this.expect(TokenType.RPAREN);
const result: WhereCondition = {};
@@ -467,6 +514,16 @@ export class Parser {
// 比较运算符
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 格式)
let value: unknown;
if (
+4
View File
@@ -38,6 +38,8 @@ export enum TokenType {
DEFAULT = 'DEFAULT',
NULL = 'NULL',
TRUE = 'TRUE',
REFERENCES = 'REFERENCES',
CASCADE = 'CASCADE',
FALSE = 'FALSE',
// JOIN 相关
@@ -129,6 +131,8 @@ export const KEYWORDS: Record<string, TokenType> = {
'NULL': TokenType.NULL,
'TRUE': TokenType.TRUE,
'FALSE': TokenType.FALSE,
'REFERENCES': TokenType.REFERENCES,
'CASCADE': TokenType.CASCADE,
// JOIN
'INNER': TokenType.INNER,
+6
View File
@@ -157,6 +157,9 @@ export function astColumnToColumnDef(astCol: {
maxLength?: number;
min?: number;
max?: number;
references?: string;
onDelete?: 'CASCADE' | 'SET NULL' | 'RESTRICT';
onUpdate?: 'CASCADE' | 'SET NULL' | 'RESTRICT';
}): ColumnDef {
return {
type: astCol.type as FieldType,
@@ -168,5 +171,8 @@ export function astColumnToColumnDef(astCol: {
maxLength: astCol.maxLength,
min: astCol.min,
max: astCol.max,
references: astCol.references,
onDelete: astCol.onDelete,
onUpdate: astCol.onUpdate,
};
}
+9 -3
View File
@@ -2,8 +2,7 @@
* metona-sqlark Transaction — 事务管理
* @module transaction
*
* 封装多操作事务,保证原子性。
* v0.0.1: 基于引擎层操作实现,IndexedDB 引擎利用原生事务。
* v0.1.13: 支持真正的回滚 — 利用引擎层 begin/commit/rollback 实现原子性。
*/
import type { IStorageEngine } from '../engine/interface';
@@ -51,15 +50,22 @@ export class Transaction {
export class TransactionManager {
constructor(private engine: IStorageEngine) {}
/** 执行事务 */
/** 执行事务 — 支持自动回滚 */
async execute<T>(fn: (trx: Transaction) => Promise<T>): Promise<T> {
const trx = new Transaction(this.engine);
// 开始引擎层事务
await this.engine.beginTransaction();
try {
const result = await fn(trx);
// 成功 → 提交
await this.engine.commitTransaction();
trx._markCompleted();
return result;
} catch (error) {
// 失败 → 回滚
await this.engine.rollbackTransaction();
if (error instanceof DatabaseError) throw error;
throw new DatabaseError(`Transaction failed: ${(error as Error).message}`, 'TRANSACTION_ERROR', error);
}