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:
thzxx
2026-07-26 15:00:01 +08:00
commit e2a590c5b1
60 changed files with 16359 additions and 0 deletions
+202
View File
@@ -0,0 +1,202 @@
/**
* metona-sqlark Constants — 类型定义 / 默认配置 / 枚举
* @module constants
*/
// ---------------------------------------------------------------------------
// 存储模式
// ---------------------------------------------------------------------------
/** 存储模式 */
export type StorageMode = 'memory' | 'disk' | 'hybrid';
/** 磁盘引擎类型 */
export type DiskEngine = 'indexeddb' | 'opfs';
/** 所有存储模式 */
export const STORAGE_MODES: StorageMode[] = ['memory', 'disk', 'hybrid'];
/** 所有磁盘引擎 */
export const DISK_ENGINES: DiskEngine[] = ['indexeddb', 'opfs'];
// ---------------------------------------------------------------------------
// 字段类型
// ---------------------------------------------------------------------------
/** 字段数据类型 */
export type FieldType = 'string' | 'number' | 'boolean' | 'date' | 'json';
/** 所有字段类型 */
export const FIELD_TYPES: FieldType[] = ['string', 'number', 'boolean', 'date', 'json'];
// ---------------------------------------------------------------------------
// 列定义 & 表结构
// ---------------------------------------------------------------------------
/** 列定义 */
export interface ColumnDef {
/** 字段类型 */
type: FieldType;
/** 是否主键 */
primaryKey?: boolean;
/** 是否必填 */
required?: boolean;
/** 是否唯一 */
unique?: boolean;
/** 默认值 */
default?: unknown;
/** 是否创建索引 */
index?: boolean;
/** 外键引用: 'table.column' */
references?: string;
/** 字符串最大长度 */
maxLength?: number;
/** 数字最小值 */
min?: number;
/** 数字最大值 */
max?: number;
}
/** 表结构定义 */
export interface TableSchema {
/** 表名 */
name: string;
/** 列定义映射 */
columns: Record<string, ColumnDef>;
}
// ---------------------------------------------------------------------------
// 数据库配置
// ---------------------------------------------------------------------------
/** 数据库配置 */
export interface DatabaseConfig {
/** 数据库名称 */
name: string;
/** 存储模式 */
mode?: StorageMode;
/** 磁盘引擎(仅 mode='disk'|'hybrid' 时生效) */
diskEngine?: DiskEngine;
/** 版本号 */
version?: number;
/** 插件列表 */
plugins?: MetonaPlugin[];
/** 数据库就绪回调 */
onReady?: (db: unknown) => void;
/** 错误回调 */
onError?: (error: Error) => void;
}
/** 数据库默认配置 */
export const DB_DEFAULTS: Readonly<Required<Omit<DatabaseConfig, 'plugins' | 'onReady' | 'onError'>>> = Object.freeze({
name: 'metona-sqlark',
mode: 'hybrid' as const,
diskEngine: 'indexeddb' as const,
version: 1,
});
// ---------------------------------------------------------------------------
// Where 操作符
// ---------------------------------------------------------------------------
/** Where 条件操作符 */
export type WhereOperator = '$eq' | '$ne' | '$gt' | '$gte' | '$lt' | '$lte' | '$in' | '$nin' | '$like' | '$and' | '$or' | '$not';
/** 简单条件值:直接相等 */
export type SimpleCondition = unknown;
/** 操作符条件 */
export type OperatorCondition = Partial<Record<WhereOperator, unknown>>;
/** 字段条件:简单值 | 操作符对象 */
export type FieldCondition = SimpleCondition | OperatorCondition;
/** Where 条件对象 */
export type WhereCondition = Record<string, FieldCondition>;
// ---------------------------------------------------------------------------
// 排序
// ---------------------------------------------------------------------------
/** 排序方向 */
export type SortDirection = 'asc' | 'desc';
/** 排序定义 */
export interface OrderBy {
/** 列名 */
column: string;
/** 排序方向 */
direction: SortDirection;
}
// ---------------------------------------------------------------------------
// 查询计划(引擎层使用)
// ---------------------------------------------------------------------------
/** 查询计划 — 由 Executor 编译 AST 后生成 */
export interface QueryPlan {
/** 表名 */
table: string;
/** 要返回的列(undefined = 全部,['*'] = 全部) */
columns?: string[];
/** 过滤条件 */
where?: WhereCondition;
/** 排序 */
orderBy?: OrderBy[];
/** 限制条数 */
limit?: number;
/** 偏移量 */
offset?: number;
}
// ---------------------------------------------------------------------------
// 插件接口
// ---------------------------------------------------------------------------
/** 钩子名称 */
export type HookName =
| 'beforeCreateTable' | 'afterCreateTable'
| 'beforeDropTable' | 'afterDropTable'
| 'beforeInsert' | 'afterInsert'
| 'beforeUpdate' | 'afterUpdate'
| 'beforeDelete' | 'afterDelete'
| 'beforeQuery' | 'afterQuery'
| 'beforeTransaction' | 'afterTransaction';
/** 插件定义 */
export interface MetonaPlugin {
/** 插件名称 */
name: string;
/** 插件版本 */
version: string;
/** 描述 */
description?: string;
/** 优先级,越大越先执行 */
priority?: number;
/** 安装 */
install(db: unknown): void;
/** 销毁 */
destroy(): void;
}
// ---------------------------------------------------------------------------
// 错误类型
// ---------------------------------------------------------------------------
/** 数据库错误 */
export class DatabaseError extends Error {
constructor(
message: string,
public code: string,
public details?: unknown,
) {
super(message);
this.name = 'DatabaseError';
}
}
// ---------------------------------------------------------------------------
// 版本
// ---------------------------------------------------------------------------
export const VERSION = '0.1.12';
+273
View File
@@ -0,0 +1,273 @@
/**
* metona-sqlark Core — 数据库主类
* @module core
*
* 管理数据库生命周期、引擎调度、表操作、SQL 查询、事务和插件。
*/
import type { IStorageEngine } from './engine/interface';
import type { DatabaseConfig, ColumnDef } from './constants';
import { DB_DEFAULTS, DatabaseError } from './constants';
import { MemoryEngine } from './engine/memory';
import { IndexedDBEngine } from './engine/indexeddb';
import { OPFSEngine } from './engine/opfs';
import { HybridEngine } from './hybrid/index';
import { Table } from './table/table';
import { createSchema } from './table/schema';
import { QueryExecutor } from './query/executor';
import { parse } from './sql/parser';
import { TransactionManager } from './transaction/index';
import { PluginManager } from './plugin/index';
import type { Statement } from './query/ast';
// ---------------------------------------------------------------------------
// MetonaSqlark
// ---------------------------------------------------------------------------
export class MetonaSqlark {
/** 数据库名称 */
readonly name: string;
/** 存储模式 */
readonly mode: string;
/** 版本号 */
private _version: number;
/** 获取版本号 */
get version(): number { return this._version; }
private engine!: IStorageEngine;
private executor!: QueryExecutor;
private transactionManager!: TransactionManager;
private pluginManager: PluginManager;
private config: DatabaseConfig;
private ready = false;
private tableCache: Map<string, Table> = new Map();
constructor(config: DatabaseConfig) {
this.config = config;
this.name = config.name ?? DB_DEFAULTS.name;
this.mode = config.mode ?? DB_DEFAULTS.mode;
this._version = config.version ?? DB_DEFAULTS.version;
this.pluginManager = new PluginManager();
}
// ---- 初始化 ----
/** 初始化数据库(创建引擎、打开连接) */
async init(): Promise<void> {
// 创建引擎
this.engine = this.createEngine();
// 打开连接
await this.engine.open(this.name, this.version);
// 初始化执行器和事务管理器
this.executor = new QueryExecutor(this.engine);
this.transactionManager = new TransactionManager(this.engine);
// 注册插件
if (this.config.plugins) {
for (const plugin of this.config.plugins) {
this.pluginManager.register(plugin);
}
}
this.ready = true;
// 回调
if (this.config.onReady) {
this.config.onReady(this);
}
}
/** 检查是否就绪 */
isReady(): boolean {
return this.ready;
}
// ---- 表管理 ----
/** 创建表 */
async defineTable(name: string, columns: Record<string, ColumnDef>): Promise<void> {
this.ensureReady();
const schema = createSchema(name, columns);
await this.pluginManager.trigger('beforeCreateTable', schema);
await this.engine.createTable(schema);
await this.pluginManager.trigger('afterCreateTable', schema);
// 清除缓存
this.tableCache.delete(name);
}
/** 获取表操作对象 */
table(name: string): Table {
this.ensureReady();
let t = this.tableCache.get(name);
if (!t) {
t = new Table(this.engine, name, this.executor);
this.tableCache.set(name, t);
}
return t;
}
/** 删除表 */
async dropTable(name: string): Promise<void> {
this.ensureReady();
await this.pluginManager.trigger('beforeDropTable', name);
await this.engine.dropTable(name);
await this.pluginManager.trigger('afterDropTable', name);
this.tableCache.delete(name);
}
/** 获取所有表名 */
async getTableNames(): Promise<string[]> {
this.ensureReady();
return this.engine.getTableNames();
}
// ---- SQL 查询 ----
/** 执行 SQL 字符串查询 */
async query(sql: string): Promise<unknown> {
this.ensureReady();
await this.pluginManager.trigger('beforeQuery', sql);
const stmt: Statement = parse(sql);
const result = await this.executor.execute(stmt);
await this.pluginManager.trigger('afterQuery', sql, result);
return result;
}
// ---- 事务 ----
/** 执行事务 */
async transaction<T>(fn: (trx: import('./transaction/index').Transaction) => Promise<T>): Promise<T> {
this.ensureReady();
await this.pluginManager.trigger('beforeTransaction');
const result = await this.transactionManager.execute(fn);
await this.pluginManager.trigger('afterTransaction');
return result;
}
// ---- 导入导出 ----
/** 导出表数据为 JSON */
async exportTable(tableName: string): Promise<Record<string, unknown>[]> {
this.ensureReady();
return this.engine.find(tableName, { table: tableName });
}
/** 导入 JSON 数据到表 */
async importTable(tableName: string, data: Record<string, unknown>[]): Promise<string[]> {
this.ensureReady();
return this.engine.insert(tableName, data);
}
/** 导出整个数据库为 JSON */
async exportAll(): Promise<Record<string, Record<string, unknown>[]>> {
this.ensureReady();
const result: Record<string, Record<string, unknown>[]> = {};
const names = await this.engine.getTableNames();
for (const name of names) {
result[name] = await this.engine.find(name, { table: name });
}
return result;
}
// ---- 发布订阅 ----
private listeners: Map<string, Set<(data: unknown) => void>> = new Map();
/** 订阅表变更 */
subscribe(tableName: string, callback: (event: { type: string; row?: unknown }) => void): () => void {
const key = `change:${tableName}`;
if (!this.listeners.has(key)) this.listeners.set(key, new Set());
this.listeners.get(key)!.add(callback as (data: unknown) => void);
return () => this.listeners.get(key)?.delete(callback as (data: unknown) => void);
}
/** 触发变更事件 */
emit(tableName: string, event: { type: string; row?: unknown }): void {
const key = `change:${tableName}`;
this.listeners.get(key)?.forEach((cb) => cb(event));
}
// ---- 迁移 ----
private migrations: Map<number, (db: MetonaSqlark) => Promise<void>> = new Map();
/** 注册迁移 */
addMigration(version: number, up: (db: MetonaSqlark) => Promise<void>): void {
this.migrations.set(version, up);
}
/** 执行迁移到指定版本 */
async migrateTo(targetVersion: number): Promise<void> {
this.ensureReady();
for (const [version, up] of [...this.migrations.entries()].sort((a, b) => a[0] - b[0])) {
if (version <= targetVersion && version > this.version) {
await up(this);
this._version = version;
}
}
}
// ---- 插件 ----
/** 获取插件管理器 */
getPluginManager(): PluginManager {
return this.pluginManager;
}
/** 注册钩子 */
on(hook: import('./constants').HookName, callback: import('./plugin/index').HookCallback): void {
this.pluginManager.on(hook, callback);
}
// ---- 生命周期 ----
/** 关闭数据库 */
async close(): Promise<void> {
this.pluginManager.destroy();
await this.engine.close();
this.tableCache.clear();
this.ready = false;
}
/** 获取底层引擎 */
getEngine(): IStorageEngine {
return this.engine;
}
// ---- 内部 ----
private createEngine(): IStorageEngine {
const mode = this.mode;
const diskEngine = this.config.diskEngine ?? 'indexeddb';
switch (mode) {
case 'memory':
return new MemoryEngine();
case 'disk':
return diskEngine === 'opfs' ? new OPFSEngine() : new IndexedDBEngine();
case 'hybrid':
return new HybridEngine(diskEngine);
default:
throw new DatabaseError(`Unknown storage mode: ${mode}`, 'CONFIG_ERROR');
}
}
private ensureReady(): void {
if (!this.ready) {
throw new DatabaseError('Database not initialized. Call await db.init() first.', 'DB_NOT_READY');
}
}
}
+9
View File
@@ -0,0 +1,9 @@
/**
* metona-sqlark Engine — 存储引擎层
* @module engine
*/
export type { IStorageEngine } from './interface';
export { MemoryEngine } from './memory';
export { IndexedDBEngine } from './indexeddb';
export { OPFSEngine } from './opfs';
+185
View File
@@ -0,0 +1,185 @@
/**
* metona-sqlark IndexedDB Engine — 基于 IndexedDB 的持久化存储引擎
* @module engine/indexeddb
*/
import type { IStorageEngine } from './interface';
import type { QueryPlan, TableSchema } from '../constants';
import { DatabaseError } from '../constants';
import { MemoryEngine } from './memory';
import { matchWhere, applyOrderBy, projectColumns } from '../query/where-matcher';
export class IndexedDBEngine implements IStorageEngine {
readonly name = 'indexeddb';
private db: IDBDatabase | null = null;
private dbName = '';
private version = 1;
private memoryCache: MemoryEngine = new MemoryEngine();
async open(dbName: string, version: number): Promise<void> {
this.dbName = dbName; this.version = version;
await this.memoryCache.open(dbName, version);
return new Promise((resolve, reject) => {
const request = indexedDB.open(dbName, version);
request.onsuccess = () => { this.db = request.result; resolve(); };
request.onerror = () => reject(new DatabaseError(`Failed to open IndexedDB "${dbName}"`, 'IDB_OPEN_ERROR', request.error));
request.onblocked = () => reject(new DatabaseError(`IndexedDB "${dbName}" is blocked`, 'IDB_BLOCKED'));
});
}
async close(): Promise<void> {
if (this.db) { this.db.close(); this.db = null; }
await this.memoryCache.close();
}
isOpen(): boolean { return this.db !== null; }
// ---- 表管理 ----
async createTable(schema: TableSchema): Promise<void> {
await this.memoryCache.createTable(schema);
const db = this.ensureDB();
return new Promise((resolve, reject) => {
const newVersion = db.version + 1; db.close();
const request = indexedDB.open(this.dbName, newVersion);
request.onupgradeneeded = (event) => {
const db = (event.target as IDBOpenDBRequest).result;
const pkColumn = Object.entries(schema.columns).find(([, c]) => c.primaryKey)?.[0] ?? Object.keys(schema.columns)[0];
const store = db.createObjectStore(schema.name, { keyPath: pkColumn });
for (const [colName, colDef] of Object.entries(schema.columns)) {
if (colDef.index && colName !== pkColumn) {
store.createIndex(`idx_${colName}`, colName, { unique: colDef.unique ?? false });
}
}
};
request.onsuccess = () => { this.db = request.result; resolve(); };
request.onerror = () => reject(new DatabaseError(`Failed to create table "${schema.name}"`, 'IDB_UPGRADE_ERROR', request.error));
});
}
async dropTable(tableName: string): Promise<void> {
await this.memoryCache.dropTable(tableName);
const db = this.ensureDB();
return new Promise((resolve, reject) => {
const newVersion = db.version + 1; db.close();
const request = indexedDB.open(this.dbName, newVersion);
request.onupgradeneeded = (event) => {
const db = (event.target as IDBOpenDBRequest).result;
if (db.objectStoreNames.contains(tableName)) db.deleteObjectStore(tableName);
};
request.onsuccess = () => { this.db = request.result; resolve(); };
request.onerror = () => reject(new DatabaseError(`Failed to drop table "${tableName}"`, 'IDB_UPGRADE_ERROR', request.error));
});
}
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);
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);
tx.onerror = () => reject(new DatabaseError(`Insert failed for "${tableName}"`, 'IDB_TX_ERROR', tx.error));
});
}
async find(tableName: string, query: QueryPlan): Promise<Record<string, unknown>[]> {
const db = this.ensureDB();
return new Promise((resolve, reject) => {
const tx = db.transaction(tableName, 'readonly');
const req = tx.objectStore(tableName).getAll();
req.onsuccess = () => {
let results: Record<string, unknown>[] = req.result ?? [];
if (query.where && Object.keys(query.where).length > 0) {
results = results.filter((row) => matchWhere(row, query.where!));
}
if (query.orderBy && query.orderBy.length > 0) {
results = applyOrderBy(results, query.orderBy);
}
const offset = query.offset ?? 0;
const limit = query.limit ?? results.length;
results = results.slice(offset, offset + limit);
if (query.columns && query.columns.length > 0 && query.columns[0] !== '*') {
results = results.map((r) => projectColumns(r, query.columns!));
}
resolve(results);
};
req.onerror = () => reject(new DatabaseError(`Find failed for "${tableName}"`, 'IDB_READ_ERROR', req.error));
});
}
async update(tableName: string, query: QueryPlan, updates: Record<string, unknown>): Promise<number> {
await this.memoryCache.update(tableName, query, updates);
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++;
}
}
};
tx.oncomplete = () => resolve(count);
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);
const db = this.ensureDB();
const schema = await this.memoryCache.getTableSchema(tableName);
if (!schema) throw new DatabaseError(`Table "${tableName}" not found`, 'TABLE_NOT_FOUND');
const pkColumn = Object.entries(schema.columns).find(([, c]) => c.primaryKey)?.[0] ?? Object.keys(schema.columns)[0];
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)) {
store.delete(row[pkColumn] as IDBValidKey); count++;
}
}
};
tx.oncomplete = () => resolve(count);
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);
const db = this.ensureDB();
return new Promise((resolve, reject) => {
const tx = db.transaction(tableName, 'readwrite');
tx.objectStore(tableName).clear();
tx.oncomplete = () => resolve();
tx.onerror = () => reject(new DatabaseError(`Clear failed for "${tableName}"`, 'IDB_TX_ERROR', tx.error));
});
}
private ensureDB(): IDBDatabase {
if (!this.db) throw new DatabaseError('Database not opened', 'DB_NOT_OPEN');
return this.db;
}
}
+57
View File
@@ -0,0 +1,57 @@
/**
* metona-sqlark Engine Interface — 存储引擎抽象接口
* @module engine/interface
*/
import type { QueryPlan, TableSchema } from '../constants';
// ---------------------------------------------------------------------------
// IStorageEngine — 所有存储引擎必须实现的接口
// ---------------------------------------------------------------------------
export interface IStorageEngine {
/** 引擎名称 */
readonly name: string;
/** 打开数据库 */
open(dbName: string, version: number): Promise<void>;
/** 关闭数据库 */
close(): Promise<void>;
/** 检查数据库是否已打开 */
isOpen(): boolean;
/** 创建表 */
createTable(schema: TableSchema): Promise<void>;
/** 删除表 */
dropTable(tableName: string): Promise<void>;
/** 检查表是否存在 */
hasTable(tableName: string): Promise<boolean>;
/** 获取所有表名 */
getTableNames(): Promise<string[]>;
/** 获取表结构 */
getTableSchema(tableName: string): Promise<TableSchema | null>;
/** 插入行,返回主键值列表 */
insert(tableName: string, rows: Record<string, unknown>[]): Promise<string[]>;
/** 查询行 */
find(tableName: string, query: QueryPlan): Promise<Record<string, unknown>[]>;
/** 更新行,返回影响行数 */
update(tableName: string, query: QueryPlan, updates: Record<string, unknown>): Promise<number>;
/** 删除行,返回影响行数 */
delete(tableName: string, query: QueryPlan): Promise<number>;
/** 计数 */
count(tableName: string, query?: QueryPlan): Promise<number>;
/** 清空表数据(保留结构) */
clear(tableName: string): Promise<void>;
}
+215
View File
@@ -0,0 +1,215 @@
/**
* metona-sqlark Memory Engine — 基于 Map 的内存存储引擎
* @module engine/memory
*/
import type { IStorageEngine } from './interface';
import type { QueryPlan, TableSchema } from '../constants';
import { DatabaseError } from '../constants';
import { matchWhere, applyOrderBy, projectColumns } from '../query/where-matcher';
export class MemoryEngine implements IStorageEngine {
readonly name = 'memory';
private tables: Map<string, Map<string, Record<string, unknown>>> = new Map();
private schemas: Map<string, TableSchema> = new Map();
private indexes: Map<string, Map<string, Map<unknown, Set<string>>>> = new Map();
private opened = false;
// ---- 生命周期 ----
async open(_dbName: string, _version: number): Promise<void> { this.opened = true; }
async close(): Promise<void> {
this.tables.clear(); this.schemas.clear(); this.indexes.clear(); this.opened = false;
}
isOpen(): boolean { return this.opened; }
// ---- 表管理 ----
async createTable(schema: TableSchema): Promise<void> {
if (this.schemas.has(schema.name)) throw new DatabaseError(`Table "${schema.name}" already exists`, 'TABLE_EXISTS');
this.schemas.set(schema.name, schema);
this.tables.set(schema.name, new Map());
const tableIndexes = new Map<string, Map<unknown, Set<string>>>();
for (const [colName, colDef] of Object.entries(schema.columns)) {
if (colDef.index || colDef.unique) tableIndexes.set(colName, new Map());
}
this.indexes.set(schema.name, tableIndexes);
}
async dropTable(tableName: string): Promise<void> {
this.ensureTable(tableName);
this.schemas.delete(tableName); this.tables.delete(tableName); this.indexes.delete(tableName);
}
async hasTable(tableName: string): Promise<boolean> { return this.schemas.has(tableName); }
async getTableNames(): Promise<string[]> { return Array.from(this.schemas.keys()); }
async getTableSchema(tableName: string): Promise<TableSchema | null> { return this.schemas.get(tableName) ?? null; }
// ---- CRUD ----
async insert(tableName: string, rows: Record<string, unknown>[]): Promise<string[]> {
this.ensureTable(tableName);
const schema = this.schemas.get(tableName)!;
const table = this.tables.get(tableName)!;
const pkColumn = this.getPrimaryKey(schema);
const pks: string[] = [];
for (const row of rows) {
const validatedRow = this.validateRow(schema, row);
const pkValue = String(validatedRow[pkColumn]);
if (table.has(pkValue)) throw new DatabaseError(`Duplicate primary key "${pkValue}" in table "${tableName}"`, 'DUPLICATE_KEY');
this.checkUniqueness(schema, validatedRow);
table.set(pkValue, validatedRow);
this.updateIndexes(tableName, validatedRow, pkValue);
pks.push(pkValue);
}
return pks;
}
async find(tableName: string, query: QueryPlan): Promise<Record<string, unknown>[]> {
this.ensureTable(tableName);
const table = this.tables.get(tableName)!;
let results = this.tryIndexLookup(tableName, table, query);
if (query.where && Object.keys(query.where).length > 0) {
results = results.filter((row) => matchWhere(row, query.where!));
}
if (query.orderBy && query.orderBy.length > 0) {
results = applyOrderBy(results, query.orderBy);
}
const offset = query.offset ?? 0;
const limit = query.limit ?? results.length;
results = results.slice(offset, offset + limit);
if (query.columns && query.columns.length > 0 && query.columns[0] !== '*') {
results = results.map((row) => projectColumns(row, query.columns!));
}
return results;
}
async update(tableName: string, query: QueryPlan, updates: Record<string, unknown>): Promise<number> {
this.ensureTable(tableName);
const schema = this.schemas.get(tableName)!;
const table = this.tables.get(tableName)!;
let count = 0;
for (const [pk, row] of table) {
if (!query.where || Object.keys(query.where).length === 0 || matchWhere(row, query.where)) {
const updated = { ...row, ...updates };
this.validateRow(schema, updated);
table.set(pk, updated);
count++;
}
}
return count;
}
async delete(tableName: string, query: QueryPlan): Promise<number> {
this.ensureTable(tableName);
const table = this.tables.get(tableName)!;
const toDelete: string[] = [];
for (const [pk, row] of table) {
if (!query.where || Object.keys(query.where).length === 0 || matchWhere(row, query.where)) {
toDelete.push(pk);
}
}
for (const pk of toDelete) table.delete(pk);
return toDelete.length;
}
async count(tableName: string, query?: QueryPlan): Promise<number> {
this.ensureTable(tableName);
const table = this.tables.get(tableName)!;
if (!query?.where || Object.keys(query.where).length === 0) return table.size;
let result = 0;
for (const row of table.values()) { if (matchWhere(row, query.where)) result++; }
return result;
}
async clear(tableName: string): Promise<void> {
this.ensureTable(tableName);
this.tables.get(tableName)!.clear();
const tableIndexes = this.indexes.get(tableName);
if (tableIndexes) for (const colIndex of tableIndexes.values()) colIndex.clear();
}
// ---- 内部辅助 ----
private ensureTable(tableName: string): void {
if (!this.tables.has(tableName)) throw new DatabaseError(`Table "${tableName}" does not exist`, 'TABLE_NOT_FOUND');
}
private getPrimaryKey(schema: TableSchema): string {
for (const [name, col] of Object.entries(schema.columns)) { if (col.primaryKey) return name; }
return Object.keys(schema.columns)[0];
}
private validateRow(schema: TableSchema, row: Record<string, unknown>): Record<string, unknown> {
const validated: Record<string, unknown> = {};
for (const [colName, colDef] of Object.entries(schema.columns)) {
let value = row[colName];
if (value === undefined && colDef.default !== undefined) value = colDef.default;
if (colDef.required && (value === undefined || value === null)) {
throw new DatabaseError(`Column "${colName}" is required in table "${schema.name}"`, 'VALIDATION_ERROR');
}
if (value !== undefined && value !== null) this.checkType(colName, colDef.type, value);
if (value !== undefined) validated[colName] = value;
}
return validated;
}
private checkType(colName: string, type: string, value: unknown): void {
const jsType = typeof value;
switch (type) {
case 'string': if (jsType !== 'string') throw new DatabaseError(`Column "${colName}" expects string, got ${jsType}`, 'TYPE_ERROR'); break;
case 'number': if (jsType !== 'number') throw new DatabaseError(`Column "${colName}" expects number, got ${jsType}`, 'TYPE_ERROR'); break;
case 'boolean': if (jsType !== 'boolean') throw new DatabaseError(`Column "${colName}" expects boolean, got ${jsType}`, 'TYPE_ERROR'); break;
case 'date': if (jsType !== 'string' || isNaN(Date.parse(value as string))) throw new DatabaseError(`Column "${colName}" expects valid date`, 'TYPE_ERROR'); break;
case 'json': if (jsType !== 'object') throw new DatabaseError(`Column "${colName}" expects object/array, got ${jsType}`, 'TYPE_ERROR'); break;
}
}
/** O(1) 唯一性检查:利用哈希索引 */
private checkUniqueness(schema: TableSchema, row: Record<string, unknown>): void {
const tableIndexes = this.indexes.get(schema.name);
if (!tableIndexes) return;
for (const [colName, colDef] of Object.entries(schema.columns)) {
if (!colDef.unique || row[colName] === undefined || row[colName] === null) continue;
const colIndex = tableIndexes.get(colName);
if (colIndex && colIndex.has(row[colName])) {
throw new DatabaseError(`Unique constraint violation on column "${colName}" in table "${schema.name}"`, 'UNIQUE_VIOLATION');
}
}
}
/** 索引查找 */
private tryIndexLookup(
tableName: string, table: Map<string, Record<string, unknown>>, query: QueryPlan,
): Record<string, unknown>[] {
const tableIndexes = this.indexes.get(tableName);
if (!tableIndexes || !query.where) return Array.from(table.values());
for (const [col, condition] of Object.entries(query.where)) {
if (typeof condition !== 'object' || condition === null) {
const colIndex = tableIndexes.get(col);
if (colIndex) {
const pks = colIndex.get(condition);
if (pks) {
const result: Record<string, unknown>[] = [];
for (const pk of pks) { const r = table.get(pk); if (r) result.push(r); }
return result;
}
return [];
}
}
}
return Array.from(table.values());
}
/** 更新索引 */
private updateIndexes(tableName: string, row: Record<string, unknown>, pk: string): void {
const tableIndexes = this.indexes.get(tableName);
if (!tableIndexes) return;
for (const [colName, colIndex] of tableIndexes) {
const value = row[colName];
if (value !== undefined && value !== null) {
if (!colIndex.has(value)) colIndex.set(value, new Set());
colIndex.get(value)!.add(pk);
}
}
}
}
+174
View File
@@ -0,0 +1,174 @@
/**
* metona-sqlark OPFS Engine — 基于 Origin Private File System 的持久化存储引擎
* @module engine/opfs
*
* 使用 JSON-per-table 文件存储方案。
* 目录结构:{dbName}/tables/{tableName}.json
*/
import type { IStorageEngine } from './interface';
import type { QueryPlan, TableSchema } from '../constants';
import { DatabaseError } from '../constants';
import { MemoryEngine } from './memory';
// ---------------------------------------------------------------------------
// OPFSEngine
// ---------------------------------------------------------------------------
export class OPFSEngine implements IStorageEngine {
readonly name = 'opfs';
private root: FileSystemDirectoryHandle | null = null;
private tablesDir: FileSystemDirectoryHandle | null = null;
private dbName = '';
// 运行时内存缓存(OPFS 文件读写有延迟)
private memoryCache: MemoryEngine = new MemoryEngine();
// ---- 生命周期 ----
async open(dbName: string, version: number): Promise<void> {
this.dbName = dbName;
await this.memoryCache.open(dbName, version);
// 获取 OPFS 根目录
this.root = await navigator.storage.getDirectory();
// 创建数据库目录
this.tablesDir = await this.root.getDirectoryHandle(dbName, { create: true });
}
async close(): Promise<void> {
this.root = null;
this.tablesDir = null;
await this.memoryCache.close();
}
isOpen(): boolean {
return this.tablesDir !== null;
}
// ---- 表管理 ----
async createTable(schema: TableSchema): Promise<void> {
await this.memoryCache.createTable(schema);
// OPFS 中表以空 JSON 数组文件形式存在
await this.writeTableData(schema.name, []);
}
async dropTable(tableName: string): Promise<void> {
await this.memoryCache.dropTable(tableName);
if (this.tablesDir) {
try {
await this.tablesDir.removeEntry(`${tableName}.json`);
} catch {
// 文件不存在则忽略
}
}
}
async hasTable(tableName: string): Promise<boolean> {
if (!this.tablesDir) return false;
try {
await this.tablesDir.getFileHandle(`${tableName}.json`);
return true;
} catch {
return false;
}
}
async getTableNames(): Promise<string[]> {
if (!this.tablesDir) return [];
const names: string[] = [];
for await (const [name] of (this.tablesDir as any).entries()) {
if (name.endsWith('.json')) {
names.push(name.replace('.json', ''));
}
}
return names;
}
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);
// 持久化到 OPFS
const allRows = await this.memoryCache.find(tableName, { table: tableName });
await this.writeTableData(tableName, allRows);
return pks;
}
async find(tableName: string, query: QueryPlan): Promise<Record<string, unknown>[]> {
return this.memoryCache.find(tableName, query);
}
async update(tableName: string, query: QueryPlan, updates: Record<string, unknown>): Promise<number> {
const count = await this.memoryCache.update(tableName, query, updates);
const allRows = await this.memoryCache.find(tableName, { table: tableName });
await this.writeTableData(tableName, allRows);
return count;
}
async delete(tableName: string, query: QueryPlan): Promise<number> {
const count = await this.memoryCache.delete(tableName, query);
const allRows = await this.memoryCache.find(tableName, { table: tableName });
await this.writeTableData(tableName, allRows);
return count;
}
async count(tableName: string, query?: QueryPlan): Promise<number> {
return this.memoryCache.count(tableName, query);
}
async clear(tableName: string): Promise<void> {
await this.memoryCache.clear(tableName);
await this.writeTableData(tableName, []);
}
// ---- 内部辅助 ----
private ensureDir(): FileSystemDirectoryHandle {
if (!this.tablesDir) {
throw new DatabaseError('Database not opened', 'DB_NOT_OPEN');
}
return this.tablesDir;
}
private async writeTableData(tableName: string, data: Record<string, unknown>[]): Promise<void> {
const dir = this.ensureDir();
const fileName = `${tableName}.json`;
const fileHandle = await dir.getFileHandle(fileName, { create: true });
const writable = await fileHandle.createWritable();
await writable.write(JSON.stringify(data));
await writable.close();
}
private async readTableData(tableName: string): Promise<Record<string, unknown>[]> {
const dir = this.ensureDir();
const fileName = `${tableName}.json`;
try {
const fileHandle = await dir.getFileHandle(fileName);
const file = await fileHandle.getFile();
const text = await file.text();
return JSON.parse(text);
} catch {
return [];
}
}
/** 从 OPFS 加载表数据到内存缓存 */
async loadTableIntoMemory(tableName: string, schema: TableSchema): Promise<void> {
await this.memoryCache.createTable(schema);
const rows = await this.readTableData(tableName);
if (rows.length > 0) {
// 直接用 Map 设置绕过 insert 校验
for (const row of rows) {
await this.memoryCache.insert(tableName, [row]);
}
}
}
}
+146
View File
@@ -0,0 +1,146 @@
/**
* metona-sqlark Hybrid Engine — 内存 + 磁盘混合存储引擎
* @module hybrid/index
*
* 采用 write-through 策略:
* - 所有写操作同时写入内存和磁盘
* - 所有读操作直接从内存返回
* - 数据库打开时从磁盘加载数据到内存
*/
import type { IStorageEngine } from '../engine/interface';
import type { QueryPlan, TableSchema, DiskEngine } from '../constants';
import { MemoryEngine } from '../engine/memory';
import { IndexedDBEngine } from '../engine/indexeddb';
import { OPFSEngine } from '../engine/opfs';
// ---------------------------------------------------------------------------
// HybridEngine
// ---------------------------------------------------------------------------
export class HybridEngine implements IStorageEngine {
readonly name = 'hybrid';
private memoryEngine: MemoryEngine;
private diskEngine: IStorageEngine;
private diskEngineType: DiskEngine;
constructor(diskEngine: DiskEngine = 'indexeddb') {
this.memoryEngine = new MemoryEngine();
this.diskEngineType = diskEngine;
this.diskEngine = diskEngine === 'opfs' ? new OPFSEngine() : new IndexedDBEngine();
}
// ---- 生命周期 ----
async open(dbName: string, version: number): Promise<void> {
// 先打开磁盘引擎
await this.diskEngine.open(dbName, version);
// 再打开内存引擎
await this.memoryEngine.open(dbName, version);
// 从磁盘加载现存表
const tableNames = await this.diskEngine.getTableNames();
for (const tableName of tableNames) {
const schema = await this.diskEngine.getTableSchema(tableName);
if (!schema) continue;
// 在内存中创建表
await this.memoryEngine.createTable(schema);
// 从磁盘加载数据到内存
const rows = await this.diskEngine.find(tableName, { table: tableName });
if (rows.length > 0) {
try {
await this.memoryEngine.insert(tableName, rows);
} catch (e) {
// eslint-disable-next-line no-console
console.warn(`[metona-sqlark] Failed to load table "${tableName}" data from disk:`, e);
}
}
}
}
async close(): Promise<void> {
await this.memoryEngine.close();
await this.diskEngine.close();
}
isOpen(): boolean {
return this.memoryEngine.isOpen() && this.diskEngine.isOpen();
}
// ---- 表管理 ----
async createTable(schema: TableSchema): Promise<void> {
await this.memoryEngine.createTable(schema);
await this.diskEngine.createTable(schema);
}
async dropTable(tableName: string): Promise<void> {
await this.memoryEngine.dropTable(tableName);
await this.diskEngine.dropTable(tableName);
}
async hasTable(tableName: string): Promise<boolean> {
return this.memoryEngine.hasTable(tableName);
}
async getTableNames(): Promise<string[]> {
return this.memoryEngine.getTableNames();
}
async getTableSchema(tableName: string): Promise<TableSchema | null> {
return this.memoryEngine.getTableSchema(tableName);
}
// ---- CRUDwrite-through 策略) ----
async insert(tableName: string, rows: Record<string, unknown>[]): Promise<string[]> {
const pks = await this.memoryEngine.insert(tableName, rows);
// write-through: 同步写入磁盘
await this.diskEngine.insert(tableName, rows);
return pks;
}
async find(tableName: string, query: QueryPlan): Promise<Record<string, unknown>[]> {
// 直接从内存读取
return this.memoryEngine.find(tableName, query);
}
async update(tableName: string, query: QueryPlan, updates: Record<string, unknown>): Promise<number> {
const count = await this.memoryEngine.update(tableName, query, updates);
// write-through: 同步更新磁盘
await this.diskEngine.update(tableName, query, updates);
return count;
}
async delete(tableName: string, query: QueryPlan): Promise<number> {
const count = await this.memoryEngine.delete(tableName, query);
// write-through: 同步删除磁盘
await this.diskEngine.delete(tableName, query);
return count;
}
async count(tableName: string, query?: QueryPlan): Promise<number> {
return this.memoryEngine.count(tableName, query);
}
async clear(tableName: string): Promise<void> {
await this.memoryEngine.clear(tableName);
await this.diskEngine.clear(tableName);
}
// ---- 引擎信息 ----
/** 获取磁盘引擎类型 */
getDiskEngineType(): DiskEngine {
return this.diskEngineType;
}
/** 获取内存引擎(供内部使用) */
getMemoryEngine(): MemoryEngine {
return this.memoryEngine;
}
}
+83
View File
@@ -0,0 +1,83 @@
/**
* metona-sqlark — 入口文件
* @module metona-sqlark
* @version 0.1.12
*
* 前端关系型数据库,内存与磁盘双模式。
* 支持 Query Builder 链式 API 和 SQL 字符串查询。
*/
import { MetonaSqlark } from './core';
import type { DatabaseConfig } from './constants';
import { VERSION } from './constants';
// ---------------------------------------------------------------------------
// 工厂函数
// ---------------------------------------------------------------------------
/**
* 创建数据库实例并初始化
*
* @example
* ```ts
* const db = await MetonaSqlark.create({
* name: 'my-app',
* mode: 'hybrid',
* });
*
* await db.defineTable('users', {
* id: { type: 'string', primaryKey: true },
* name: { type: 'string', required: true },
* });
*
* await db.table('users').insert({ id: '1', name: 'Alice' });
* const results = await db.query('SELECT * FROM users');
* ```
*/
async function create(config: DatabaseConfig): Promise<MetonaSqlark> {
const db = new MetonaSqlark(config);
await db.init();
return db;
}
// ---------------------------------------------------------------------------
// 全局 API
// ---------------------------------------------------------------------------
const api = {
VERSION,
version: VERSION,
create,
MetonaSqlark,
MeSqlark: MetonaSqlark,
};
// 浏览器全局挂载
declare global { interface Window { MetonaSqlark: typeof api; MeSqlark: typeof api; } }
if (typeof window !== 'undefined') {
window.MetonaSqlark = api;
window.MeSqlark = api;
}
export default api;
export {
api,
VERSION,
create,
MetonaSqlark,
};
// 别名
export const MeSqlark = MetonaSqlark;
// 类型导出
export type { DatabaseConfig, TableSchema, ColumnDef, FieldType, StorageMode, DiskEngine } from './constants';
export type { IStorageEngine } from './engine/interface';
export type { Statement, SelectStatement, InsertStatement, UpdateStatement, DeleteStatement } from './query/ast';
export { MemoryEngine } from './engine/memory';
export { IndexedDBEngine } from './engine/indexeddb';
export { OPFSEngine } from './engine/opfs';
export { HybridEngine } from './hybrid/index';
export { Table } from './table/table';
export { parse } from './sql/parser';
export { tokenize } from './sql/lexer';
+80
View File
@@ -0,0 +1,80 @@
// @ts-nocheck
/**
* metona-sqlark React Integration (v0.1.11)
* @module integrations/react
*
* 轻量 React hooks,需要 react 作为 peer dependency。
*
* @example
* import { useQuery, useTable } from 'metona-sqlark/react';
* import { db } from './db';
*
* function UserList() {
* const { data, loading, refresh } = useQuery(db, 'SELECT * FROM users');
* return loading ? <div>Loading...</div> : <div>{data.map(u => <div key={u.id}>{u.name}</div>)}</div>;
* }
*/
import type { MetonaSqlark } from '../core';
import { useState, useEffect, useCallback, useRef } from 'react';
/** useQuery: 执行 SQL 查询 */
export function useQuery(
db: MetonaSqlark,
sql: string,
deps: unknown[] = [],
): { data: Record<string, unknown>[]; loading: boolean; error: Error | null; refresh: () => void } {
const [data, setData] = useState<Record<string, unknown>[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
const execute = useCallback(async () => {
setLoading(true);
try {
const result = await db.query(sql) as Record<string, unknown>[];
setData(result);
setError(null);
} catch (e) {
setError(e as Error);
} finally {
setLoading(false);
}
}, [db, sql, ...deps]);
useEffect(() => { execute(); }, [execute]);
return { data, loading, error, refresh: execute };
}
/** useTable: 快速获取表数据 */
export function useTable(
db: MetonaSqlark,
tableName: string,
): { data: Record<string, unknown>[]; loading: boolean; refresh: () => void } {
const { data, loading, refresh } = useQuery(db, `SELECT * FROM ${tableName}`, [tableName]);
return { data, loading, refresh };
}
/** useDatabase: 创建/管理数据库实例 */
export function useDatabase(config: import('../constants').DatabaseConfig): {
db: MetonaSqlark | null;
ready: boolean;
error: Error | null;
} {
const [db, setDb] = useState<MetonaSqlark | null>(null);
const [ready, setReady] = useState(false);
const [error, setError] = useState<Error | null>(null);
const initRef = useRef(false);
useEffect(() => {
if (initRef.current) return;
initRef.current = true;
const instance = new MetonaSqlark(config);
instance.init()
.then(() => { setDb(instance); setReady(true); })
.catch(setError);
return () => { instance.close(); };
}, []);
return { db, ready, error };
}
+77
View File
@@ -0,0 +1,77 @@
// @ts-nocheck
/**
* metona-sqlark Vue Integration (v0.1.11)
* @module integrations/vue
*
* 轻量 Vue composables,需要 vue 作为 peer dependency。
*
* @example
* import { useSqlarkQuery, useSqlarkTable } from 'metona-sqlark/vue';
* import { db } from './db';
*
* const { data, loading, refresh } = useSqlarkQuery(db, 'SELECT * FROM users');
*/
import type { MetonaSqlark } from '../core';
import { ref, watch, onMounted, type Ref } from 'vue';
/** useSqlarkQuery: 执行 SQL 查询 */
export function useSqlarkQuery(
db: MetonaSqlark,
sql: string,
deps: Ref<unknown>[] = [],
): { data: Ref<Record<string, unknown>[]>; loading: Ref<boolean>; error: Ref<Error | null>; refresh: () => void } {
const data = ref<Record<string, unknown>[]>([]) as Ref<Record<string, unknown>[]>;
const loading = ref(true);
const error = ref<Error | null>(null) as Ref<Error | null>;
const execute = async () => {
loading.value = true;
try {
data.value = await db.query(sql) as Record<string, unknown>[];
error.value = null;
} catch (e) {
error.value = e as Error;
} finally {
loading.value = false;
}
};
onMounted(execute);
watch([() => sql, ...deps], execute);
return { data, loading, error, refresh: execute };
}
/** useSqlarkTable: 快速获取表数据 */
export function useSqlarkTable(
db: MetonaSqlark,
tableName: string,
): { data: Ref<Record<string, unknown>[]>; loading: Ref<boolean>; refresh: () => void } {
const { data, loading, refresh } = useSqlarkQuery(db, `SELECT * FROM ${tableName}`);
return { data, loading, refresh };
}
/** useSqlarkDatabase: 创建/管理数据库实例 */
export function useSqlarkDatabase(config: import('../constants').DatabaseConfig): {
db: Ref<MetonaSqlark | null>;
ready: Ref<boolean>;
error: Ref<Error | null>;
} {
const db = ref<MetonaSqlark | null>(null) as Ref<MetonaSqlark | null>;
const ready = ref(false);
const error = ref<Error | null>(null) as Ref<Error | null>;
onMounted(async () => {
try {
const instance = new MetonaSqlark(config);
await instance.init();
db.value = instance;
ready.value = true;
} catch (e) {
error.value = e as Error;
}
});
return { db, ready, error };
}
+91
View File
@@ -0,0 +1,91 @@
/**
* metona-sqlark Plugin — 插件系统
* @module plugin
*
* 管理插件的注册、生命周期和钩子调度。
*/
import type { MetonaPlugin, HookName } from '../constants';
// ---------------------------------------------------------------------------
// Hook 回调类型
// ---------------------------------------------------------------------------
export type HookCallback = (...args: unknown[]) => void | Promise<void>;
// ---------------------------------------------------------------------------
// PluginManager
// ---------------------------------------------------------------------------
export class PluginManager {
private plugins: MetonaPlugin[] = [];
private hooks: Map<HookName, HookCallback[]> = new Map();
/** 注册插件 */
register(plugin: MetonaPlugin): void {
// 按优先级插入
const priority = plugin.priority ?? 0;
const insertIndex = this.plugins.findIndex(
(p) => (p.priority ?? 0) < priority,
);
if (insertIndex === -1) {
this.plugins.push(plugin);
} else {
this.plugins.splice(insertIndex, 0, plugin);
}
// 安装
plugin.install(null); // 实际引用由 MetonaSqlark 注入
}
/** 卸载插件 */
unregister(pluginName: string): void {
const idx = this.plugins.findIndex((p) => p.name === pluginName);
if (idx !== -1) {
this.plugins[idx].destroy();
this.plugins.splice(idx, 1);
}
}
/** 获取所有已注册插件 */
getPlugins(): MetonaPlugin[] {
return [...this.plugins];
}
/** 添加钩子回调 */
on(hook: HookName, callback: HookCallback): void {
const callbacks = this.hooks.get(hook) ?? [];
callbacks.push(callback);
this.hooks.set(hook, callbacks);
}
/** 移除钩子回调 */
off(hook: HookName, callback: HookCallback): void {
const callbacks = this.hooks.get(hook);
if (callbacks) {
const idx = callbacks.indexOf(callback);
if (idx !== -1) callbacks.splice(idx, 1);
}
}
/** 触发钩子 */
async trigger(hook: HookName, ...args: unknown[]): Promise<void> {
const callbacks = this.hooks.get(hook);
if (callbacks) {
for (const cb of callbacks) {
await cb(...args);
}
}
}
/** 销毁所有插件 */
destroy(): void {
for (const plugin of this.plugins) {
try { plugin.destroy(); } catch (e) {
// eslint-disable-next-line no-console
console.warn(`[metona-sqlark] Plugin "${plugin.name}" destroy error:`, e);
}
}
this.plugins = [];
this.hooks.clear();
}
}
+157
View File
@@ -0,0 +1,157 @@
/**
* metona-sqlark Query AST — 查询抽象语法树类型定义
* @module query/ast
*
* QueryBuilder 和 SQL Parser 统一输出此 AST
* Executor 只认 AST,保证两种查询接口行为一致。
*/
import type { WhereCondition, OrderBy } from '../constants';
// ---------------------------------------------------------------------------
// AST 语句类型枚举
// ---------------------------------------------------------------------------
export type StatementType =
| 'SELECT'
| 'INSERT'
| 'UPDATE'
| 'DELETE'
| 'CREATE_TABLE'
| 'DROP_TABLE';
// ---------------------------------------------------------------------------
// 列引用
// ---------------------------------------------------------------------------
/** 列引用,'*' 表示所有列;支持 'table.column' 格式 */
export type ColumnRef = string;
// ---------------------------------------------------------------------------
// JOIN
// ---------------------------------------------------------------------------
/** JOIN 类型 */
export type JoinType = 'INNER' | 'LEFT' | 'RIGHT' | 'CROSS';
/** JOIN 子句 */
export interface JoinClause {
type: JoinType;
table: string;
alias?: string;
on: WhereCondition;
}
// ---------------------------------------------------------------------------
// 聚合函数
// ---------------------------------------------------------------------------
/** 聚合函数类型 */
export type AggregateFunc = 'COUNT' | 'SUM' | 'AVG' | 'MIN' | 'MAX';
/** 聚合表达式 */
export interface AggregateExpression {
type: 'AGGREGATE';
func: AggregateFunc;
column: string; // '*' for COUNT(*)
alias?: string;
}
// ---------------------------------------------------------------------------
// DDL: CREATE TABLE
// ---------------------------------------------------------------------------
export interface ASTColumnDef {
name: string;
type: string;
primaryKey?: boolean;
unique?: boolean;
required?: boolean;
default?: unknown;
index?: boolean;
maxLength?: number;
min?: number;
max?: number;
}
export interface CreateTableStatement {
type: 'CREATE_TABLE';
name: string;
columns: ASTColumnDef[];
}
// ---------------------------------------------------------------------------
// DDL: DROP TABLE
// ---------------------------------------------------------------------------
export interface DropTableStatement {
type: 'DROP_TABLE';
name: string;
}
// ---------------------------------------------------------------------------
// DML: INSERT
// ---------------------------------------------------------------------------
export interface InsertStatement {
type: 'INSERT';
into: string;
columns?: string[];
values: unknown[][];
}
// ---------------------------------------------------------------------------
// DML: UPDATE
// ---------------------------------------------------------------------------
export interface UpdateStatement {
type: 'UPDATE';
table: string;
sets: Record<string, unknown>;
where: WhereCondition;
}
// ---------------------------------------------------------------------------
// DML: DELETE
// ---------------------------------------------------------------------------
export interface DeleteStatement {
type: 'DELETE';
from: string;
where: WhereCondition;
}
// ---------------------------------------------------------------------------
// DQL: SELECT
// ---------------------------------------------------------------------------
export interface SelectStatement {
type: 'SELECT';
columns: ColumnRef[];
distinct?: boolean;
from: string;
/** 主表别名 */
alias?: string;
/** JOIN 子句列表 */
joins?: JoinClause[];
where: WhereCondition;
/** GROUP BY */
groupBy?: string[];
/** HAVING */
having?: WhereCondition;
orderBy?: OrderBy[];
limit?: number;
offset?: number;
}
// ---------------------------------------------------------------------------
// AST 联合类型
// ---------------------------------------------------------------------------
export type Statement =
| SelectStatement
| InsertStatement
| UpdateStatement
| DeleteStatement
| CreateTableStatement
| DropTableStatement;
+182
View File
@@ -0,0 +1,182 @@
/**
* metona-sqlark Query Builder — 链式查询构建器
* @module query/builder
*
* 链式调用 → 构建 AST → 执行引擎操作。
* 支持 JOIN(需要 Executor)。
*/
import type { IStorageEngine } from '../engine/interface';
import type { WhereCondition, OrderBy, SortDirection } from '../constants';
import type { SelectStatement, UpdateStatement, DeleteStatement, JoinType, JoinClause } from './ast';
import { QueryExecutor } from './executor';
// ---------------------------------------------------------------------------
// SelectQueryBuilder
// ---------------------------------------------------------------------------
export class SelectQueryBuilder {
private _where: WhereCondition = {};
private _orderBy: OrderBy[] = [];
private _limit?: number;
private _offset?: number;
private _joins: JoinClause[] = [];
private _alias?: string;
private _executor?: QueryExecutor;
constructor(
private engine: IStorageEngine,
private tableName: string,
private _columns: string[] = ['*'],
executor?: QueryExecutor,
) {
this._executor = executor;
}
/** 主表别名 */
as(alias: string): this {
this._alias = alias;
return this;
}
/** INNER JOIN */
innerJoin(table: string, on: WhereCondition, alias?: string): this {
return this._addJoin('INNER', table, on, alias);
}
/** LEFT JOIN */
leftJoin(table: string, on: WhereCondition, alias?: string): this {
return this._addJoin('LEFT', table, on, alias);
}
/** RIGHT JOIN */
rightJoin(table: string, on: WhereCondition, alias?: string): this {
return this._addJoin('RIGHT', table, on, alias);
}
/** CROSS JOIN */
crossJoin(table: string, alias?: string): this {
return this._addJoin('CROSS', table, {}, alias);
}
/** 通用 JOIN */
join(table: string, on: WhereCondition, alias?: string): this {
return this._addJoin('INNER', table, on, alias);
}
private _addJoin(type: JoinType, table: string, on: WhereCondition, alias?: string): this {
this._joins.push({ type, table, on, alias });
return this;
}
/** 添加过滤条件 */
where(condition: WhereCondition): this {
this._where = { ...this._where, ...condition };
return this;
}
/** 排序 */
orderBy(column: string, direction: SortDirection = 'asc'): this {
this._orderBy.push({ column, direction });
return this;
}
/** 限制返回条数 */
limit(n: number): this {
this._limit = n;
return this;
}
/** 偏移量 */
offset(n: number): this {
this._offset = n;
return this;
}
/** 执行查询 */
async execute(): Promise<Record<string, unknown>[]> {
// 有 JOIN → 通过 Executor 执行
if (this._joins.length > 0 && this._executor) {
const ast = this.toAST();
return this._executor.execute(ast) as Promise<Record<string, unknown>[]>;
}
// 无 JOIN → 直接调用引擎
return this.engine.find(this.tableName, {
table: this.tableName,
columns: this._columns,
where: this._where,
orderBy: this._orderBy.length > 0 ? this._orderBy : undefined,
limit: this._limit,
offset: this._offset,
});
}
/** 获取 AST */
toAST(): SelectStatement {
return {
type: 'SELECT',
columns: this._columns,
from: this.tableName,
alias: this._alias,
joins: this._joins.length > 0 ? [...this._joins] : undefined,
where: this._where,
orderBy: this._orderBy.length > 0 ? this._orderBy : undefined,
limit: this._limit,
offset: this._offset,
};
}
}
// ---------------------------------------------------------------------------
// UpdateQueryBuilder
// ---------------------------------------------------------------------------
export class UpdateQueryBuilder {
private _where: WhereCondition = {};
constructor(
private engine: IStorageEngine,
private tableName: string,
private _updates: Record<string, unknown>,
) {}
where(condition: WhereCondition): this {
this._where = { ...this._where, ...condition };
return this;
}
async execute(): Promise<number> {
return this.engine.update(this.tableName, { table: this.tableName, where: this._where }, this._updates);
}
toAST(): UpdateStatement {
return { type: 'UPDATE', table: this.tableName, sets: this._updates, where: this._where };
}
}
// ---------------------------------------------------------------------------
// DeleteQueryBuilder
// ---------------------------------------------------------------------------
export class DeleteQueryBuilder {
private _where: WhereCondition = {};
constructor(
private engine: IStorageEngine,
private tableName: string,
) {}
where(condition: WhereCondition): this {
this._where = { ...this._where, ...condition };
return this;
}
async execute(): Promise<number> {
return this.engine.delete(this.tableName, { table: this.tableName, where: this._where });
}
toAST(): DeleteStatement {
return { type: 'DELETE', from: this.tableName, where: this._where };
}
}
+57
View File
@@ -0,0 +1,57 @@
/**
* metona-sqlark Query Compiler — AST → 查询计划
* @module query/compiler
*
* 将 AST 语句编译为引擎可执行的 QueryPlan。
* v0.0.1: 简单直接映射,未来可加入索引选择、过滤下推等优化。
*/
import type { QueryPlan } from '../constants';
import { DatabaseError } from '../constants';
import type { Statement, SelectStatement, DeleteStatement, UpdateStatement } from './ast';
// ---------------------------------------------------------------------------
// 编译 AST → QueryPlan
// ---------------------------------------------------------------------------
/**
* 编译 SELECT / DELETE / UPDATE 语句为 QueryPlan。
* INSERT 和 DDL 语句不需要 QueryPlan。
*/
export function compileStatement(stmt: Statement): QueryPlan {
switch (stmt.type) {
case 'SELECT':
return compileSelect(stmt);
case 'DELETE':
return compileDelete(stmt);
case 'UPDATE':
return compileUpdate(stmt);
default:
throw new DatabaseError(`Cannot compile statement type "${stmt.type}" to QueryPlan`, 'COMPILE_ERROR');
}
}
function compileSelect(stmt: SelectStatement): QueryPlan {
return {
table: stmt.from,
columns: stmt.columns,
where: stmt.where,
orderBy: stmt.orderBy?.length ? stmt.orderBy : undefined,
limit: stmt.limit,
offset: stmt.offset,
};
}
function compileDelete(stmt: DeleteStatement): QueryPlan {
return {
table: stmt.from,
where: stmt.where,
};
}
function compileUpdate(stmt: UpdateStatement): QueryPlan {
return {
table: stmt.table,
where: stmt.where,
};
}
+229
View File
@@ -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; }
}
+9
View File
@@ -0,0 +1,9 @@
/**
* metona-sqlark Query — 查询系统
* @module query
*/
export type * from './ast';
export { SelectQueryBuilder, UpdateQueryBuilder, DeleteQueryBuilder } from './builder';
export { compileStatement } from './compiler';
export { QueryExecutor } from './executor';
+173
View File
@@ -0,0 +1,173 @@
/**
* metona-sqlark Shared WHERE Matcher — 统一的条件匹配逻辑
* @module query/where-matcher
*
* MemoryEngine / IndexedDBEngine / QueryExecutor 共享此模块,
* 消除 220+ 行重复代码,统一 $and/$or/$not/$col 行为。
*/
import type { WhereCondition, OrderBy } from '../constants';
// ---------------------------------------------------------------------------
// LIKE 正则缓存
// ---------------------------------------------------------------------------
const likeCache = new Map<string, RegExp>();
function compileLikeRegex(pattern: string): RegExp {
const cached = likeCache.get(pattern);
if (cached) return cached;
const escaped = pattern
.replace(/[.+^${}()|[\]\\]/g, '\\$&')
.replace(/%/g, '.*')
.replace(/_/g, '.');
const regex = new RegExp(`^${escaped}$`, 'i');
likeCache.set(pattern, regex);
return regex;
}
// ---------------------------------------------------------------------------
// WHERE 匹配(顶层入口)
// ---------------------------------------------------------------------------
/**
* 匹配完整 WHERE 条件
* @param row 当前数据行
* @param where WHERE 条件对象
* @param options.$col 是否启用 $col 列引用解析
*/
export function matchWhere(
row: Record<string, unknown>,
where: WhereCondition,
options: { $col?: boolean } = {},
): boolean {
for (const [field, condition] of Object.entries(where)) {
// 顶层 $and
if (field === '$and') {
const subs = condition as WhereCondition[];
if (!subs.every((sub) => matchWhere(row, sub, options))) return false;
continue;
}
// 顶层 $or
if (field === '$or') {
const subs = condition as WhereCondition[];
if (!subs.some((sub) => matchWhere(row, sub, options))) return false;
continue;
}
if (!matchField(row[field], condition, row, options)) return false;
}
return true;
}
// ---------------------------------------------------------------------------
// 字段匹配
// ---------------------------------------------------------------------------
function matchField(
value: unknown,
condition: unknown,
row: Record<string, unknown>,
options: { $col?: boolean },
): boolean {
// 嵌套 $and
if (typeof condition === 'object' && condition !== null && '$and' in (condition as Record<string, unknown>)) {
const subs = (condition as Record<string, unknown>).$and as WhereCondition[];
return subs.every((sub) => matchWhere(row, sub, options));
}
// 嵌套 $or
if (typeof condition === 'object' && condition !== null && '$or' in (condition as Record<string, unknown>)) {
const subs = (condition as Record<string, unknown>).$or as WhereCondition[];
return subs.some((sub) => matchWhere(row, sub, options));
}
// $not
if (typeof condition === 'object' && condition !== null && '$not' in (condition as Record<string, unknown>)) {
return !matchField(value, (condition as Record<string, unknown>).$not, row, options);
}
// 简单值 => $eq
if (typeof condition !== 'object' || condition === null || Array.isArray(condition)) {
return value === condition;
}
const ops = condition as Record<string, unknown>;
// $col 简写: { $col: name } === { $eq: { $col: name } }(仅 JOIN ON 场景)
if (options.$col && '$col' in ops && Object.keys(ops).length === 1) {
return value === row[ops.$col as string];
}
// 遍历操作符
for (const [op, operand] of Object.entries(ops)) {
let actualOperand = operand;
// $col 列引用解析
if (options.$col && typeof operand === 'object' && operand !== null && '$col' in (operand as Record<string, unknown>)) {
actualOperand = row[(operand as Record<string, unknown>).$col as string];
}
if (!matchOperator(value, op, actualOperand)) return false;
}
return true;
}
// ---------------------------------------------------------------------------
// 操作符匹配
// ---------------------------------------------------------------------------
function matchOperator(value: unknown, op: string, operand: unknown): boolean {
switch (op) {
case '$eq': return value === operand;
case '$ne': return value !== operand;
case '$gt': return (value as number) > (operand as number);
case '$gte': return (value as number) >= (operand as number);
case '$lt': return (value as number) < (operand as number);
case '$lte': return (value as number) <= (operand as number);
case '$in': return Array.isArray(operand) && operand.includes(value);
case '$nin': return Array.isArray(operand) && !operand.includes(value);
case '$like': return compileLikeRegex(String(operand)).test(String(value));
default: return true;
}
}
// ---------------------------------------------------------------------------
// 排序
// ---------------------------------------------------------------------------
export function applyOrderBy(rows: Record<string, unknown>[], orderBy: OrderBy[]): Record<string, unknown>[] {
return [...rows].sort((a, b) => {
for (const { column, direction } of orderBy) {
const cmp = compare(a[column], b[column]);
if (cmp !== 0) return direction === 'desc' ? -cmp : cmp;
}
return 0;
});
}
function compare(a: unknown, b: unknown): number {
if (a === b) return 0;
if (a === null || a === undefined) return 1;
if (b === null || b === undefined) return -1;
if (typeof a === 'string' && typeof b === 'string') return a.localeCompare(b);
if (typeof a === 'number' && typeof b === 'number') return a - b;
return String(a).localeCompare(String(b));
}
// ---------------------------------------------------------------------------
// 列投影
// ---------------------------------------------------------------------------
export function projectColumns(row: Record<string, unknown>, columns: string[]): Record<string, unknown> {
const projected: Record<string, unknown> = {};
for (const col of columns) {
if (col in row) {
projected[col] = row[col];
} else {
for (const key of Object.keys(row)) {
if (key.endsWith(`.${col}`) || key === col) {
projected[col] = row[key];
break;
}
}
}
}
return projected;
}
+9
View File
@@ -0,0 +1,9 @@
/**
* metona-sqlark SQL Parser — SQL 字符串解析
* @module sql
*/
export { Lexer, tokenize } from './lexer';
export { Parser, parse } from './parser';
export { TokenType } from './tokens';
export type { Token } from './tokens';
+215
View File
@@ -0,0 +1,215 @@
/**
* metona-sqlark SQL Lexer — 词法分析器
* @module sql/lexer
*
* 将 SQL 字符串切分为 Token 流。
*/
import { TokenType, type Token, KEYWORDS } from './tokens';
// ---------------------------------------------------------------------------
// Lexer
// ---------------------------------------------------------------------------
export class Lexer {
private input: string;
private position = 0;
private readPosition = 0;
private ch: string = '';
constructor(input: string) {
this.input = input;
this.readChar();
}
/** 读取下一个 Token */
nextToken(): Token {
this.skipWhitespace();
let tok: Token;
switch (this.ch) {
case ',':
tok = this.makeToken(TokenType.COMMA, ',');
break;
case '(':
tok = this.makeToken(TokenType.LPAREN, '(');
break;
case ')':
tok = this.makeToken(TokenType.RPAREN, ')');
break;
case ';':
tok = this.makeToken(TokenType.SEMICOLON, ';');
break;
case '*':
tok = this.makeToken(TokenType.STAR, '*');
break;
case '.':
tok = this.makeToken(TokenType.DOT, '.');
break;
case '=':
tok = this.makeToken(TokenType.EQ, '=');
break;
case '!':
if (this.peekChar() === '=') {
this.readChar();
tok = this.makeToken(TokenType.NEQ, '!=');
} else {
tok = this.makeToken(TokenType.ILLEGAL, '!');
}
break;
case '>':
if (this.peekChar() === '=') {
this.readChar();
tok = this.makeToken(TokenType.GTE, '>=');
} else {
tok = this.makeToken(TokenType.GT, '>');
}
break;
case '<':
if (this.peekChar() === '=') {
this.readChar();
tok = this.makeToken(TokenType.LTE, '<=');
} else if (this.peekChar() === '>') {
this.readChar();
tok = this.makeToken(TokenType.NEQ, '<>');
} else {
tok = this.makeToken(TokenType.LT, '<');
}
break;
case "'":
case '"':
tok = this.readString(this.ch);
break;
case '':
tok = { type: TokenType.EOF, value: '', position: this.position };
break;
default:
if (this.isLetter(this.ch)) {
const ident = this.readIdentifier();
const keyword = KEYWORDS[ident.toUpperCase()];
tok = {
type: keyword ?? TokenType.IDENTIFIER,
value: ident,
position: this.position - ident.length,
};
return tok; // 已读取完毕,不需要再 readChar
} else if (this.isDigit(this.ch) || (this.ch === '-' && this.isDigit(this.peekChar()))) {
const num = this.readNumber();
tok = {
type: TokenType.NUMBER,
value: num,
position: this.position - num.length,
};
return tok;
} else {
tok = this.makeToken(TokenType.ILLEGAL, this.ch);
}
break;
}
this.readChar();
return tok;
}
// ---- 内部 ----
private readChar(): void {
if (this.readPosition >= this.input.length) {
this.ch = '';
} else {
this.ch = this.input[this.readPosition];
}
this.position = this.readPosition;
this.readPosition++;
}
private peekChar(): string {
if (this.readPosition >= this.input.length) return '';
return this.input[this.readPosition];
}
private skipWhitespace(): void {
while (this.ch === ' ' || this.ch === '\t' || this.ch === '\n' || this.ch === '\r') {
this.readChar();
}
}
private readIdentifier(): string {
const start = this.position;
while (this.isLetter(this.ch) || this.isDigit(this.ch) || this.ch === '_') {
this.readChar();
}
return this.input.slice(start, this.position);
}
private readNumber(): string {
const start = this.position;
// 负号
if (this.ch === '-') this.readChar();
while (this.isDigit(this.ch)) {
this.readChar();
}
// 小数点
if (this.ch === '.' && this.isDigit(this.peekChar())) {
this.readChar();
while (this.isDigit(this.ch)) {
this.readChar();
}
}
return this.input.slice(start, this.position);
}
private readString(quote: string): Token {
const start = this.position + 1; // 跳过一个引号
this.readChar(); // 跳过开始引号
let value = '';
while (this.ch !== quote && this.ch !== '') {
// 处理转义
if (this.ch === '\\' && this.peekChar() === quote) {
this.readChar();
value += quote;
} else {
value += this.ch;
}
this.readChar();
}
// 跳过结束引号(在 readChar 之后才会调用,所以这里不需要处理)
return {
type: TokenType.STRING,
value,
position: start,
};
}
private isLetter(ch: string): boolean {
return /[a-zA-Z_]/.test(ch);
}
private isDigit(ch: string): boolean {
return /[0-9]/.test(ch);
}
private makeToken(type: TokenType, value: string): Token {
return { type, value, position: this.position };
}
}
// ---------------------------------------------------------------------------
// 便捷方法:一次性词法分析
// ---------------------------------------------------------------------------
/** 将 SQL 字符串解析为 Token 列表 */
export function tokenize(sql: string): Token[] {
const lexer = new Lexer(sql);
const tokens: Token[] = [];
let tok = lexer.nextToken();
while (tok.type !== TokenType.EOF) {
tokens.push(tok);
tok = lexer.nextToken();
}
tokens.push(tok); // EOF
return tokens;
}
+717
View File
@@ -0,0 +1,717 @@
/**
* metona-sqlark SQL Parser — 递归下降语法分析器
* @module sql/parser
*
* Token 流 → AST Statement。
* 支持的语法是标准 SQL 的子集。
*/
import { Lexer } from './lexer';
import { TokenType, type Token } from './tokens';
import type {
Statement,
SelectStatement,
InsertStatement,
UpdateStatement,
DeleteStatement,
CreateTableStatement,
DropTableStatement,
ASTColumnDef,
} from '../query/ast';
import type { WhereCondition, OrderBy, SortDirection } from '../constants';
import { DatabaseError } from '../constants';
// ---------------------------------------------------------------------------
// Parser
// ---------------------------------------------------------------------------
export class Parser {
private lexer: Lexer;
private curToken!: Token;
private peekToken!: Token;
constructor(sql: string) {
this.lexer = new Lexer(sql);
// 预读两个 token
this.nextToken();
this.nextToken();
}
/** 解析完整 SQL 语句 */
parseStatement(): Statement {
switch (this.curToken.type) {
case TokenType.SELECT:
return this.parseSelect();
case TokenType.INSERT:
return this.parseInsert();
case TokenType.UPDATE:
return this.parseUpdate();
case TokenType.DELETE:
return this.parseDelete();
case TokenType.CREATE:
return this.parseCreateTable();
case TokenType.DROP:
return this.parseDropTable();
default:
throw this.error(`Unexpected token "${this.curToken.value}"`);
}
}
// ===================================================================
// SELECT
// ===================================================================
private parseSelect(): SelectStatement {
this.expect(TokenType.SELECT);
// DISTINCT(可选)
let distinct = false;
if (this.curTokenIs(TokenType.DISTINCT)) {
distinct = true;
this.nextToken();
}
// 列
const columns: string[] = [];
if (this.curTokenIs(TokenType.STAR)) {
columns.push('*');
this.nextToken();
} else {
columns.push(...this.parseColumnList());
}
// FROM
this.expect(TokenType.FROM);
const tableName = this.expectIdentifier('table name');
// 表别名(可选)
let alias: string | undefined;
if (this.curTokenIs(TokenType.AS)) {
this.nextToken();
alias = this.expectIdentifier('alias');
} else if (this.curToken.type === TokenType.IDENTIFIER && !this._isReservedAfterFrom()) {
alias = this.curToken.value;
this.nextToken();
}
const stmt: SelectStatement = {
type: 'SELECT',
columns,
distinct: distinct || undefined,
from: tableName,
alias,
where: {},
};
// JOIN 子句(可选,支持多个)
const joins = this.parseJoinClauses();
if (joins.length > 0) {
stmt.joins = joins;
}
// WHERE(可选)
if (this.curTokenIs(TokenType.WHERE)) {
this.nextToken();
stmt.where = this.parseCondition();
}
// GROUP BY(可选)
if (this.curTokenIs(TokenType.GROUP)) {
this.nextToken();
this.expect(TokenType.BY);
stmt.groupBy = this.parseIdentifierList();
}
// HAVING(可选)
if (this.curTokenIs(TokenType.HAVING)) {
this.nextToken();
stmt.having = this.parseCondition();
}
// ORDER BY(可选)
if (this.curTokenIs(TokenType.ORDER)) {
this.nextToken();
this.expect(TokenType.BY);
stmt.orderBy = this.parseOrderByList();
}
// LIMIT(可选)
if (this.curTokenIs(TokenType.LIMIT)) {
this.nextToken();
stmt.limit = this.expectNumber('LIMIT value');
}
// OFFSET(可选)
if (this.curTokenIs(TokenType.OFFSET)) {
this.nextToken();
stmt.offset = this.expectNumber('OFFSET value');
}
return stmt;
}
/** 解析 JOIN 子句列表 */
private parseJoinClauses(): import('../query/ast').JoinClause[] {
const joins: import('../query/ast').JoinClause[] = [];
while (this._isJoinKeyword()) {
joins.push(this.parseJoinClause());
}
return joins;
}
private _isJoinKeyword(): boolean {
return (
this.curTokenIs(TokenType.INNER) ||
this.curTokenIs(TokenType.LEFT) ||
this.curTokenIs(TokenType.RIGHT) ||
this.curTokenIs(TokenType.CROSS) ||
this.curTokenIs(TokenType.JOIN)
);
}
/** 解析单个 JOIN 子句 */
private parseJoinClause(): import('../query/ast').JoinClause {
let type: import('../query/ast').JoinType = 'INNER';
if (this.curTokenIs(TokenType.INNER)) {
type = 'INNER';
this.nextToken();
} else if (this.curTokenIs(TokenType.LEFT)) {
type = 'LEFT';
this.nextToken();
if (this.curTokenIs(TokenType.OUTER)) this.nextToken(); // 可选 OUTER
} else if (this.curTokenIs(TokenType.RIGHT)) {
type = 'RIGHT';
this.nextToken();
if (this.curTokenIs(TokenType.OUTER)) this.nextToken();
} else if (this.curTokenIs(TokenType.CROSS)) {
type = 'CROSS';
this.nextToken();
}
this.expect(TokenType.JOIN);
const tableName = this.expectIdentifier('table name');
// JOIN 表别名(可选)
let alias: string | undefined;
if (this.curTokenIs(TokenType.AS)) {
this.nextToken();
alias = this.expectIdentifier('alias');
} else if (this.curToken.type === TokenType.IDENTIFIER && !this._isJoinReserved()) {
alias = this.curToken.value;
this.nextToken();
}
// ON 条件(CROSS JOIN 不需要 ON
let on = {};
if (type !== 'CROSS' && this.curTokenIs(TokenType.ON)) {
this.nextToken();
on = this.parseCondition();
}
return { type, table: tableName, alias, on };
}
/** 判断当前 token 是否为 FROM 之后的保留字 */
private _isReservedAfterFrom(): boolean {
return (
this.curTokenIs(TokenType.WHERE) ||
this.curTokenIs(TokenType.ORDER) ||
this.curTokenIs(TokenType.LIMIT) ||
this.curTokenIs(TokenType.OFFSET) ||
this.curTokenIs(TokenType.GROUP) ||
this._isJoinKeyword()
);
}
private _isJoinReserved(): boolean {
return (
this.curTokenIs(TokenType.ON) ||
this.curTokenIs(TokenType.WHERE) ||
this.curTokenIs(TokenType.ORDER) ||
this.curTokenIs(TokenType.LIMIT) ||
this._isJoinKeyword()
);
}
// ===================================================================
// INSERT
// ===================================================================
private parseInsert(): InsertStatement {
this.expect(TokenType.INSERT);
this.expect(TokenType.INTO);
const tableName = this.expectIdentifier('table name');
// 列名(可选)
let columns: string[] | undefined;
if (this.curTokenIs(TokenType.LPAREN)) {
this.nextToken();
columns = this.parseIdentifierList();
this.expect(TokenType.RPAREN);
}
// VALUES
this.expect(TokenType.VALUES);
// 值列表
const values: unknown[][] = [];
do {
if (this.curTokenIs(TokenType.COMMA)) {
this.nextToken();
}
this.expect(TokenType.LPAREN);
const rowValues = this.parseValueList();
this.expect(TokenType.RPAREN);
values.push(rowValues);
} while (this.curTokenIs(TokenType.COMMA));
return {
type: 'INSERT',
into: tableName,
columns,
values,
};
}
// ===================================================================
// UPDATE
// ===================================================================
private parseUpdate(): UpdateStatement {
this.expect(TokenType.UPDATE);
const tableName = this.expectIdentifier('table name');
this.expect(TokenType.SET);
// SET col=val, ...
const sets: Record<string, unknown> = {};
do {
if (this.curTokenIs(TokenType.COMMA)) this.nextToken();
const col = this.expectIdentifier('column name');
this.expect(TokenType.EQ);
sets[col] = this.parseValue();
} while (this.curTokenIs(TokenType.COMMA));
let where: WhereCondition = {};
if (this.curTokenIs(TokenType.WHERE)) {
this.nextToken();
where = this.parseCondition();
}
return { type: 'UPDATE', table: tableName, sets, where };
}
// ===================================================================
// DELETE
// ===================================================================
private parseDelete(): DeleteStatement {
this.expect(TokenType.DELETE);
this.expect(TokenType.FROM);
const tableName = this.expectIdentifier('table name');
let where: WhereCondition = {};
if (this.curTokenIs(TokenType.WHERE)) {
this.nextToken();
where = this.parseCondition();
}
return { type: 'DELETE', from: tableName, where };
}
// ===================================================================
// CREATE TABLE
// ===================================================================
private parseCreateTable(): CreateTableStatement {
this.expect(TokenType.CREATE);
this.expect(TokenType.TABLE);
const tableName = this.expectIdentifier('table name');
this.expect(TokenType.LPAREN);
const columns: ASTColumnDef[] = [];
do {
if (this.curTokenIs(TokenType.COMMA)) this.nextToken();
columns.push(this.parseColumnDef());
} while (this.curTokenIs(TokenType.COMMA));
this.expect(TokenType.RPAREN);
return { type: 'CREATE_TABLE', name: tableName, columns };
}
private parseColumnDef(): ASTColumnDef {
const name = this.expectIdentifier('column name');
const type = this.expectIdentifier('column type').toLowerCase();
const col: ASTColumnDef = { name, type };
// 修饰符
while (
this.curTokenIs(TokenType.PRIMARY) ||
this.curTokenIs(TokenType.UNIQUE) ||
this.curTokenIs(TokenType.NOT) ||
this.curTokenIs(TokenType.DEFAULT)
) {
if (this.curTokenIs(TokenType.PRIMARY)) {
this.nextToken();
this.expect(TokenType.KEY);
col.primaryKey = true;
} else if (this.curTokenIs(TokenType.UNIQUE)) {
this.nextToken();
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 {
break;
}
}
return col;
}
// ===================================================================
// DROP TABLE
// ===================================================================
private parseDropTable(): DropTableStatement {
this.expect(TokenType.DROP);
this.expect(TokenType.TABLE);
const tableName = this.expectIdentifier('table name');
return { type: 'DROP_TABLE', name: tableName };
}
// ===================================================================
// 条件表达式
// ===================================================================
/** condition → simple_cond ((AND|OR) simple_cond)* */
private parseCondition(): WhereCondition {
let left = this.parseSimpleCondition();
while (this.curTokenIs(TokenType.AND) || this.curTokenIs(TokenType.OR)) {
const isAnd = this.curTokenIs(TokenType.AND);
this.nextToken();
const right = this.parseSimpleCondition();
if (isAnd) {
// 合并到 $and
left = { $and: [left, right] } as unknown as WhereCondition;
} else {
left = { $or: [left, right] } as unknown as WhereCondition;
}
}
return left;
}
/** simple_cond → column op value | column IS [NOT] NULL | column [NOT] LIKE pattern
* | column [NOT] IN (values) | NOT condition | (condition) */
private parseSimpleCondition(): WhereCondition {
// NOT expr
if (this.curTokenIs(TokenType.NOT)) {
this.nextToken();
const inner = this.parseSimpleCondition();
return { $not: inner } as unknown as WhereCondition;
}
// (condition)
if (this.curTokenIs(TokenType.LPAREN)) {
this.nextToken();
const inner = this.parseCondition();
this.expect(TokenType.RPAREN);
return inner;
}
// column
const column = this.parseColumnRef();
// IS NULL / IS NOT NULL
if (this.curTokenIs(TokenType.IDENTIFIER) && this.curToken.value.toUpperCase() === 'IS') {
this.nextToken();
const isNot = this.curTokenIs(TokenType.NOT);
if (isNot) this.nextToken();
this.expect(TokenType.NULL);
const result: WhereCondition = {};
result[column] = isNot ? { $ne: null } : { $eq: null };
return result;
}
// LIKE
if (this.curTokenIs(TokenType.LIKE)) {
this.nextToken();
const pattern = this.parseValue();
const result: WhereCondition = {};
result[column] = { $like: pattern };
return result;
}
// IN
if (this.curTokenIs(TokenType.IN)) {
this.nextToken();
this.expect(TokenType.LPAREN);
const values = this.parseValueList();
this.expect(TokenType.RPAREN);
const result: WhereCondition = {};
result[column] = { $in: values };
return result;
}
// 比较运算符
const op = this.parseComparisonOp();
// 尝试解析列引用(identifier DOT identifier 格式)
let value: unknown;
if (
(this.curToken.type === TokenType.IDENTIFIER || this._isKeywordAsIdent()) &&
this.peekTokenIs(TokenType.DOT)
) {
const colRef = this.parseColumnRef();
value = { $col: colRef };
} else {
value = this.parseValue();
}
const result: WhereCondition = {};
result[column] = { [op]: value };
return result;
}
private peekTokenIs(type: TokenType): boolean {
return this.peekToken.type === type;
}
private parseComparisonOp(): string {
switch (this.curToken.type) {
case TokenType.EQ: this.nextToken(); return '$eq';
case TokenType.NEQ: this.nextToken(); return '$ne';
case TokenType.GT: this.nextToken(); return '$gt';
case TokenType.GTE: this.nextToken(); return '$gte';
case TokenType.LT: this.nextToken(); return '$lt';
case TokenType.LTE: this.nextToken(); return '$lte';
default:
throw this.error(`Expected comparison operator, got "${this.curToken.value}"`);
}
}
// ===================================================================
// 辅助解析
// ===================================================================
private parseColumnList(): string[] {
const cols: string[] = [];
cols.push(this.parseColumnRef());
while (this.curTokenIs(TokenType.COMMA)) {
this.nextToken();
cols.push(this.parseColumnRef());
}
return cols;
}
/** 解析列引用:支持 'col'、'table.col' 和 'COUNT(*)'/'SUM(col)' 等 */
private parseColumnRef(): string {
// 聚合函数?
if (
this.curTokenIs(TokenType.COUNT) ||
this.curTokenIs(TokenType.SUM) ||
this.curTokenIs(TokenType.AVG) ||
this.curTokenIs(TokenType.MIN) ||
this.curTokenIs(TokenType.MAX)
) {
return this.parseAggregateCall();
}
const first = this.expectIdentifier('column name');
if (this.curTokenIs(TokenType.DOT)) {
this.nextToken();
const second = this.expectIdentifier('column name');
return `${first}.${second}`;
}
return first;
}
/** 解析聚合函数调用: COUNT(*), SUM(col), AVG(col), MIN(col), MAX(col) */
private parseAggregateCall(): string {
const func = this.curToken.value.toUpperCase();
this.nextToken();
this.expect(TokenType.LPAREN);
let arg: string;
if (this.curTokenIs(TokenType.STAR)) {
arg = '*';
this.nextToken();
} else {
arg = this.parseColumnRef();
}
this.expect(TokenType.RPAREN);
// 可选别名: AS alias
let alias = '';
if (this.curTokenIs(TokenType.AS)) {
this.nextToken();
alias = this.expectIdentifier('alias');
} else if (this.curToken.type === TokenType.IDENTIFIER && this._isAggregateAlias()) {
alias = this.curToken.value;
this.nextToken();
}
if (alias) {
return `${func}(${arg}) AS ${alias}`;
}
return `${func}(${arg})`;
}
private _isAggregateAlias(): boolean {
return !this._isReservedAfterFrom() && !this._isJoinKeyword();
}
private parseIdentifierList(): string[] {
const ids: string[] = [];
ids.push(this.expectIdentifier('identifier'));
while (this.curTokenIs(TokenType.COMMA)) {
this.nextToken();
ids.push(this.expectIdentifier('identifier'));
}
return ids;
}
private parseValueList(): unknown[] {
const vals: unknown[] = [];
vals.push(this.parseValue());
while (this.curTokenIs(TokenType.COMMA)) {
this.nextToken();
vals.push(this.parseValue());
}
return vals;
}
private parseOrderByList(): OrderBy[] {
const list: OrderBy[] = [];
list.push(this.parseOrderBy());
while (this.curTokenIs(TokenType.COMMA)) {
this.nextToken();
list.push(this.parseOrderBy());
}
return list;
}
private parseOrderBy(): OrderBy {
const column = this.expectIdentifier('column name');
let direction: SortDirection = 'asc';
if (this.curTokenIs(TokenType.ASC)) {
this.nextToken();
} else if (this.curTokenIs(TokenType.DESC)) {
direction = 'desc';
this.nextToken();
}
return { column, direction };
}
/** 解析字面量值 */
private parseValue(): unknown {
switch (this.curToken.type) {
case TokenType.STRING: {
const val = this.curToken.value;
this.nextToken();
return val;
}
case TokenType.NUMBER: {
const val = Number(this.curToken.value);
this.nextToken();
return val;
}
case TokenType.TRUE: this.nextToken(); return true;
case TokenType.FALSE: this.nextToken(); return false;
case TokenType.NULL: this.nextToken(); return null;
default:
throw this.error(`Expected value, got "${this.curToken.value}"`);
}
}
// ===================================================================
// Token 操作
// ===================================================================
private nextToken(): void {
this.curToken = this.peekToken;
this.peekToken = this.lexer.nextToken();
}
private curTokenIs(type: TokenType): boolean {
return this.curToken.type === type;
}
private expect(type: TokenType): void {
if (this.curTokenIs(type)) {
this.nextToken();
return;
}
throw this.error(`Expected ${type}, got "${this.curToken.value}"`);
}
private expectIdentifier(context: string): string {
if (this.curToken.type === TokenType.IDENTIFIER || this._isKeywordAsIdent()) {
const val = this.curToken.value;
this.nextToken();
return val;
}
throw this.error(`Expected ${context}, got "${this.curToken.value}"`);
}
/** 关键字可以作为标识符(如列名等于关键字) */
private _isKeywordAsIdent(): boolean {
return (
this.curToken.type !== TokenType.EOF &&
this.curToken.type !== TokenType.ILLEGAL &&
this.curToken.type !== TokenType.STRING &&
this.curToken.type !== TokenType.NUMBER &&
this.curToken.type !== TokenType.COMMA &&
this.curToken.type !== TokenType.LPAREN &&
this.curToken.type !== TokenType.RPAREN &&
this.curToken.type !== TokenType.SEMICOLON &&
this.curToken.type !== TokenType.EQ &&
this.curToken.type !== TokenType.NEQ &&
this.curToken.type !== TokenType.GT &&
this.curToken.type !== TokenType.GTE &&
this.curToken.type !== TokenType.LT &&
this.curToken.type !== TokenType.LTE &&
this.curToken.type !== TokenType.DOT &&
this.curToken.type !== TokenType.STAR
);
}
private expectNumber(context: string): number {
if (this.curToken.type === TokenType.NUMBER) {
const val = Number(this.curToken.value);
this.nextToken();
return val;
}
throw this.error(`Expected ${context}, got "${this.curToken.value}"`);
}
private error(msg: string): DatabaseError {
return new DatabaseError(
`Parse error at position ${this.curToken.position}: ${msg}`,
'PARSE_ERROR',
);
}
}
// ---------------------------------------------------------------------------
// 便捷方法
// ---------------------------------------------------------------------------
/** 解析 SQL 字符串为 AST Statement */
export function parse(sql: string): Statement {
const parser = new Parser(sql);
const stmt = parser.parseStatement();
return stmt;
}
+152
View File
@@ -0,0 +1,152 @@
/**
* metona-sqlark SQL Token Types — 词法单元定义
* @module sql/tokens
*/
// ---------------------------------------------------------------------------
// Token 类型枚举
// ---------------------------------------------------------------------------
export enum TokenType {
// 关键字
SELECT = 'SELECT',
FROM = 'FROM',
WHERE = 'WHERE',
INSERT = 'INSERT',
INTO = 'INTO',
VALUES = 'VALUES',
UPDATE = 'UPDATE',
SET = 'SET',
DELETE = 'DELETE',
CREATE = 'CREATE',
TABLE = 'TABLE',
DROP = 'DROP',
ORDER = 'ORDER',
BY = 'BY',
ASC = 'ASC',
DESC = 'DESC',
LIMIT = 'LIMIT',
OFFSET = 'OFFSET',
AND = 'AND',
OR = 'OR',
NOT = 'NOT',
LIKE = 'LIKE',
IN = 'IN',
PRIMARY = 'PRIMARY',
KEY = 'KEY',
UNIQUE = 'UNIQUE',
DEFAULT = 'DEFAULT',
NULL = 'NULL',
TRUE = 'TRUE',
FALSE = 'FALSE',
// JOIN 相关
INNER = 'INNER',
LEFT = 'LEFT',
RIGHT = 'RIGHT',
CROSS = 'CROSS',
JOIN = 'JOIN',
ON = 'ON',
AS = 'AS',
OUTER = 'OUTER',
// 聚合
GROUP = 'GROUP',
HAVING = 'HAVING',
COUNT = 'COUNT',
SUM = 'SUM',
AVG = 'AVG',
MIN = 'MIN',
MAX = 'MAX',
DISTINCT = 'DISTINCT',
// 标识符 & 字面量
IDENTIFIER = 'IDENTIFIER',
STRING = 'STRING',
NUMBER = 'NUMBER',
// 运算符 & 分隔符
COMMA = 'COMMA',
LPAREN = 'LPAREN',
RPAREN = 'RPAREN',
SEMICOLON = 'SEMICOLON',
EQ = 'EQ',
NEQ = 'NEQ',
GT = 'GT',
GTE = 'GTE',
LT = 'LT',
LTE = 'LTE',
STAR = 'STAR',
DOT = 'DOT',
// 特殊
EOF = 'EOF',
ILLEGAL = 'ILLEGAL',
}
// ---------------------------------------------------------------------------
// Token
// ---------------------------------------------------------------------------
export interface Token {
type: TokenType;
value: string;
position: number; // 在源码中的位置
}
// ---------------------------------------------------------------------------
// 关键字映射
// ---------------------------------------------------------------------------
export const KEYWORDS: Record<string, TokenType> = {
'SELECT': TokenType.SELECT,
'FROM': TokenType.FROM,
'WHERE': TokenType.WHERE,
'INSERT': TokenType.INSERT,
'INTO': TokenType.INTO,
'VALUES': TokenType.VALUES,
'UPDATE': TokenType.UPDATE,
'SET': TokenType.SET,
'DELETE': TokenType.DELETE,
'CREATE': TokenType.CREATE,
'TABLE': TokenType.TABLE,
'DROP': TokenType.DROP,
'ORDER': TokenType.ORDER,
'BY': TokenType.BY,
'ASC': TokenType.ASC,
'DESC': TokenType.DESC,
'LIMIT': TokenType.LIMIT,
'OFFSET': TokenType.OFFSET,
'AND': TokenType.AND,
'OR': TokenType.OR,
'NOT': TokenType.NOT,
'LIKE': TokenType.LIKE,
'IN': TokenType.IN,
'PRIMARY': TokenType.PRIMARY,
'KEY': TokenType.KEY,
'UNIQUE': TokenType.UNIQUE,
'DEFAULT': TokenType.DEFAULT,
'NULL': TokenType.NULL,
'TRUE': TokenType.TRUE,
'FALSE': TokenType.FALSE,
// JOIN
'INNER': TokenType.INNER,
'LEFT': TokenType.LEFT,
'RIGHT': TokenType.RIGHT,
'CROSS': TokenType.CROSS,
'JOIN': TokenType.JOIN,
'ON': TokenType.ON,
'AS': TokenType.AS,
'OUTER': TokenType.OUTER,
// 聚合
'GROUP': TokenType.GROUP,
'HAVING': TokenType.HAVING,
'COUNT': TokenType.COUNT,
'SUM': TokenType.SUM,
'AVG': TokenType.AVG,
'MIN': TokenType.MIN,
'MAX': TokenType.MAX,
'DISTINCT': TokenType.DISTINCT,
};
+7
View File
@@ -0,0 +1,7 @@
/**
* metona-sqlark Table — 表管理
* @module table
*/
export { createSchema, validateColumns, validateRow, getPrimaryKey } from './schema';
export { Table } from './table';
+172
View File
@@ -0,0 +1,172 @@
/**
* metona-sqlark Schema — 表结构定义与校验
* @module table/schema
*/
import type { TableSchema, ColumnDef, FieldType } from '../constants';
import { FIELD_TYPES, DatabaseError } from '../constants';
// ---------------------------------------------------------------------------
// Schema 工具
// ---------------------------------------------------------------------------
/** 从列定义创建 TableSchema */
export function createSchema(name: string, columns: Record<string, ColumnDef>): TableSchema {
validateColumns(columns);
return { name, columns };
}
/** 校验列定义 */
export function validateColumns(columns: Record<string, ColumnDef>): void {
const colNames = Object.keys(columns);
if (colNames.length === 0) {
throw new DatabaseError('Table must have at least one column', 'SCHEMA_ERROR');
}
let primaryKeyCount = 0;
for (const [colName, colDef] of Object.entries(columns)) {
// 类型校验
if (!FIELD_TYPES.includes(colDef.type)) {
throw new DatabaseError(
`Invalid type "${colDef.type}" for column "${colName}". Valid types: ${FIELD_TYPES.join(', ')}`,
'SCHEMA_ERROR',
);
}
// 主键计数
if (colDef.primaryKey) {
primaryKeyCount++;
}
}
// 至少需要一个主键
if (primaryKeyCount === 0) {
throw new DatabaseError('Table must have at least one primary key column', 'SCHEMA_ERROR');
}
}
/** 获取主键列名 */
export function getPrimaryKey(schema: TableSchema): string {
for (const [name, col] of Object.entries(schema.columns)) {
if (col.primaryKey) return name;
}
return Object.keys(schema.columns)[0];
}
/** 校验行数据 */
export function validateRow(schema: TableSchema, row: Record<string, unknown>): Record<string, unknown> {
const validated: Record<string, unknown> = {};
for (const [colName, colDef] of Object.entries(schema.columns)) {
let value = row[colName];
// 默认值
if (value === undefined && colDef.default !== undefined) {
value = colDef.default;
}
// 必填检查
if (colDef.required && (value === undefined || value === null)) {
throw new DatabaseError(
`Column "${colName}" is required in table "${schema.name}"`,
'VALIDATION_ERROR',
);
}
// 类型检查
if (value !== undefined && value !== null) {
checkFieldType(schema.name, colName, colDef.type, value, colDef);
}
if (value !== undefined) {
validated[colName] = value;
}
}
return validated;
}
/** 检查字段类型 */
export function checkFieldType(
tableName: string,
colName: string,
type: FieldType,
value: unknown,
_colDef?: ColumnDef,
): void {
const jsType = typeof value;
switch (type) {
case 'string':
if (jsType !== 'string') {
throw new DatabaseError(
`Column "${colName}" in table "${tableName}" expects string, got ${jsType}`,
'TYPE_ERROR',
);
}
break;
case 'number':
if (jsType !== 'number') {
throw new DatabaseError(
`Column "${colName}" in table "${tableName}" expects number, got ${jsType}`,
'TYPE_ERROR',
);
}
break;
case 'boolean':
if (jsType !== 'boolean') {
throw new DatabaseError(
`Column "${colName}" in table "${tableName}" expects boolean, got ${jsType}`,
'TYPE_ERROR',
);
}
break;
case 'date':
if (jsType !== 'string' || isNaN(Date.parse(value as string))) {
throw new DatabaseError(
`Column "${colName}" in table "${tableName}" expects valid date string, got ${typeof value}`,
'TYPE_ERROR',
);
}
break;
case 'json':
if (jsType !== 'object') {
throw new DatabaseError(
`Column "${colName}" in table "${tableName}" expects object/array, got ${jsType}`,
'TYPE_ERROR',
);
}
break;
}
}
/** 将 AST 列定义转换为 ColumnDef */
export function astColumnToColumnDef(astCol: {
name: string;
type: string;
primaryKey?: boolean;
unique?: boolean;
required?: boolean;
default?: unknown;
index?: boolean;
maxLength?: number;
min?: number;
max?: number;
}): ColumnDef {
return {
type: astCol.type as FieldType,
primaryKey: astCol.primaryKey,
unique: astCol.unique,
required: astCol.required,
default: astCol.default,
index: astCol.index,
maxLength: astCol.maxLength,
min: astCol.min,
max: astCol.max,
};
}
+83
View File
@@ -0,0 +1,83 @@
/**
* metona-sqlark Table — 表操作 API
* @module table/table
*/
import type { IStorageEngine } from '../engine/interface';
import type { TableSchema } from '../constants';
import { DatabaseError } from '../constants';
import { SelectQueryBuilder, UpdateQueryBuilder, DeleteQueryBuilder } from '../query/builder';
import { QueryExecutor } from '../query/executor';
// ---------------------------------------------------------------------------
// Table
// ---------------------------------------------------------------------------
export class Table<T = Record<string, unknown>> {
readonly name: string;
private engine: IStorageEngine;
private schema: TableSchema | null = null;
private executor: QueryExecutor | undefined;
constructor(engine: IStorageEngine, tableName: string, executor?: QueryExecutor) {
this.engine = engine;
this.name = tableName;
this.executor = executor;
}
// ---- Schema ----
async getSchema(): Promise<TableSchema> {
if (!this.schema) {
const s = await this.engine.getTableSchema(this.name);
if (!s) throw new DatabaseError(`Table "${this.name}" does not exist`, 'TABLE_NOT_FOUND');
this.schema = s;
}
return this.schema;
}
// ---- 插入 ----
async insert(row: T & Record<string, unknown>): Promise<string> {
const pks = await this.engine.insert(this.name, [row as Record<string, unknown>]);
return pks[0];
}
async insertMany(rows: (T & Record<string, unknown>)[]): Promise<string[]> {
return this.engine.insert(this.name, rows as Record<string, unknown>[]);
}
// ---- 查询 ----
select(columns: string[] = ['*']): SelectQueryBuilder {
return new SelectQueryBuilder(this.engine, this.name, columns, this.executor);
}
// ---- 更新 ----
update(updates: Partial<T> & Record<string, unknown>): UpdateQueryBuilder {
return new UpdateQueryBuilder(this.engine, this.name, updates);
}
// ---- 删除 ----
delete(): DeleteQueryBuilder {
return new DeleteQueryBuilder(this.engine, this.name);
}
// ---- 聚合 ----
async count(where?: Record<string, unknown>): Promise<number> {
return this.engine.count(this.name, where ? { table: this.name, where } : undefined);
}
// ---- 管理 ----
async clear(): Promise<void> {
return this.engine.clear(this.name);
}
async drop(): Promise<void> {
return this.engine.dropTable(this.name);
}
}
+67
View File
@@ -0,0 +1,67 @@
/**
* metona-sqlark Transaction — 事务管理
* @module transaction
*
* 封装多操作事务,保证原子性。
* v0.0.1: 基于引擎层操作实现,IndexedDB 引擎利用原生事务。
*/
import type { IStorageEngine } from '../engine/interface';
import { Table } from '../table/table';
import { DatabaseError } from '../constants';
// ---------------------------------------------------------------------------
// Transaction
// ---------------------------------------------------------------------------
export class Transaction {
private engine: IStorageEngine;
private tables: Map<string, Table> = new Map();
private completed = false;
constructor(engine: IStorageEngine) {
this.engine = engine;
}
/** 获取表操作对象 */
table(tableName: string): Table {
let t = this.tables.get(tableName);
if (!t) {
t = new Table(this.engine, tableName);
this.tables.set(tableName, t);
}
return t;
}
/** 标记事务完成(由 TransactionManager 调用) */
_markCompleted(): void {
this.completed = true;
}
/** 是否已完成 */
isCompleted(): boolean {
return this.completed;
}
}
// ---------------------------------------------------------------------------
// TransactionManager
// ---------------------------------------------------------------------------
export class TransactionManager {
constructor(private engine: IStorageEngine) {}
/** 执行事务 */
async execute<T>(fn: (trx: Transaction) => Promise<T>): Promise<T> {
const trx = new Transaction(this.engine);
try {
const result = await fn(trx);
trx._markCompleted();
return result;
} catch (error) {
if (error instanceof DatabaseError) throw error;
throw new DatabaseError(`Transaction failed: ${(error as Error).message}`, 'TRANSACTION_ERROR', error);
}
}
}
+80
View File
@@ -0,0 +1,80 @@
/**
* YOUR-PROJECT Utils — 工具函数
* @module utils
*/
/** 生成唯一 ID */
export const generateId = (): string => {
return 'id-' + Date.now().toString(36) + '-' + Math.random().toString(36).slice(2, 8);
};
/** HTML 转义 */
export const escapeHTML = (s: unknown): string => {
const str = String(s == null ? '' : s);
if (typeof document === 'undefined') {
return str
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;');
}
const div = document.createElement('div');
div.textContent = str;
return div.innerHTML;
};
/** 防抖 */
export function debounce<T extends (...args: any[]) => void>(
func: T,
wait: number,
immediate = false,
): (...args: Parameters<T>) => void {
let timeout: ReturnType<typeof setTimeout> | null;
return function (this: any, ...args: Parameters<T>) {
const context = this;
const later = () => {
timeout = null;
if (!immediate) func.apply(context, args);
};
const callNow = immediate && !timeout;
if (timeout) clearTimeout(timeout);
timeout = setTimeout(later, wait);
if (callNow) func.apply(context, args);
};
}
/** 节流 */
export function throttle<T extends (...args: any[]) => void>(
func: T,
limit: number,
): (...args: Parameters<T>) => void {
let inThrottle = false;
return function (this: any, ...args: Parameters<T>) {
if (!inThrottle) {
func.apply(this, args);
inThrottle = true;
setTimeout(() => { inThrottle = false; }, limit);
}
};
}
/** 深度合并 */
export const deepMerge = <T extends Record<string, unknown>>(target: T, source: Partial<T>): T => {
const output: Record<string, unknown> = { ...target };
for (const key of Object.keys(source as Record<string, unknown>)) {
const sv = (source as Record<string, unknown>)[key];
const tv = (target as Record<string, unknown>)[key];
if (sv instanceof Object && key in target && tv instanceof Object) {
output[key] = deepMerge(tv as Record<string, unknown>, sv as Record<string, unknown>);
} else {
output[key] = source[key];
}
}
return output as T;
};
/** 浏览器环境检测 */
export const isBrowser = (): boolean => {
return typeof window !== 'undefined' && typeof document !== 'undefined';
};