feat: metona-sqlark v0.1.12 — 前端TypeScript关系型数据库
- 4种存储引擎:Memory / IndexedDB / OPFS / Hybrid - 完整SQL支持:SELECT/INSERT/UPDATE/DELETE/JOIN/GROUP BY/HAVING/DISTINCT - Query Builder链式API + TypeScript泛型支持 - 聚合函数:COUNT/SUM/AVG/MIN/MAX - 事务、插件系统(14 hooks)、发布订阅、数据迁移、导入导出 - React/Vue框架集成 - 264个测试用例,93.46%覆盖率 - 零运行时依赖
This commit is contained in:
@@ -0,0 +1,229 @@
|
||||
/**
|
||||
* metona-sqlark Query Executor — AST 执行器
|
||||
* @module query/executor
|
||||
*
|
||||
* JOIN / GROUP BY / DISTINCT 逻辑在此层处理。
|
||||
*/
|
||||
|
||||
import type { IStorageEngine } from '../engine/interface';
|
||||
import type {
|
||||
Statement, SelectStatement, InsertStatement, UpdateStatement,
|
||||
DeleteStatement, CreateTableStatement, DropTableStatement, JoinClause,
|
||||
} from './ast';
|
||||
import { DatabaseError } from '../constants';
|
||||
import { compileStatement } from './compiler';
|
||||
import { createSchema, astColumnToColumnDef } from '../table/schema';
|
||||
import { matchWhere, applyOrderBy, projectColumns } from './where-matcher';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Executor
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export class QueryExecutor {
|
||||
constructor(private engine: IStorageEngine) {}
|
||||
|
||||
async execute(stmt: Statement): Promise<unknown> {
|
||||
switch (stmt.type) {
|
||||
case 'SELECT': return this.executeSelect(stmt);
|
||||
case 'INSERT': return this.executeInsert(stmt);
|
||||
case 'UPDATE': return this.executeUpdate(stmt);
|
||||
case 'DELETE': return this.executeDelete(stmt);
|
||||
case 'CREATE_TABLE': return this.executeCreateTable(stmt);
|
||||
case 'DROP_TABLE': return this.executeDropTable(stmt);
|
||||
default: throw new DatabaseError('Unknown statement type', 'UNKNOWN_STATEMENT');
|
||||
}
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
// SELECT
|
||||
// ===================================================================
|
||||
|
||||
private async executeSelect(stmt: SelectStatement): Promise<Record<string, unknown>[]> {
|
||||
const hasGroupBy = !!(stmt.groupBy && stmt.groupBy.length > 0);
|
||||
let rows: Record<string, unknown>[];
|
||||
|
||||
if (!stmt.joins || stmt.joins.length === 0) {
|
||||
const plan = compileStatement(hasGroupBy ? { ...stmt, columns: ['*'] } : stmt);
|
||||
rows = await this.engine.find(plan.table, plan);
|
||||
} else {
|
||||
rows = await this.executeJoinSelect(stmt);
|
||||
}
|
||||
|
||||
if (hasGroupBy) rows = this.executeGroupBy(rows, stmt);
|
||||
if (stmt.distinct) rows = this.executeDistinct(rows);
|
||||
if (stmt.having && Object.keys(stmt.having).length > 0) {
|
||||
rows = rows.filter((row) => matchWhere(row, stmt.having!));
|
||||
}
|
||||
if (stmt.orderBy && stmt.orderBy.length > 0) rows = applyOrderBy(rows, stmt.orderBy);
|
||||
const offset = stmt.offset ?? 0;
|
||||
const limit = stmt.limit ?? rows.length;
|
||||
rows = rows.slice(offset, offset + limit);
|
||||
if (!hasGroupBy && stmt.columns.length > 0 && stmt.columns[0] !== '*') {
|
||||
rows = rows.map((row) => projectColumns(row, stmt.columns));
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
// ---- JOIN ----
|
||||
|
||||
private async executeJoinSelect(stmt: SelectStatement): Promise<Record<string, unknown>[]> {
|
||||
const mainAlias = stmt.alias ?? stmt.from;
|
||||
const mainRows = (await this.engine.find(stmt.from, { table: stmt.from }))
|
||||
.map((row) => this.prefixRow(row, mainAlias));
|
||||
let resultRows = mainRows;
|
||||
|
||||
for (const join of stmt.joins!) {
|
||||
const joinAlias = join.alias ?? join.table;
|
||||
const joinRows = (await this.engine.find(join.table, { table: join.table }))
|
||||
.map((row) => this.prefixRow(row, joinAlias));
|
||||
resultRows = this.joinRows(resultRows, joinRows, join);
|
||||
}
|
||||
|
||||
if (stmt.where && Object.keys(stmt.where).length > 0) {
|
||||
resultRows = resultRows.filter((row) => matchWhere(row, stmt.where));
|
||||
}
|
||||
return resultRows;
|
||||
}
|
||||
|
||||
private prefixRow(row: Record<string, unknown>, alias: string): Record<string, unknown> {
|
||||
const prefixed: Record<string, unknown> = {};
|
||||
for (const [key, value] of Object.entries(row)) prefixed[`${alias}.${key}`] = value;
|
||||
return prefixed;
|
||||
}
|
||||
|
||||
/** 嵌套循环连接(优化:避免 ON 时对象扩散) */
|
||||
private joinRows(
|
||||
leftRows: Record<string, unknown>[],
|
||||
rightRows: Record<string, unknown>[],
|
||||
join: JoinClause,
|
||||
): Record<string, unknown>[] {
|
||||
if (join.type === 'CROSS') {
|
||||
const result: Record<string, unknown>[] = [];
|
||||
for (const l of leftRows) for (const r of rightRows) result.push({ ...l, ...r });
|
||||
return result;
|
||||
}
|
||||
|
||||
const result: Record<string, unknown>[] = [];
|
||||
for (const l of leftRows) {
|
||||
let matched = false;
|
||||
for (const r of rightRows) {
|
||||
// 合并后匹配 ON(避免创建临时对象再丢弃)
|
||||
const merged = { ...l, ...r };
|
||||
if (matchWhere(merged, join.on, { $col: true })) {
|
||||
result.push(merged);
|
||||
matched = true;
|
||||
}
|
||||
}
|
||||
if (!matched && join.type === 'LEFT') {
|
||||
const nullRight: Record<string, unknown> = {};
|
||||
for (const key of Object.keys(rightRows[0] ?? {})) nullRight[key] = null;
|
||||
result.push({ ...l, ...nullRight });
|
||||
}
|
||||
}
|
||||
|
||||
if (join.type === 'RIGHT') {
|
||||
for (const r of rightRows) {
|
||||
const isMatched = leftRows.some((l) => {
|
||||
const merged = { ...l, ...r };
|
||||
return matchWhere(merged, join.on, { $col: true });
|
||||
});
|
||||
if (!isMatched) {
|
||||
const nullLeft: Record<string, unknown> = {};
|
||||
for (const key of Object.keys(leftRows[0] ?? {})) nullLeft[key] = null;
|
||||
result.push({ ...nullLeft, ...r });
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// ---- GROUP BY ----
|
||||
|
||||
private executeGroupBy(rows: Record<string, unknown>[], stmt: SelectStatement): Record<string, unknown>[] {
|
||||
const groups = new Map<string, Record<string, unknown>[]>();
|
||||
for (const row of rows) {
|
||||
const key = stmt.groupBy!.map((col) => String(row[col] ?? 'null')).join('|');
|
||||
if (!groups.has(key)) groups.set(key, []);
|
||||
groups.get(key)!.push(row);
|
||||
}
|
||||
const result: Record<string, unknown>[] = [];
|
||||
for (const groupRows of groups.values()) {
|
||||
const aggregated: Record<string, unknown> = {};
|
||||
for (const col of stmt.groupBy!) aggregated[col] = groupRows[0][col];
|
||||
for (const colExpr of stmt.columns) {
|
||||
if (colExpr === '*') continue;
|
||||
const m = colExpr.match(/^(COUNT|SUM|AVG|MIN|MAX)\((.+?)\)(?:\s+AS\s+(\w+))?$/i);
|
||||
if (m) {
|
||||
const [, func, arg, alias] = m;
|
||||
aggregated[alias || colExpr] = this.computeAggregate(func.toUpperCase(), groupRows, arg.trim());
|
||||
} else if (!stmt.groupBy!.includes(colExpr)) {
|
||||
aggregated[colExpr] = groupRows[0][colExpr];
|
||||
}
|
||||
}
|
||||
result.push(aggregated);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private computeAggregate(func: string, rows: Record<string, unknown>[], col: string): number {
|
||||
const nums = rows.map((r) => r[col]).filter((v) => v !== null && v !== undefined).map(Number);
|
||||
switch (func) {
|
||||
case 'COUNT': return col === '*' ? rows.length : nums.length;
|
||||
case 'SUM': return nums.reduce((a: number, b) => a + b, 0);
|
||||
case 'AVG': return nums.length === 0 ? 0 : nums.reduce((a: number, b) => a + b, 0) / nums.length;
|
||||
case 'MIN': return nums.length === 0 ? 0 : Math.min(...nums);
|
||||
case 'MAX': return nums.length === 0 ? 0 : Math.max(...nums);
|
||||
default: return 0;
|
||||
}
|
||||
}
|
||||
|
||||
// ---- DISTINCT(优化:列值拼接代替 JSON.stringify) ----
|
||||
|
||||
private executeDistinct(rows: Record<string, unknown>[]): Record<string, unknown>[] {
|
||||
const seen = new Set<string>();
|
||||
return rows.filter((row) => {
|
||||
const key = Object.values(row).map((v) => String(v ?? '\0')).join('\x1f');
|
||||
if (seen.has(key)) return false;
|
||||
seen.add(key);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
// 其他语句
|
||||
// ===================================================================
|
||||
|
||||
private async executeInsert(stmt: InsertStatement): Promise<string[]> {
|
||||
const schema = await this.engine.getTableSchema(stmt.into);
|
||||
if (!schema) throw new DatabaseError(`Table "${stmt.into}" does not exist`, 'TABLE_NOT_FOUND');
|
||||
const colNames = stmt.columns ?? Object.keys(schema.columns);
|
||||
const rows: Record<string, unknown>[] = stmt.values.map((vals: unknown[]) => {
|
||||
const row: Record<string, unknown> = {};
|
||||
for (let i = 0; i < colNames.length; i++) { if (i < vals.length) row[colNames[i]] = vals[i]; }
|
||||
return row;
|
||||
});
|
||||
return this.engine.insert(stmt.into, rows);
|
||||
}
|
||||
|
||||
private async executeUpdate(stmt: UpdateStatement): Promise<number> {
|
||||
const plan = compileStatement(stmt);
|
||||
return this.engine.update(plan.table, plan, stmt.sets);
|
||||
}
|
||||
|
||||
private async executeDelete(stmt: DeleteStatement): Promise<number> {
|
||||
const plan = compileStatement(stmt);
|
||||
return this.engine.delete(plan.table, plan);
|
||||
}
|
||||
|
||||
private async executeCreateTable(stmt: CreateTableStatement): Promise<void> {
|
||||
const columns: Record<string, any> = {};
|
||||
for (const col of stmt.columns) columns[col.name] = astColumnToColumnDef(col);
|
||||
return this.engine.createTable(createSchema(stmt.name, columns));
|
||||
}
|
||||
|
||||
private async executeDropTable(stmt: DropTableStatement): Promise<void> {
|
||||
return this.engine.dropTable(stmt.name);
|
||||
}
|
||||
|
||||
getEngine(): IStorageEngine { return this.engine; }
|
||||
}
|
||||
Reference in New Issue
Block a user