release: v0.2.5 — 质量加固 + Bug修复 + 性能优化 + SQL扩展
CI / test (18.x) (push) Successful in 10m4s
CI / test (20.x) (push) Successful in 10m0s
CI / test (22.x) (push) Successful in 9m58s
CI / test (24.x) (push) Successful in 9m58s

This commit is contained in:
thzxx
2026-07-29 21:50:53 +08:00
parent 29d779d96a
commit eb79b2198e
34 changed files with 2017 additions and 298 deletions
+2 -2
View File
@@ -89,7 +89,7 @@ export interface DatabaseConfig {
onReady?: (db: unknown) => void;
/** 错误回调 */
onError?: (error: Error) => void;
/** 查询结果行数上限(默认 100000 表示不限制) */
/** 查询结果行数上限(默认 0,0 表示不限制) */
maxRowsPerQuery?: number;
/** 调试模式(启用后输出详细操作日志) */
debug?: boolean;
@@ -209,4 +209,4 @@ export class DatabaseError extends Error {
// 版本
// ---------------------------------------------------------------------------
export const VERSION = '0.2.0';
export const VERSION = '0.2.5';
+41 -15
View File
@@ -72,13 +72,13 @@ export class MetonaSqlark {
await this.engine.open(this.name, this.version);
// 初始化执行器和事务管理器
this.executor = new QueryExecutor(this.engine);
this.executor = new QueryExecutor(this.engine, this.maxRowsPerQuery);
this.transactionManager = new TransactionManager(this.engine);
// 注册插件
if (this.config.plugins) {
for (const plugin of this.config.plugins) {
this.pluginManager.register(plugin);
this.pluginManager.register(plugin, this);
}
}
@@ -102,9 +102,14 @@ export class MetonaSqlark {
this.ensureReady();
const schema = createSchema(name, columns);
await this.pluginManager.trigger('beforeCreateTable', schema);
await this.engine.createTable(schema);
await this.pluginManager.trigger('afterCreateTable', schema);
try {
await this.pluginManager.trigger('beforeCreateTable', schema);
await this.engine.createTable(schema);
await this.pluginManager.trigger('afterCreateTable', schema);
} catch (error) {
this._onError(error as Error);
throw error;
}
// 清除缓存
this.tableCache.delete(name);
@@ -125,9 +130,14 @@ export class MetonaSqlark {
/** 删除表 */
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);
try {
await this.pluginManager.trigger('beforeDropTable', name);
await this.engine.dropTable(name);
await this.pluginManager.trigger('afterDropTable', name);
} catch (error) {
this._onError(error as Error);
throw error;
}
this.tableCache.delete(name);
}
@@ -146,8 +156,14 @@ export class MetonaSqlark {
await this.pluginManager.trigger('beforeQuery', sql);
const stmt: Statement = parse(sql);
const result = await this.executor.execute(stmt);
let result: unknown;
try {
const stmt: Statement = parse(sql);
result = await this.executor.execute(stmt);
} catch (error) {
this._onError(error as Error);
throw error;
}
await this.pluginManager.trigger('afterQuery', sql, result);
@@ -166,9 +182,14 @@ export class MetonaSqlark {
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;
try {
const result = await this.transactionManager.execute(fn);
await this.pluginManager.trigger('afterTransaction');
return result;
} catch (error) {
this._onError(error as Error);
throw error;
}
}
// ---- 导入导出 ----
@@ -182,7 +203,12 @@ export class MetonaSqlark {
/** 导入 JSON 数据到表 */
async importTable(tableName: string, data: Record<string, unknown>[]): Promise<string[]> {
this.ensureReady();
return this.engine.insert(tableName, data);
try {
return await this.engine.insert(tableName, data);
} catch (error) {
this._onError(error as Error);
throw error;
}
}
/** 导出整个数据库为 JSON */
@@ -273,7 +299,7 @@ export class MetonaSqlark {
case 'disk':
return diskEngine === 'opfs' ? new OPFSEngine() : new IndexedDBEngine();
case 'aria':
return new AriaEngine({ storageBackend: diskEngine === 'opfs' ? 'memory' : 'indexeddb' });
return new AriaEngine({ storageBackend: diskEngine === 'opfs' ? 'opfs' : 'indexeddb' });
case 'hybrid':
return new HybridEngine(diskEngine);
default:
+60 -23
View File
@@ -2,43 +2,80 @@
* AriaEngine Crypto — 页面级 AES-GCM 加密
* @module engine/aria/crypto
*
* 使用 Web Crypto API (SubtleCrypto) 进行 AES-256-GCM 加密
* v0.2.5: 改为实例化 CryptoManager,避免多实例共享全局状态
* 保留全局函数兼容旧代码(委托给全局单例)。
*/
const ALGO = 'AES-GCM';
const IV_LENGTH = 12;
let cryptoKey: CryptoKey | null = null;
let enabled = false;
/**
* CryptoManager — 实例级加密管理器
* 每个 AriaEngine 实例可拥有独立的加密配置。
*/
export class CryptoManager {
private cryptoKey: CryptoKey | null = null;
private _enabled = false;
get enabled(): boolean { return this._enabled; }
async init(password: string, salt?: Uint8Array): Promise<Uint8Array> {
const enc = new TextEncoder();
const keyMaterial = await crypto.subtle.importKey(
'raw', enc.encode(password), 'PBKDF2', false, ['deriveKey'],
);
const actualSalt: any = salt || crypto.getRandomValues(new Uint8Array(16));
this.cryptoKey = await crypto.subtle.deriveKey(
{ name: 'PBKDF2', salt: actualSalt, iterations: 100000, hash: 'SHA-256' } as any,
keyMaterial, { name: ALGO, length: 256 } as any, false, ['encrypt', 'decrypt'],
);
this._enabled = true;
return actualSalt as Uint8Array;
}
async encryptPage(data: ArrayBuffer): Promise<{ iv: Uint8Array; data: ArrayBuffer }> {
if (!this.cryptoKey) throw new Error('Crypto not initialized');
const iv = crypto.getRandomValues(new Uint8Array(IV_LENGTH)) as any;
const ciphertext = await crypto.subtle.encrypt({ name: ALGO, iv } as any, this.cryptoKey, data);
return { iv: iv as Uint8Array, data: ciphertext };
}
async decryptPage(iv: Uint8Array, data: ArrayBuffer): Promise<ArrayBuffer> {
if (!this.cryptoKey) throw new Error('Crypto not initialized');
return crypto.subtle.decrypt({ name: ALGO, iv } as any, this.cryptoKey, data);
}
close(): void {
this.cryptoKey = null;
this._enabled = false;
}
}
// ---------------------------------------------------------------------------
// 全局兼容层(旧代码仍可使用全局函数)
// ---------------------------------------------------------------------------
const globalCrypto = new CryptoManager();
/** @deprecated 使用 CryptoManager 实例代替 */
export async function initCrypto(password: string, salt?: Uint8Array): Promise<Uint8Array> {
const enc = new TextEncoder();
const keyMaterial = await crypto.subtle.importKey(
'raw', enc.encode(password), 'PBKDF2', false, ['deriveKey'],
);
const actualSalt: any = salt || crypto.getRandomValues(new Uint8Array(16));
cryptoKey = await crypto.subtle.deriveKey(
{ name: 'PBKDF2', salt: actualSalt, iterations: 100000, hash: 'SHA-256' } as any,
keyMaterial, { name: ALGO, length: 256 } as any, false, ['encrypt', 'decrypt'],
);
enabled = true;
return actualSalt as Uint8Array;
return globalCrypto.init(password, salt);
}
export function isCryptoEnabled(): boolean { return enabled; }
/** @deprecated 使用 CryptoManager 实例代替 */
export function isCryptoEnabled(): boolean { return globalCrypto.enabled; }
/** @deprecated 使用 CryptoManager 实例代替 */
export async function encryptPage(data: ArrayBuffer): Promise<{ iv: Uint8Array; data: ArrayBuffer }> {
if (!cryptoKey) throw new Error('Crypto not initialized');
const iv = crypto.getRandomValues(new Uint8Array(IV_LENGTH)) as any;
const ciphertext = await crypto.subtle.encrypt({ name: ALGO, iv } as any, cryptoKey, data);
return { iv: iv as Uint8Array, data: ciphertext };
return globalCrypto.encryptPage(data);
}
/** @deprecated 使用 CryptoManager 实例代替 */
export async function decryptPage(iv: Uint8Array, data: ArrayBuffer): Promise<ArrayBuffer> {
if (!cryptoKey) throw new Error('Crypto not initialized');
return crypto.subtle.decrypt({ name: ALGO, iv } as any, cryptoKey, data);
return globalCrypto.decryptPage(iv, data);
}
/** @deprecated 使用 CryptoManager 实例代替 */
export function closeCrypto(): void {
cryptoKey = null;
enabled = false;
globalCrypto.close();
}
+23 -13
View File
@@ -2,7 +2,7 @@
* AriaEngine — 自研页面式存储引擎主类
* @module engine/aria/index
*
* v0.2.4: 二级索引 + MVCC 集成 + 生产加固
* v0.2.5: WAL 同步修复 + MVCC 接入 + 版本统一 + 生产加固
*/
import type { IStorageEngine } from '../interface';
@@ -27,6 +27,7 @@ import { BloomFilter } from './index/bloom';
import { BufferPool } from './buffer/pool';
import { compressLZ4, decompressLZ4 } from './compression/lz4';
import { isCryptoEnabled, encryptPage, decryptPage } from './crypto';
import type { WALRecord as _WALRecord } from './types';
// ---------------------------------------------------------------------------
// AriaEngine
@@ -163,12 +164,13 @@ export class AriaEngine implements IStorageEngine {
}
}
// 8. Checkpoint ManagerBufferPool 暂简化,使用 flush 替代
// 8. Checkpoint Manager接入 WAL 大小阈值
this.checkpointManager = new CheckpointManager(
this.lsm,
this.wal,
{ flushAll: async () => { await this.lsm.flush(); } } as any,
this.config.checkpointInterval,
this.config.walSizeThreshold,
);
this.opened = true;
@@ -220,7 +222,7 @@ export class AriaEngine implements IStorageEngine {
await this.persistSchemas();
this.wal.append({
await this.wal.append({
type: WALRecordType.CREATE_TABLE,
txnId: 0,
tableName: schema.name,
@@ -244,7 +246,7 @@ export class AriaEngine implements IStorageEngine {
this.tablePKs.delete(tableName);
await this.persistSchemas();
this.wal.append({
await this.wal.append({
type: WALRecordType.DROP_TABLE,
txnId: 0,
tableName,
@@ -293,8 +295,9 @@ export class AriaEngine implements IStorageEngine {
}
if (this.currentTxnId && this.txnSnapshot) {
// Within transaction: buffer to snapshot
// Within transaction: buffer to snapshot + MVCC version chain
this.txnSnapshot.set(key, validated);
this.mvcc.writeVersion(tableName, pkValue, validated, this.currentTxnId);
} else {
// Direct write to LSM (PK index)
this.lsm.put(key, validated);
@@ -305,7 +308,7 @@ export class AriaEngine implements IStorageEngine {
pks.push(pkValue);
this.wal.append({
await this.wal.append({
type: WALRecordType.INSERT,
txnId: this.currentTxnId ?? 0,
tableName,
@@ -399,12 +402,13 @@ export class AriaEngine implements IStorageEngine {
if (this.currentTxnId && this.txnSnapshot) {
this.txnSnapshot.set(key, updated);
this.mvcc.writeVersion(tableName, String(row[pkCol]), updated, this.currentTxnId);
} else {
this.lsm.put(key, updated);
}
count++;
this.wal.append({
await this.wal.append({
type: WALRecordType.UPDATE,
txnId: this.currentTxnId ?? 0,
tableName,
@@ -435,14 +439,15 @@ export class AriaEngine implements IStorageEngine {
if (!query.where || Object.keys(query.where).length === 0 || matchWhere(row, query.where)) {
if (this.currentTxnId && this.txnSnapshot) {
// Buffer delete in snapshot
// Buffer delete in snapshot + MVCC tombstone
this.txnSnapshot.set(key, { __txn_deleted: true } as unknown as Record<string, unknown>);
this.mvcc.deleteVersion(tableName, String(row[pkCol]), this.currentTxnId);
} else {
this.lsm.delete(key);
}
count++;
this.wal.append({
await this.wal.append({
type: WALRecordType.DELETE,
txnId: this.currentTxnId ?? 0,
tableName,
@@ -486,7 +491,7 @@ export class AriaEngine implements IStorageEngine {
this.currentTxnId = this.mvcc.beginTransaction();
this.txnSnapshot = new Map();
this.wal.append({
await this.wal.append({
type: WALRecordType.BEGIN,
txnId: this.currentTxnId,
tableName: '',
@@ -509,7 +514,7 @@ export class AriaEngine implements IStorageEngine {
this.mvcc.commitTransaction(this.currentTxnId);
this.wal.append({
await this.wal.append({
type: WALRecordType.COMMIT,
txnId: this.currentTxnId,
tableName: '',
@@ -527,7 +532,7 @@ export class AriaEngine implements IStorageEngine {
this.mvcc.rollbackTransaction(this.currentTxnId);
this.txnSnapshot = null;
this.wal.append({
await this.wal.append({
type: WALRecordType.ROLLBACK,
txnId: this.currentTxnId,
tableName: '',
@@ -922,6 +927,11 @@ export class AriaEngine implements IStorageEngine {
}
}
/** 估算 WAL 大小(字节) */
getWALEstimatedSize(): number {
return this.wal.getBufferedCount() * 200; // 粗略估算每条 ~200B
}
/**
* ANALYZE: 收集表统计信息
* 返回行数、平均行大小、索引深度等
@@ -997,7 +1007,7 @@ export class AriaEngine implements IStorageEngine {
// 压缩各层级
for (let level = 0; level < 6; level++) {
if (this.lsm.getStats().levelCounts[level] >= 2) {
(this.lsm as any).compactLevelSync(level);
this.lsm.compactLevel(level);
}
}
// GC MVCC 版本(保留最新 10 个)
+6 -1
View File
@@ -331,7 +331,12 @@ export class LSM {
// Compaction
// =======================================================================
/** 同步执行 Compaction简化版,仅供内部调用) */
/** 同步执行 Compactionpublic,供 VACUUM 等外部调用) */
compactLevel(level: number): void {
this.compactLevelSync(level);
}
/** 同步执行 Compaction(简化版,内部实现) */
private compactLevelSync(level: number): void {
if (level >= MAX_LSM_LEVELS - 1) return;
if (this.levels[level].length < 4) return;
+12 -6
View File
@@ -240,16 +240,22 @@ export class SSTableReader {
}
private locateBlockGE(key: string): number {
for (let i = 0; i < this.indexEntries.length; i++) {
if (this.indexEntries[i].key >= key) return i;
let lo = 0, hi = this.indexEntries.length;
while (lo < hi) {
const mid = (lo + hi) >> 1;
if (this.indexEntries[mid].key < key) lo = mid + 1;
else hi = mid;
}
return this.indexEntries.length - 1;
return lo < this.indexEntries.length ? lo : this.indexEntries.length - 1;
}
private locateBlockLE(key: string): number {
for (let i = this.indexEntries.length - 1; i >= 0; i--) {
if (this.indexEntries[i].key <= key) return i;
let lo = 0, hi = this.indexEntries.length;
while (lo < hi) {
const mid = (lo + hi) >> 1;
if (this.indexEntries[mid].key <= key) lo = mid + 1;
else hi = mid;
}
return 0;
return lo > 0 ? lo - 1 : 0;
}
}
+11 -1
View File
@@ -24,26 +24,36 @@ export class CheckpointManager {
private flushable: Flushable | null;
private interval: number;
private opCount = 0;
private walSizeThreshold: number;
constructor(
lsm: LSM,
wal: WAL,
flushable: Flushable | null = null,
interval: number = 1000,
walSizeThreshold: number = 16 * 1024 * 1024,
) {
this.lsm = lsm;
this.wal = wal;
this.flushable = flushable;
this.interval = interval;
this.walSizeThreshold = walSizeThreshold;
}
async tick(): Promise<void> {
this.opCount++;
if (this.opCount >= this.interval) {
// 检查操作计数或 WAL 大小是否超阈值
if (this.opCount >= this.interval || this.getWALEstimatedSize() >= this.walSizeThreshold) {
await this.checkpoint();
}
}
/** 估算 WAL 大小 */
private getWALEstimatedSize(): number {
const count = typeof this.wal.getBufferedCount === 'function' ? this.wal.getBufferedCount() : 0;
return count * 200;
}
async checkpoint(): Promise<void> {
await this.lsm.flush();
if (this.flushable) {
+6 -4
View File
@@ -59,8 +59,8 @@ export class WAL {
// 写入
// =======================================================================
/** 追加一条 WAL 记录 */
append(record: Omit<WALRecord, 'lsn' | 'checksum'>): void {
/** 追加一条 WAL 记录full 模式同步等待写入完成) */
async append(record: Omit<WALRecord, 'lsn' | 'checksum'>): Promise<void> {
if (!this.enabled) return;
this.lsn++;
@@ -73,10 +73,12 @@ export class WAL {
const bytes = this.encodeRecord(fullRecord);
if (this.syncMode === 'full') {
this.store.append(bytes).catch(() => {
try {
await this.store.append(bytes);
} catch {
// eslint-disable-next-line no-console
console.warn('[AriaEngine WAL] Failed to append record');
});
}
} else if (this.syncMode === 'batch') {
this.buffer.push(bytes);
}
+71
View File
@@ -187,6 +187,14 @@ export class IndexedDBEngine implements IStorageEngine {
private async idbFind(tableName: string, query: QueryPlan): Promise<Record<string, unknown>[]> {
const db = this.ensureDB();
// 尝试使用 IDB 索引进行等值查询
if (query.where) {
const indexResult = await this.tryIDBIndexLookup(db, tableName, query);
if (indexResult !== null) return indexResult;
}
// 回退到全量 getAll + 内存过滤
return new Promise((resolve, reject) => {
const tx = db.transaction(tableName, 'readonly');
const req = tx.objectStore(tableName).getAll();
@@ -210,6 +218,69 @@ export class IndexedDBEngine implements IStorageEngine {
});
}
/** 尝试使用 IDB 索引进行等值查询,成功返回结果,不适用返回 null */
private async tryIDBIndexLookup(
db: IDBDatabase,
tableName: string,
query: QueryPlan,
): Promise<Record<string, unknown>[] | null> {
if (!query.where) return null;
for (const [col, condition] of Object.entries(query.where)) {
// 跳过逻辑组合符
if (col === '$and' || col === '$or' || col === '$not') continue;
// 只处理等值查询
let targetValue: unknown;
if (typeof condition !== 'object' || condition === null) {
targetValue = condition;
} else if ('$eq' in (condition as Record<string, unknown>)) {
targetValue = (condition as Record<string, unknown>).$eq;
} else {
continue;
}
// 检查是否有对应的 IDB 索引
const indexName = `idx_${col}`;
try {
return await new Promise((resolve, reject) => {
const tx = db.transaction(tableName, 'readonly');
const store = tx.objectStore(tableName);
if (!store.indexNames.contains(indexName)) {
resolve(null); // 没有索引,回退
return;
}
const index = store.index(indexName);
const req = index.getAll(targetValue as IDBValidKey);
req.onsuccess = () => {
let results: Record<string, unknown>[] = req.result ?? [];
// 如果有其他 WHERE 条件,继续过滤
const otherKeys = Object.keys(query.where!).filter(
(k) => k !== col && k !== '$and' && k !== '$or' && k !== '$not',
);
if (otherKeys.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(`Index lookup failed for "${tableName}.${col}"`, 'IDB_READ_ERROR', req.error));
});
} catch {
return null; // 索引不可用,回退
}
}
return null;
}
private async idbUpdate(tableName: string, query: QueryPlan, updates: Record<string, unknown>): Promise<void> {
const db = this.ensureDB();
return new Promise((resolve, reject) => {
+1 -1
View File
@@ -1,7 +1,7 @@
/**
* metona-sqlark — 入口文件
* @module metona-sqlark
* @version 0.1.12
* @version 0.2.5
*
* 前端关系型数据库,内存与磁盘双模式。
* 支持 Query Builder 链式 API 和 SQL 字符串查询。
+10 -2
View File
@@ -1,6 +1,6 @@
// @ts-nocheck
/**
* metona-sqlark React Integration (v0.1.11)
* metona-sqlark React Integration (v0.2.5)
* @module integrations/react
*
* 轻量 React hooks,需要 react 作为 peer dependency。
@@ -46,12 +46,20 @@ export function useQuery(
return { data, loading, error, refresh: execute };
}
/** 表名合法性校验(防 SQL 注入) */
function validateTableName(name: string): string {
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(name)) {
throw new Error(`Invalid table name: "${name}"`);
}
return name;
}
/** 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]);
const { data, loading, refresh } = useQuery(db, `SELECT * FROM ${validateTableName(tableName)}`, [tableName]);
return { data, loading, refresh };
}
+10 -2
View File
@@ -1,6 +1,6 @@
// @ts-nocheck
/**
* metona-sqlark Vue Integration (v0.1.11)
* metona-sqlark Vue Integration (v0.2.5)
* @module integrations/vue
*
* 轻量 Vue composables,需要 vue 作为 peer dependency。
@@ -43,12 +43,20 @@ export function useSqlarkQuery(
return { data, loading, error, refresh: execute };
}
/** 表名合法性校验(防 SQL 注入) */
function validateTableName(name: string): string {
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(name)) {
throw new Error(`Invalid table name: "${name}"`);
}
return name;
}
/** 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}`);
const { data, loading, refresh } = useSqlarkQuery(db, `SELECT * FROM ${validateTableName(tableName)}`);
return { data, loading, refresh };
}
+3 -3
View File
@@ -22,7 +22,7 @@ export class PluginManager {
private hooks: Map<HookName, HookCallback[]> = new Map();
/** 注册插件 */
register(plugin: MetonaPlugin): void {
register(plugin: MetonaPlugin, db?: unknown): void {
// 按优先级插入
const priority = plugin.priority ?? 0;
const insertIndex = this.plugins.findIndex(
@@ -33,8 +33,8 @@ export class PluginManager {
} else {
this.plugins.splice(insertIndex, 0, plugin);
}
// 安装
plugin.install(null); // 实际引用由 MetonaSqlark 注入
// 安装(传入 db 实例)
plugin.install(db);
}
/** 卸载插件 */
+26 -2
View File
@@ -19,7 +19,9 @@ export type StatementType =
| 'UPDATE'
| 'DELETE'
| 'CREATE_TABLE'
| 'DROP_TABLE';
| 'DROP_TABLE'
| 'ALTER_TABLE'
| 'TRUNCATE_TABLE';
// ---------------------------------------------------------------------------
// 列引用
@@ -171,6 +173,26 @@ export interface SelectStatement {
offset?: number;
}
// ---------------------------------------------------------------------------
// DDL: ALTER TABLE
// ---------------------------------------------------------------------------
export interface AlterTableStatement {
type: 'ALTER_TABLE';
name: string;
action: 'ADD' | 'DROP';
column: ASTColumnDef;
}
// ---------------------------------------------------------------------------
// DDL: TRUNCATE TABLE
// ---------------------------------------------------------------------------
export interface TruncateTableStatement {
type: 'TRUNCATE_TABLE';
name: string;
}
// ---------------------------------------------------------------------------
// AST 联合类型
// ---------------------------------------------------------------------------
@@ -182,4 +204,6 @@ export type Statement =
| UpdateStatement
| DeleteStatement
| CreateTableStatement
| DropTableStatement;
| DropTableStatement
| AlterTableStatement
| TruncateTableStatement;
+47 -1
View File
@@ -9,6 +9,7 @@ import type { IStorageEngine } from '../engine/interface';
import type {
Statement, SelectStatement, InsertStatement, UpdateStatement,
DeleteStatement, CreateTableStatement, DropTableStatement, JoinClause,
AlterTableStatement, TruncateTableStatement,
} from './ast';
import { DatabaseError } from '../constants';
import { compileStatement } from './compiler';
@@ -21,7 +22,16 @@ import type { WhereCondition } from '../constants';
// ---------------------------------------------------------------------------
export class QueryExecutor {
constructor(private engine: IStorageEngine) {}
private maxRowsPerQuery: number;
constructor(private engine: IStorageEngine, maxRowsPerQuery: number = 0) {
this.maxRowsPerQuery = maxRowsPerQuery;
}
/** 设置查询结果行数上限 */
setMaxRowsPerQuery(max: number): void {
this.maxRowsPerQuery = max;
}
async execute(stmt: Statement): Promise<unknown> {
switch (stmt.type) {
@@ -32,6 +42,8 @@ export class QueryExecutor {
case 'DELETE': return this.executeDelete(stmt);
case 'CREATE_TABLE': return this.executeCreateTable(stmt);
case 'DROP_TABLE': return this.executeDropTable(stmt);
case 'ALTER_TABLE': return this.executeAlterTable(stmt as any);
case 'TRUNCATE_TABLE': return this.executeTruncateTable(stmt as any);
default: throw new DatabaseError('Unknown statement type', 'UNKNOWN_STATEMENT');
}
}
@@ -99,6 +111,12 @@ export class QueryExecutor {
if (!hasGroupBy && !hasAggregate && stmt.columns.length > 0 && stmt.columns[0] !== '*') {
rows = rows.map((row) => projectColumns(row, stmt.columns));
}
// 全局行数上限保护
if (this.maxRowsPerQuery > 0 && rows.length > this.maxRowsPerQuery) {
rows = rows.slice(0, this.maxRowsPerQuery);
}
return rows;
}
@@ -272,6 +290,34 @@ export class QueryExecutor {
return this.engine.dropTable(stmt.name);
}
private async executeAlterTable(stmt: AlterTableStatement): Promise<void> {
const exists = await this.engine.hasTable(stmt.name);
if (!exists) throw new DatabaseError(`Table "${stmt.name}" does not exist`, 'TABLE_NOT_FOUND');
const schema = await this.engine.getTableSchema(stmt.name);
if (!schema) return;
if (stmt.action === 'ADD') {
if (schema.columns[stmt.column.name]) {
throw new DatabaseError(`Column "${stmt.column.name}" already exists in table "${stmt.name}"`, 'COLUMN_EXISTS');
}
schema.columns[stmt.column.name] = astColumnToColumnDef(stmt.column);
} else if (stmt.action === 'DROP') {
if (!schema.columns[stmt.column.name]) {
throw new DatabaseError(`Column "${stmt.column.name}" does not exist in table "${stmt.name}"`, 'COLUMN_NOT_FOUND');
}
delete schema.columns[stmt.column.name];
}
// 重建表结构
await this.engine.dropTable(stmt.name);
await this.engine.createTable(schema);
}
private async executeTruncateTable(stmt: TruncateTableStatement): Promise<void> {
const exists = await this.engine.hasTable(stmt.name);
if (!exists) throw new DatabaseError(`Table "${stmt.name}" does not exist`, 'TABLE_NOT_FOUND');
return this.engine.clear(stmt.name);
}
getEngine(): IStorageEngine { return this.engine; }
// ===================================================================
+52
View File
@@ -16,6 +16,8 @@ import type {
DeleteStatement,
CreateTableStatement,
DropTableStatement,
AlterTableStatement,
TruncateTableStatement,
ASTColumnDef,
} from '../query/ast';
import type { WhereCondition, OrderBy, SortDirection } from '../constants';
@@ -52,6 +54,10 @@ export class Parser {
return this.parseCreateTable();
case TokenType.DROP:
return this.parseDropTable();
case TokenType.ALTER:
return this.parseAlterTable();
case TokenType.TRUNCATE:
return this.parseTruncateTable();
default:
throw this.error(`Unexpected token "${this.curToken.value}"`);
}
@@ -426,6 +432,52 @@ export class Parser {
return 'RESTRICT';
}
// ===================================================================
// ALTER TABLE
// ===================================================================
private parseAlterTable(): AlterTableStatement {
this.expect(TokenType.ALTER);
this.expect(TokenType.TABLE);
const tableName = this.expectIdentifier('table name');
// ADD COLUMN / DROP COLUMN
let action: 'ADD' | 'DROP';
if (this.curTokenIs(TokenType.ADD)) {
action = 'ADD';
this.nextToken();
// Optional COLUMN keyword
if (this.curToken.type === TokenType.IDENTIFIER && this.curToken.value.toUpperCase() === 'COLUMN') {
this.nextToken();
}
const col = this.parseColumnDef();
return { type: 'ALTER_TABLE', name: tableName, action, column: col };
} else if (this.curTokenIs(TokenType.DROP) ||
(this._isKeywordAsIdent() && this.curToken.value.toUpperCase() === 'DROP')) {
action = 'DROP';
this.nextToken();
// Optional COLUMN keyword
if (this.curToken.type === TokenType.IDENTIFIER && this.curToken.value.toUpperCase() === 'COLUMN') {
this.nextToken();
}
const colName = this.expectIdentifier('column name');
return { type: 'ALTER_TABLE', name: tableName, action, column: { name: colName, type: 'string' } };
} else {
throw this.error('Expected ADD or DROP in ALTER TABLE');
}
}
// ===================================================================
// TRUNCATE TABLE
// ===================================================================
private parseTruncateTable(): TruncateTableStatement {
this.expect(TokenType.TRUNCATE);
this.expect(TokenType.TABLE);
const tableName = this.expectIdentifier('table name');
return { type: 'TRUNCATE_TABLE', name: tableName };
}
// ===================================================================
// DROP TABLE
// ===================================================================
+6
View File
@@ -44,6 +44,9 @@ export enum TokenType {
IF = 'IF',
EXISTS = 'EXISTS',
FALSE = 'FALSE',
ALTER = 'ALTER',
ADD = 'ADD',
TRUNCATE = 'TRUNCATE',
// JOIN 相关
INNER = 'INNER',
@@ -139,6 +142,9 @@ export const KEYWORDS: Record<string, TokenType> = {
'BETWEEN': TokenType.BETWEEN,
'IF': TokenType.IF,
'EXISTS': TokenType.EXISTS,
'ALTER': TokenType.ALTER,
'ADD': TokenType.ADD,
'TRUNCATE': TokenType.TRUNCATE,
// JOIN
'INNER': TokenType.INNER,
+1 -1
View File
@@ -1,5 +1,5 @@
/**
* YOUR-PROJECT Utils — 工具函数
* metona-sqlark Utils — 工具函数
* @module utils
*/