Files
MetonaSqlark/dist/metona-sqlark.d.ts
T

2378 lines
105 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* AriaEngine Storage Backend — 存储后端抽象层
* @module engine/aria/store/backend
*
* 封装底层浏览器存储 APIIndexedDB / OPFS / Memory 回退),
* 供 Buffer Pool 的 PageIO 和 WAL 的 WALStore 使用。
*/
interface IStorageBackend {
/** 打开存储 */
open(name: string): Promise<void>;
/** 关闭存储 */
close(): Promise<void>;
/** 是否已打开 */
isOpen(): boolean;
/** 读取数据块 */
read(key: string): Promise<ArrayBuffer | null>;
/** 写入数据块 */
write(key: string, data: ArrayBuffer): Promise<void>;
/**
* 追加写入(v0.4.5 WAL 分片用,可选):
* - OPFS 后端实现真追加(createWritable keepExistingData + seekO(chunk)
* - 未实现的后端由调用方回退 read+writeEncryptedBackend 包装时整体重写保正确性)
* 语义:在 key 现有内容末尾追加 data;key 不存在时等同 write。
*/
append?(key: string, data: ArrayBuffer): Promise<void>;
/**
* 批量原子写入(v0.4.2-fix):多个 key 在单个底层事务中提交,
* 中断时整体回滚,不留半写状态。WAL count 与记录同事务保证一致性。
*/
writeMany(entries: Record<string, ArrayBuffer>): Promise<void>;
/** 删除数据块 */
delete(key: string): Promise<void>;
/**
* 批量原子删除(v0.4.2-fix):多个 key 在单个底层事务中提交。
*/
deleteMany(keys: string[]): Promise<void>;
/** 列出所有 key */
listKeys(): Promise<string[]>;
/** 检查 key 是否存在 */
exists(key: string): Promise<boolean>;
/** 清空所有数据 */
clear(): Promise<void>;
}
interface AriaEngineConfig {
/** 页面大小(默认 4096 */
pageSize?: number;
/** Buffer Pool 页面数量(默认 256 */
bufferPoolPages?: number;
/** MemTable 刷盘阈值(默认 4MB */
memtableSizeThreshold?: number;
/** LSM 层级之间的容量倍数(默认 10) */
levelSizeMultiplier?: number;
/** Bloom Filter 每 key 位数(默认 10 */
bloomFilterBitsPerKey?: number;
/** 是否启用 WAL(默认 true */
walEnabled?: boolean;
/** WAL 同步模式 */
walSyncMode?: 'full' | 'batch' | 'none';
/** Checkpoint 间隔(操作数,默认 1000 */
checkpointInterval?: number;
/** 是否启用页面压缩(默认 false) */
compression?: boolean;
/** 存储后端 */
storageBackend?: 'opfs' | 'memory' | 'kv';
/** WAL 大小阈值(字节,超过则强制 checkpoint,默认 16MB */
walSizeThreshold?: number;
/** 最大内存预算(MB,默认 64) */
maxMemoryMB?: number;
/**
* v0.4.5: 全库 AES-256-GCM 加密(backend 层透明加解密,WAL/SSTable/Schema/元数据全覆盖)。
* 密钥由 PBKDF2salt 持久化于库内 __aria_keymeta)派生,重启用同一密码即可解密。
*/
encryption?: {
/** 加密密码 */
password: string;
};
/**
* v0.4.5: SSTable 页面化存储(4KB 页面 + BufferPool/FileManager 管理,LRU 缓存)。
* 默认:storageBackend === 'opfs' 时自动启用(大文件随机读/写放大优化);
* 显式 false 强制关闭(整 value 存储,兼容旧行为)。
*/
pageStorage?: boolean;
/**
* **仅测试使用**:直接注入存储后端(跳过 storageBackend 选择逻辑)。
*
* 为什么需要这个口子:崩溃/撕裂语义的验证必须让引擎**从打开那一刻起**
* 就走被测后端 —— `this.backend` 在 `open()` 里被 WAL、FileManager、
* SSTableStore 一起捕获,事后替换 `engine.backend` 只会替换其中一部分
*(实测:替换后 WAL 仍写旧后端,于是"崩溃"根本没覆盖 WAL 路径,
* 探针得出的是假结论)。缺少这个口子会让所有故障注入只能在孤立后端上
* 验证,而无法验证"引擎整体在崩裂介质上的行为"。
*/
testBackend?: IStorageBackend;
}
/**
* metona-sqlark Constants — 类型定义 / 默认配置 / 枚举
* @module constants
*/
/** 存储模式 */
type StorageMode = 'memory' | 'disk' | 'hybrid' | 'aria';
/** 磁盘引擎类型(v0.6.0: IndexedDB 已移除;'memory' 供 aria 内存后端;'kv' 供 aria 自研 KVStore 后端) */
type DiskEngine = 'opfs' | 'memory' | 'kv';
/** 字段数据类型 */
type FieldType = 'string' | 'number' | 'boolean' | 'date' | 'json';
/** 列定义 */
interface ColumnDef {
/** 字段类型 */
type: FieldType;
/** 是否主键 */
primaryKey?: boolean;
/** 是否必填 */
required?: boolean;
/** 是否唯一 */
unique?: boolean;
/** 默认值 */
default?: unknown;
/** 是否创建索引 */
index?: boolean;
/** 外键引用: 'table.column' */
references?: string;
/** 删除级联: 'CASCADE' | 'SET NULL' | 'RESTRICT' */
onDelete?: 'CASCADE' | 'SET NULL' | 'RESTRICT';
/** 更新级联: 'CASCADE' | 'SET NULL' | 'RESTRICT' */
onUpdate?: 'CASCADE' | 'SET NULL' | 'RESTRICT';
/** 字符串最大长度 */
maxLength?: number;
/** 数字最小值 */
min?: number;
/** 数字最大值 */
max?: number;
}
/** 表结构定义 */
interface TableSchema {
/** 表名 */
name: string;
/** 列定义映射 */
columns: Record<string, ColumnDef>;
}
/** 数据库配置 */
interface DatabaseConfig {
/** 数据库名称 */
name: string;
/** 存储模式 */
mode?: StorageMode;
/**
* 磁盘后端选择。
*
* v0.8.0 修正注释:**仅 `mode: 'aria'` 真正生效**(作为 Aria 的存储后端,
* 见 core.ts 的 aria 分支)。`disk` 模式恒用自研 KVStoreEngine
* `hybrid` 的内存+磁盘组合也恒用 KVStoreEngine —— 两者会忽略本项
*(此前注释写成"disk 模式生效",与实现相反)。
*/
diskEngine?: DiskEngine;
/** 版本号 */
version?: number;
/** 插件列表 */
plugins?: MetonaPlugin[];
/** 数据库就绪回调 */
onReady?: (db: unknown) => void;
/** 错误回调 */
onError?: (error: Error) => void;
/** 查询结果行数上限(默认 0,0 表示不限制) */
maxRowsPerQuery?: number;
/** 调试模式(启用后输出详细操作日志) */
debug?: boolean;
/** 多标签页同步(v0.3.2):BroadcastChannel 广播表变更,其他标签页自动刷新 */
multiTabSync?: boolean;
/**
* v0.4.5: AriaEngine 专属配置(mode='aria' 时透传):
* walSyncMode / checkpointInterval / encryption / pageStorage / compression 等
*/
aria?: AriaEngineConfig;
}
/** Where 条件操作符 */
type WhereOperator = '$eq' | '$ne' | '$gt' | '$gte' | '$lt' | '$lte' | '$in' | '$nin' | '$like' | '$and' | '$or' | '$not';
/** 简单条件值:直接相等 */
type SimpleCondition = unknown;
/** 操作符条件 */
type OperatorCondition = Partial<Record<WhereOperator, unknown>>;
/** 字段条件:简单值 | 操作符对象 */
type FieldCondition = SimpleCondition | OperatorCondition;
/** Where 条件对象 */
type WhereCondition = Record<string, FieldCondition>;
/** 排序方向 */
type SortDirection = 'asc' | 'desc';
/** 排序定义 */
interface OrderBy {
/** 列名 */
column: string;
/** 排序方向 */
direction: SortDirection;
/** v0.4.0: NULL 值排序位置(first 排最前 / last 排最后,默认同引擎行为) */
nulls?: 'first' | 'last';
}
/** 查询计划 — 由 Executor 编译 AST 后生成 */
interface QueryPlan {
/** 表名 */
table: string;
/** 要返回的列(undefined = 全部,['*'] = 全部) */
columns?: string[];
/** 过滤条件 */
where?: WhereCondition;
/** 排序 */
orderBy?: OrderBy[];
/** 限制条数 */
limit?: number;
/** 偏移量 */
offset?: number;
}
/** 钩子名称 */
type HookName = 'beforeCreateTable' | 'afterCreateTable' | 'beforeDropTable' | 'afterDropTable' | 'beforeInsert' | 'afterInsert' | 'beforeUpdate' | 'afterUpdate' | 'beforeDelete' | 'afterDelete' | 'beforeQuery' | 'afterQuery' | 'beforeTransaction' | 'afterTransaction';
/** 插件定义 */
interface MetonaPlugin {
/** 插件名称 */
name: string;
/** 插件版本 */
version: string;
/** 描述 */
description?: string;
/**
* 优先级:**越大越先执行**(含 `install()` 与钩子触发顺序)。
*
* v0.8.0 起真正生效 —— Core 会先按 priority 降序稳定排序再注册插件
*(同优先级保持 config 数组顺序)。此前 register() 虽按优先级插入数组,
* 但 install() 在 register 内立即执行,实际顺序 = config 数组顺序。
*/
priority?: number;
/** 安装 */
install(db: unknown): void;
/** 销毁 */
destroy(): void;
}
/** 数据库错误 */
declare class DatabaseError extends Error {
code: string;
details?: unknown | undefined;
constructor(message: string, code: string, details?: unknown | undefined);
}
declare const VERSION = "0.8.0";
/**
* metona-sqlark Plugin — 插件系统
* @module plugin
*
* 管理插件的注册、生命周期和钩子调度。
*/
type HookCallback = (...args: unknown[]) => void | Promise<void>;
declare class PluginManager {
private plugins;
private hooks;
/** 注册插件 */
register(plugin: MetonaPlugin, db?: unknown): void;
/** 卸载插件 */
unregister(pluginName: string): void;
/** 获取所有已注册插件 */
getPlugins(): MetonaPlugin[];
/** 添加钩子回调 */
on(hook: HookName, callback: HookCallback): void;
/** 移除钩子回调 */
off(hook: HookName, callback: HookCallback): void;
/** 触发钩子 */
trigger(hook: HookName, ...args: unknown[]): Promise<void>;
/** 销毁所有插件 */
destroy(): void;
}
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[]>;
/**
* v0.8.0(B-1):写入前置校验 —— 未知列、类型、`maxLength` / `min` / `max`、
* `required` / 主键非空、`__proto__` 防污染,全部由 `table/validation.ts` 的
* **唯一**实现判定。
*
* 为什么把它放进引擎接口而不是留在 Executor:
* - **未知列**必须在引擎边界拦下。`INSERT INTO t (id, nope) VALUES ('1', 2)`
* 若在 Executor 拦,QueryBuilder / `db.table().insert()` 等直通路径仍然静默
* 丢列(缺陷 A17 的真实形态:executor 按 schema 列序构造行,那个位置没有值,
* 于是 `nope` 既不进 schema 也无从校验)。
* - **规范化必须在同一处**:`default` 填充与类型检查一旦分家,就会出现
* "executor 校验通过、引擎写入时又被改写"这类双份语义。
*
* 引擎**必须**使用 `compileValidator` 而不是自己实现 —— 此前 Memory 与 Aria
* 各写一份,`maxLength` 只在 Aria 生效(A12)。
*
* @param mode `'insert'`default 生效、缺列合法)或 `'update'`(仅校验出现的列)。
* 默认 `'insert'`,与历史行为一致。
* @throws DatabaseError COLUMN_NOT_FOUND | VALIDATION_ERROR | TYPE_ERROR
*/
validatePayload?(tableName: string, rows: Record<string, unknown>[], mode?: 'insert' | 'update'): Promise<void>;
/** 查询行 */
find(tableName: string, query: QueryPlan): Promise<Record<string, unknown>[]>;
/** v0.4.0: 流式查询 — 逐行回调扫描(有 where/limit/projection,无 orderBy 语义;有 orderBy 时实现可回退物化) */
findStream?(tableName: string, query: QueryPlan, onRow: (row: Record<string, unknown>) => void): Promise<number>;
/** 更新行,返回影响行数 */
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>;
/** v0.4.1: ALTER TABLE(可选)— 引擎级结构变更(Aria 需重写存储行,其余引擎走 Executor 通用路径) */
alterTable?(tableName: string, action: 'ADD' | 'DROP', column: ColumnDef & {
name: string;
}): Promise<void>;
/** 创建二级索引(CREATE INDEX */
createIndex?(tableName: string, column: string, unique?: boolean): Promise<void>;
/** 删除二级索引(DROP INDEX */
dropIndex?(tableName: string, column: string, indexName?: string): Promise<void>;
/** 开始事务 */
beginTransaction(): Promise<void>;
/** 提交事务 */
commitTransaction(): Promise<void>;
/** 回滚事务 */
rollbackTransaction(): Promise<void>;
/** 创建 Savepoint */
savepoint?(name: string): Promise<void>;
/** 回滚到 Savepoint */
rollbackToSavepoint?(name: string): Promise<void>;
/** 释放 Savepoint */
releaseSavepoint?(name: string): Promise<void>;
/** 在线备份:导出全库一致性快照 */
backup?(): Promise<Record<string, Record<string, unknown>[]>>;
/** 崩溃恢复自愈:校验并清理损坏数据、恢复一致性(检测到异常后调用,无需删库重建) */
repair?(): Promise<void>;
/** 清空全部数据与表结构(保留库本身,供演示页刷新/重建用) */
clearAll?(): Promise<void>;
/** 读取库内元数据(迁移版本持久化用) */
getMeta?(key: string): Promise<string | null>;
/** 写入库内元数据(迁移版本持久化用) */
setMeta?(key: string, value: string): Promise<void>;
}
/**
* metona-sqlark Query AST — 查询抽象语法树类型定义
* @module query/ast
*
* QueryBuilder 和 SQL Parser 统一输出此 AST
* Executor 只认 AST,保证两种查询接口行为一致。
*/
/** 列引用,'*' 表示所有列;支持 'table.column' 格式 */
type ColumnRef = string;
/** JOIN 类型 */
type JoinType = 'INNER' | 'LEFT' | 'RIGHT' | 'CROSS';
/** JOIN 子句 */
interface JoinClause {
type: JoinType;
table: string;
alias?: string;
on: WhereCondition;
}
interface ASTColumnDef {
name: string;
type: string;
primaryKey?: boolean;
unique?: boolean;
required?: boolean;
default?: unknown;
index?: boolean;
maxLength?: number;
min?: number;
max?: number;
/** 外键引用 */
references?: string;
/** 级联删除 */
onDelete?: 'CASCADE' | 'SET NULL' | 'RESTRICT';
/** 级联更新 */
onUpdate?: 'CASCADE' | 'SET NULL' | 'RESTRICT';
}
interface CreateTableStatement {
type: 'CREATE_TABLE';
name: string;
columns: ASTColumnDef[];
/** IF NOT EXISTS — 表已存在时不报错 */
ifNotExists?: boolean;
}
interface DropTableStatement {
type: 'DROP_TABLE';
name: string;
/** IF EXISTS — 表不存在时不报错 */
ifExists?: boolean;
}
/** EXPLAIN 查询计划 */
interface ExplainStatement {
type: 'EXPLAIN';
query: Statement;
}
interface InsertStatement {
type: 'INSERT';
into: string;
columns?: string[];
/** VALUES 字面量 */
values?: unknown[][];
/** INSERT INTO ... SELECT ...v0.3.0 */
select?: SelectStatement | SelectUnionStatement;
}
interface UpdateStatement {
type: 'UPDATE';
table: string;
sets: Record<string, unknown>;
where: WhereCondition;
}
interface DeleteStatement {
type: 'DELETE';
from: string;
where: WhereCondition;
}
interface SelectStatement {
type: 'SELECT';
columns: ColumnRef[];
distinct?: boolean;
from: string;
/** v0.4.0: FROM (SELECT ...) 派生表(存在时 from 为占位,行源取此子查询结果) */
fromSubquery?: SelectStatement | SelectUnionStatement;
/** 主表别名 */
alias?: string;
/** JOIN 子句列表 */
joins?: JoinClause[];
where: WhereCondition;
/** GROUP BY */
groupBy?: string[];
/** HAVING */
having?: WhereCondition;
orderBy?: OrderBy[];
limit?: number;
offset?: number;
}
interface SelectUnionStatement {
type: 'SELECT_UNION';
/** 左操作数(可以是 SELECT 或嵌套 UNION */
left: SelectStatement | SelectUnionStatement;
/** 右操作数 */
right: SelectStatement | SelectUnionStatement;
/** UNION ALL 不去重 */
all?: boolean;
/**
* v0.8.0A26):复合查询**整体**的 ORDER BY / LIMIT / OFFSET。
*
* SQL 标准里这三者作用于整个 UNION 结果,而不是最后一个 SELECT。
* 此前 AST 没有这三个字段,parser 把它们挂在了 UNION 右侧的 SELECT 上 ——
* 于是 `A UNION B ORDER BY id DESC` 只对 B 排序、`... LIMIT 3` 只截断 B
* (实测 `SELECT id FROM t UNION SELECT id FROM t LIMIT 3` 返回 4 行)。
*/
orderBy?: OrderBy[];
limit?: number;
offset?: number;
}
interface AlterTableStatement {
type: 'ALTER_TABLE';
name: string;
action: 'ADD' | 'DROP';
column: ASTColumnDef;
}
interface TruncateTableStatement {
type: 'TRUNCATE_TABLE';
name: string;
}
interface CreateIndexStatement {
type: 'CREATE_INDEX';
/** 索引名(语法占位) */
name: string;
table: string;
column: string;
/** UNIQUE 索引 */
unique?: boolean;
}
interface DropIndexStatement {
type: 'DROP_INDEX';
name: string;
table: string;
column: string;
}
interface BeginTransactionStatement {
type: 'BEGIN';
}
interface CommitTransactionStatement {
type: 'COMMIT';
}
interface RollbackTransactionStatement {
type: 'ROLLBACK';
}
/** SAVEPOINT name / ROLLBACK TO SAVEPOINT name / RELEASE SAVEPOINT name */
interface SavepointStatement {
type: 'SAVEPOINT';
name: string;
/** SAVE = 创建;ROLLBACK = 回滚到;RELEASE = 释放 */
action: 'SAVE' | 'ROLLBACK' | 'RELEASE';
}
/** ANALYZE TABLE name — 收集表统计信息 */
interface AnalyzeStatement {
type: 'ANALYZE';
table: string;
}
/** REINDEX TABLE name — 重建表二级索引 */
interface ReindexStatement {
type: 'REINDEX';
table: string;
}
/** VACUUM — 压缩 LSM + 清理碎片 */
interface VacuumStatement {
type: 'VACUUM';
}
type Statement = SelectStatement | SelectUnionStatement | ExplainStatement | InsertStatement | UpdateStatement | DeleteStatement | CreateTableStatement | DropTableStatement | AlterTableStatement | TruncateTableStatement | CreateIndexStatement | DropIndexStatement | BeginTransactionStatement | CommitTransactionStatement | RollbackTransactionStatement | SavepointStatement | AnalyzeStatement | ReindexStatement | VacuumStatement;
/**
* metona-sqlark Query Executor — AST 执行器
* @module query/executor
*
* JOIN / GROUP BY / DISTINCT 逻辑在此层处理。
*/
/**
* 一条 SELECT 的执行形态判定结果。
*
* 由 {@link QueryExecutor.analyzeSelect} 统一产出,供 executor 自身与
* `core.queryStream` 共享 —— 避免"两条入口各写一套规则"导致的语义漂移。
*/
interface SelectExecutionShape {
/** 有 GROUP BY */
hasGroupBy: boolean;
/** 无 GROUP BY 但 SELECT 列表含聚合函数 */
hasAggregate: boolean;
/** 含 JOIN */
isJoinQuery: boolean;
/** 需要原始行(SELECT 列或 WHERE 含 CASE 表达式) */
needsRawRows: boolean;
/** ORDER BY 引用了 SELECT 别名(投影后才存在) */
orderByAlias: boolean;
/** SELECT 列含 `col AS alias` */
hasSelectAlias: boolean;
/** LIMIT/OFFSET 可安全下推给引擎(否则由 executor 末尾应用一次) */
limitPushdownSafe: boolean;
/** 引擎层投影与 executor 投影语义等价(列均为裸列引用) */
engineEquivalentProjection: boolean;
/** 可直连引擎 findStream 做真流式(无任何改变行集合/行序/行形状的阶段) */
streamable: boolean;
}
declare class QueryExecutor {
private engine;
private maxRowsPerQuery;
constructor(engine: IStorageEngine, maxRowsPerQuery?: number);
/**
* 执行一条语句。
*
* v0.8.0**同步抛错也必须表现为 rejected promise**。
*
* `db.query()` 是 async 的,但 `async` 只把**函数体内**的同步抛错转成 rejection
* 这里 `return this.executeXxx(stmt)` 的调用发生在 async 函数的同步前导段,
* 若被调方法在**进入第一个 await 之前**就抛错(例如 INSERT 的 arity 校验、
* VALIDATION_ERROR),异常会穿过 async 边界成为**同步抛出**:
* await expect(db.query(...)).rejects.toMatchObject(...) // 断言不生效,测试崩
* db.query(...).catch(...) // 永远不执行
* 对调用方而言这是不可预期的 —— 同一个 API 有的错误走 catch、有的走 try。
* 因此这里显式包一层 try/catch 统一成 rejection`async` 方法里 `throw`
* 一定产出 rejected promise)。
*/
execute(stmt: Statement): Promise<unknown>;
private dispatch;
/**
* 递归执行 UNION / UNION ALL,返回合并结果。
*
* v0.8.0A26):尾部 ORDER BY / LIMIT / OFFSET 作用于**整个**复合结果。
*
* 此前 `SelectUnionStatement` 没有这三个字段,parser 把它们挂在右侧 SELECT 上
* —— 于是 `A UNION B ORDER BY id DESC` 只排 B(实测返回 1,2,4,3),
* `A UNION B LIMIT 3` 只截断 B(实测返回 4 行)。现在由本方法在合并 + 去重
* **之后**统一排序与截断(顺序:合并 → 去重 → 排序 → OFFSET/LIMIT)。
*/
private executeSelectUnion;
/**
* 复合结果排序:把 ORDER BY 项解析为结果行的键。
*
* 支持两种写法(与单表 SELECT 一致):
* - 输出列序号:`ORDER BY 1` → 第 1 个输出列(SQL 标准,UNION 场景最常见,
* 因为各分支的输出列名可能不同);
* - 输出列名:`ORDER BY id` → 结果行的 `id` 键。
* 引用不存在的列时返回原序(不静默丢弃排序 —— 排序键缺失本身不改变行集合,
* 但会让用户以为已排序;故此处抛 COLUMN_NOT_FOUND,与 SELECT 路径口径一致)。
*/
private orderCompoundResult;
/**
* 执行一个 SELECT 部件(含 UNION)。
*
* @param purpose 透传给 `executeSelect` —— 作为写语句的输入行源时必须传
* `'source'`,否则 `maxRowsPerQuery` 会在写入前静默截断行源(A29)。
*/
private executeSelectPart;
/** 将 UNION 右侧行投影为左侧列结构(按位置取值) */
private projectUnionRow;
/** EXPLAIN: 输出查询计划 */
private executeExplain;
/**
* v0.8.0: SELECT 语句的**执行形态分析**(单一事实来源)。
*
* 为什么把它独立出来:core.queryStream 此前在 core.ts 里**自己重新推导**了一遍
* "这条 SELECT 能不能走引擎快路径、列投影怎么算",与 executor 的规则各写一份,
* 于是两者漂移出四类静默不一致(实测):
* SELECT id AS x FROM t query=[{x}] stream=[{id,v}](全列 + 原列名)
* SELECT t.id FROM t query=[{id}] stream=[{}](空对象)
* ... LIMIT 2 OFFSET 1 query=1 行 stream=2 行(引擎与 executor 各切一次)
* ... LIMIT 0 query=[] stream=1 行
*
* 现在由 executor 提供唯一判定,core 只消费结论,不再复制规则。
*/
analyzeSelect(stmt: SelectStatement): SelectExecutionShape;
/**
* @param purpose `'result'`(默认)= 结果交付给用户,受 `maxRowsPerQuery` 截断;
* `'source'` = 作为写语句的输入行源,**不得**截断(见下方说明)。
*/
private executeSelect;
private executeJoinSelect;
private prefixRow;
/**
* v0.4.1: 提取可下推的 WHERE 条件 — 主表别名前缀的普通条件(如 o.user_id = '1')。
* 下推到引擎可走二级索引;$col/$subquery/$and/$or/$not 等复杂条件保守不下推。
*/
private extractPushableWhere;
/**
* 哈希连接(v0.3.2 单等值 / v0.4.0 多列等值):
* ON 为等值条件(单列或多列)且右表任一列为索引/主键时,
* 收集左表连接值 → 一次 $in 查询右表 → 哈希映射匹配。
* 替代嵌套循环,大表 INNER/LEFT JOIN 复杂度 O(N + M)。
* 不适用时返回 null(回退嵌套循环)。
*/
private tryHashJoin;
/** 嵌套循环连接(优化:避免 ON 时对象扩散) */
private joinRows;
/**
* 分组聚合。
*
* v0.8.0A22/A23)两项行为修正,都源于**分组语义只有一个定义**这一原则:
*
* 1. **GROUP BY 可用 SELECT 别名**A22)。
* SQL 标准允许 `SELECT g AS grp, COUNT(*) FROM t GROUP BY grp`。
* 此前 `grp` 直接当列名去 `row['grp']` 取到 undefined → 全部行落入同一组;
* 更糟的是后续投影阶段发现 `g` 不在输出行里,抛
* `COLUMN_NOT_FOUND Unknown column "g" in SELECT list` —— 用户看到的是
* 一个与真正原因(GROUP BY 引用别名)无关的错误。
* 这里先把 GROUP BY 项解析为**基列**(别名 → 其源表达式),再做分组与输出。
*
* 2. **HAVING 可用未出现在 SELECT 里的聚合**(A23)。
* `SELECT g FROM t GROUP BY g HAVING SUM(n) > 25` 是标准写法。
* 此前聚合只在 `stmt.columns` 上计算,`SUM(n)` 从未被求值 →
* HAVING 的键 `SUM(n)` 在行里不存在 → matchWhere 取到 undefined →
* UNKNOWN → **空结果**`HAVING SUM(n) > 25` 返回 [],而 MAX(n) > 25 同样 [])。
* 现在把 SELECT 列与 HAVING 里出现的聚合**并集**一起算进分组行,再交给
* HAVING 过滤。多算的聚合只影响中间行,最终输出仍严格按 SELECT 列投影,
* 因此不会泄漏额外列。
*/
private executeGroupBy;
/** 对全部分组行做输出投影(HAVING 之后调用) */
private projectGroupedRows;
/**
* 把分组行收缩为 SELECT 列表要求的输出列(保持 SELECT 顺序)。
*
* GROUP BY 路径不做通用的 `projectRow`(那会丢掉聚合值),因此需要这一层显式投影。
* 处理四类列:
* - `*`:保留分组行全部键(`SELECT * ... GROUP BY g` 的既有语义);
* - 聚合表达式:键取 `parseAggregateExpression` 的 outputKey
* - `expr AS alias`:输出 alias,值从分组行按 expr 取(含二义性由取值函数处理);
* - 裸列引用:输出剥离别名前缀后的基列名。
*/
private projectGroupedRow;
/**
* GROUP BY 项解析为基列:`GROUP BY grp`grp 是 SELECT 别名)→ `g`。
*
* 只解析"SELECT 列表里带 AS 别名"与"CASE ... AS 别名"这两种可追踪形态;
* 其余原样返回(真正的列名或表达式)。同名歧义时保持原样 —— 后续
* `resolveColumnValue` 的 strict 模式会给出明确的 COLUMN_NOT_FOUND/歧义错误。
*/
private resolveGroupByColumns;
/**
* 收集本次分组需要计算的全部聚合表达式(SELECT 列 HAVING),按 exprKey 去重。
*
* 同时扫描 HAVING 是 A23 的核心:`HAVING SUM(n) > 25` 里的 `SUM(n)` 必须被求值,
* 否则 HAVING 阶段取不到该键。
*/
private collectAggregateExpressions;
/**
* 计算单个聚合值。
*
* v0.8.0: 返回类型放宽为 unknown —— SUM/AVG/MIN/MAX 对空集返回 nullSQL 标准),
* COUNT 仍返回 number。最小/最大改为单次遍历(不再展开实参,消除栈溢出)。
*/
private computeAggregate;
/**
* DISTINCT 聚合(`COUNT(DISTINCT col)` / `SUM(DISTINCT col)`)。
*
* v0.8.0A25):独立成函数而不是在 computeAggregate 里加分支 —— 去重键
* 必须用 `encodeValueKey`(类型安全),而"对原始值去重"COUNT)与
* "对数值化后去重"(SUM)用的键不同,混在一个函数里正是此前
* `String(v)` 与 `JSON.stringify(v)` 两套编码并存的原因。
*/
private computeDistinctAggregate;
private executeDistinct;
private executeInsert;
/**
* v0.8.0(A29):写路径的行数上限保护。
*
* `maxRowsPerQuery` 此前只在 SELECT 的返回处生效(`executeSelect` 末尾切片),
* 而 `INSERT INTO dst SELECT * FROM huge_src` 的**中间结果集**完全不受约束 ——
* 它由 `executeSelectPart` 直接产出并逐行写入,既不切片也不报错。
* 于是"防止一次查询把浏览器内存打满"这一配置项在最容易打满内存的路径上失效。
*
* 这里选择**报错**而不是静默截断:静默只写一部分行会让用户以为全部写完
* (又一次"写路径静默丢数据")。错误里给出上限值与来源,便于用户改配置或
* 改写查询。
*/
private assertWithinRowLimit;
private executeUpdate;
private executeDelete;
/**
* v0.7.4: 写语句(UPDATE/DELETEWHERE 的子查询解析。
* 非关联子查询($subquery)解析为具体值列表/标量;
* 关联引用($col / 关联 EXISTS)在写语句中无法逐行绑定外层上下文
* (引擎层 matchWhere 无 $col 绑定选项)→ 显式 NOT_SUPPORTED 而非静默 0 行。
*/
private resolveWriteWhere;
private executeCreateTable;
private executeDropTable;
private executeAlterTable;
private executeTruncateTable;
private executeCreateIndex;
private executeDropIndex;
private executeBegin;
private executeCommit;
private executeRollback;
/** SAVEPOINT name / ROLLBACK TO SAVEPOINT name / RELEASE SAVEPOINT name */
private executeSavepoint;
/** ANALYZE TABLE name — 收集表统计信息 */
private executeAnalyze;
/** REINDEX TABLE name — 重建表二级索引 */
private executeReindex;
/** VACUUM — 压缩 LSM + 清理碎片 */
private executeVacuum;
/**
* SELECT 列表的**输出列名**集合(投影后行里会出现的键)。
*
* 与 `projectRow` 的键规则保持一致:
* - `*` → 未知(返回 `null` 表示"无法判定",调用方按"包含"处理,避免误判需要原始列);
* - `expr AS alias` → `alias`
* - 聚合 `FUNC(arg) [AS alias]` → `alias` 或表达式原文;
* - CASE `... AS alias` → `alias`
* - 字符串/数字常量列 → 表达式原文;
* - 裸列引用 → 剥离别名前缀后的列名。
*/
private outputColumnNames;
/** ORDER BY 的键是否全部能在**输出列**里找到(决定排序发生在投影前还是投影后) */
private orderByReferencesOutputColumns;
/**
* DISTINCT 是否必须在投影**前**执行。
*
* 仅当 ORDER BY 引用了不在输出列里的列时成立:`SELECT DISTINCT dept FROM e ORDER BY v`
* 需要先按 `v` 排序、再按输出列 `dept` 去重。若把 DISTINCT 放到投影后,
* `v` 已被丢弃,排序无从进行(会报 COLUMN_NOT_FOUND)。
*
* SQL 标准禁止这种写法;此处保留既有语义(排序后去重),并把该例外显式记录,
* 而不是让 DISTINCT 的位置在所有情况下都"碰巧"由排序决定。
*/
private distinctNeedsPreProjectionSort;
/**
* v0.8.0A37):校验 JOIN ON 里引用的列在**参与连接的两张表**之一存在。
*
* `ON a.x = b.y` 的 `a.x` 属于主表或已有 JOIN 表,`b.y` 属于当前 JOIN 表 ——
* 两侧都要能找到归属;否则报 COLUMN_NOT_FOUND(而不是让连接静默产生空结果)。
* 裸列名只要求"某一侧存在"(`ON k = k` 的既有语义是取主表列)。
*/
private validateJoinOnColumns;
/**
* v0.8.0B-4):校验 SELECT / GROUP BY / HAVING / ORDER BY 里 CASE 表达式引用的列存在。
*
* 为什么单独一个方法:CASE 可以出现在四个子句里,而每个子句的校验时机不同
* WHERE 有 `assertWhereColumnsExist`,投影有 `assertProjectionColumnsExist`)。
* 统一在这里按 **schema** 收集可见列,与行形状解耦,避免"分组后校验不到源列"。
*/
private assertCaseColumnsExist;
/**
* v0.8.0A37):校验 WHERE 中出现的列名都存在于行源。
*
* 覆盖两类引用:
* - **键位**`WHERE x = 1` 的 `x`
* - **`$col` 值位**`WHERE x = y` 的 `y`parser 生成 `{ x: { $eq: { $col: 'y' } } }`),
* 含 `$and`/`$or`/`$not` 内部的操作数槽。
*
* 派生表与子查询的行源:跳过(其列来自子查询投影,需要单独解析;
* 由 `normalizeUnprefixedReferences` + 运行时行形状决定)。
* JOIN:两侧的表别名都要参与判定 —— `t.x` 与另一表的 `u.x` 都属于合法引用。
*
* 之所以必须是**显式错误**而不是依赖运行时 UNKNOWN:`$col` 取不到值时
* 三值求值器只能返回 UNKNOWN,而 UNKNOWN 在 WHERE 里表现为"不保留该行" ——
* 用户看到的是"没有匹配数据",与"列名拼错"完全无法区分。
*/
private assertWhereColumnsExist;
/**
* 通用的 WHERE 侧列引用校验(SELECT 的 WHERE 与 JOIN 的 ON 共用)。
*
* @param context 错误消息中的位置描述('WHERE' / 'JOIN ON'
* @param rejectAmbiguous JOIN 场景下裸列名被多表共有 → 报歧义(SQL 标准要求限定)。
* **JOIN ON 传 false**`ON a.x = b.x` 里两侧的限定名各自
* 合法,而 `ON k = k` 这种裸写法在 JOIN 语境下按"取主表列"
* 解释(与既有 joinRows 行为一致),不应因为"两表都有 k"
* 就拒绝 —— 那会把常见的等值连接写法判为错误。
*/
private validateWhereColumns;
/**
* 列列表是否包含 CASE WHEN 表达式 */
private hasCaseColumn;
/**
* v0.3.3: ORDER BY 是否引用 SELECT 别名(如 `SELECT name AS n ... ORDER BY n`)。
* 别名列在引擎层投影前不存在,需投影后重新排序。
*/
/**
* v0.8.0B-5):SELECT 列表产出的**别名集合**`AS x` 与 `CASE ... AS x`)。
*
* 与 `orderByUsesSelectAlias` 共用同一套识别规则 —— 两处若各写一份,
* 会出现"排序认为它是别名、校验认为它是列"的矛盾(本项目反复出现的漂移模式)。
*/
private selectAliasNames;
private orderByUsesSelectAlias;
/** WHERE 是否包含 CASE WHEN 表达式键 */
private whereHasCase;
getEngine(): IStorageEngine;
/**
* v0.8.0: 校验 SELECT 列表中的**裸列引用**在结果行里确实存在,否则抛 COLUMN_NOT_FOUND。
*
* 为什么必须做:`SELECT bogus FROM t` 此前返回 `[{},{},...]`(行数对、内容空、无报错),
* 这是"静默错误结果"里最难被发现的一类 —— 调用方拿到的是结构正确但全空的表格。
*
* 判定规则(与 projectRow 的分类保持一致):
* - `*` 跳过;
* - 字符串/数字常量列跳过;
* - CASE 表达式跳过(其内部列引用由 evaluateCase 处理);
* - `expr AS alias`:字符串/数字常量跳过,否则取 `expr` 作为被引用列;
* - 其余视为裸列引用。
* 存在性检查允许两种形态:精确匹配,或**唯一**以 `.<col>` 结尾(JOIN 行以 `alias.col` 为键)。
* 若同一个后缀出现在多个表别名下则视为歧义,同样报错(符合"未限定列名歧义应报错"的语义)。
*
* 结果集为空时无法判定,此时跳过(空表 + 未知列不会误报)。
*/
private assertProjectionColumnsExist;
/**
* 列投影(v0.3.1):普通列走 projectColumnsCASE WHEN 表达式逐行求值;
* v0.3.3: 支持 `col AS alias` 列别名
*/
private projectRow;
/** 检查 SELECT 列列表中是否包含聚合函数(与执行路径共用同一解析器) */
private _hasAggregateColumn;
/**
* 计算单行聚合结果(无 GROUP BY)。
*
* v0.8.0(A25):聚合识别与取值改为与 GROUP BY 路径**共用**
* `parseAggregateExpression` / `resolveColumnValue` —— 此前这里有第二份正则,
* 于是 `COUNT (n)`(函数名后有空格)在"是否聚合"判定与"如何求值"两处结论不同。
*/
private computeSingleAggregate;
/**
* v0.8.0B-5):把 ORDER BY / GROUP BY 里的**输出列序号**解析为输出列名。
*
* SQL 标准允许 `ORDER BY 1` / `GROUP BY 2` 按输出列位置引用(UNION 各分支
* 列名可能不同,只能按序号引用)。规则:
* 1. 若存在**同名的真实列**(如列名就是 `"1"`,用双引号建表),按列名优先 ——
* 显式标识符胜过位置简写;
* 2. 纯数字 → 第 N 个输出列的**表达式原文**`SELECT n AS num ... ORDER BY 1`
* 解析为 `num`,因为投影后行里只有 `num`);
* 3. 序号越界 → `QUERY_ERROR`(不说"未知列",因为问题出在位置而不是名字);
* 4. `GROUP BY <序号>` 指向聚合表达式 → `QUERY_ERROR`
* (按聚合值分组语义上不成立,SQL 标准同样禁止)。
*/
private resolveOutputOrdinals;
/**
* 归一化"行键不带前缀"的查询中的所有引用 —— 剥离表别名前缀。
*
* v0.8.0(A36):抽成单一实现,因为**两条**路径需要同一规则:
* - 非 JOIN 单表(行键是裸列名,`WHERE u.age` 要变成 `age`);
* - 派生表非 JOIN`FROM (SELECT ...) AS d`,行键来自子查询投影,同样无前缀)。
* 此前只有前者做归一化,后者完全没做 → `SELECT d.id FROM (...) AS d` 静默空结果,
* 而同义的 `SELECT id FROM (...) AS d` 正确。
*
* 覆盖 WHERE(含 `$col` 嵌套引用)/ ORDER BY / GROUP BY / SELECT 四类引用。
* 聚合表达式与 CASE 表达式**整体跳过**(其内部的列引用由各自的求值器处理,
* 而它们的求值器现在都走统一的 `resolveColumnValue`,本身支持前缀)。
*/
private normalizeUnprefixedReferences;
/** 剥离主表别名前缀:'u.id' → 'id'(键与 $col 值均处理,支持多层别名) */
private normalizeWhereColumns;
private normalizeExistsValue;
private normalizeFieldValue;
private stripAlias;
/** WHERE 是否含关联引用($col 或关联 EXISTS)或 CASE WHEN 表达式键 */
private hasCorrelatedRefs;
private fieldHasColRef;
/**
* 构造**引擎层预过滤**用的 WHERE 子句。
*
* 逐行求值的谓词必须整体移出引擎层,否则引擎的 `matchWhere`(没有外层行上下文)
* 会把它们判为 UNKNOWN → **所有行被过滤掉**,逐行求值再正确也无行可算:
* - 关联 `EXISTS``$exists` 子查询未执行,`{ $subquery: ... }` 引擎无法求值;
* - CASE WHEN 表达式键:需要行上下文才能算出布尔;
* - `$col` 列引用(`WHERE t.x = t.y` → `{ x: { $eq: { $col: 'y' } } }`):
* 引擎层取不到"另一列"的值。
*
* 移出的粒度取决于连接词 —— 这里**不是**保守兜底,而是逻辑上唯一正确的做法:
* - 顶层 / `$and` 的成员:可以单独删除该谓词,其余谓词仍然安全可下推;
* - `$or` / `$not` 的成员:不能单独删除。删掉 `A OR B` 中的 `B` 会得到更严的
* `A`**漏行**);删掉 `NOT B` 中的 `B` 会得到恒真的 `NOT true`**多行**)。
* 因此整条 `$or` / `$not` 都交给逐行求值(`$not` 的恒真情形直接丢弃该键)。
*/
private enginePreFilter;
/**
* 该 WHERE 片段是否可完全交给引擎层求值(无 `$col` / 关联 `EXISTS` / CASE 键)。
*
* 与 `hasCorrelatedRefs` 的区别:`hasCorrelatedRefs` 回答"是否需要逐行求值",
* 本函数回答"能否整体下推"。两者互补,缺一不可 —— 后者是前者在 `$or`/`$not`
* 内部传播后的结果。
*/
private isEngineEvaluable;
/** 字段条件里是否含未解析子查询(引擎层无法执行) */
private fieldHasSubquery;
/** 逐行绑定外层行上下文,求值关联 EXISTS、$col 引用与 CASE WHEN 键 */
private filterCorrelated;
/** 将 WHERE 中的 CASE WHEN 表达式键求值为布尔条件($caseResult */
private resolveCaseKeys;
/** CASE 求值结果与操作符条件比较 */
private caseConditionMatches;
/** 将 where 中的 $col 引用替换为上下文行值 */
private bindColumnRefs;
/**
* v0.8.0A10):在外层行里取 `$col` 引用的值。
*
* 关键点:引用可能带外层表名/别名前缀(`u.id`),而外层行键是**不带前缀**的
* (非 JOIN 路径会先剥离)。因此这里必须先剥前缀再取值 —— 否则 `u.id` 取到
* `undefined`,被 `?? null` 静默变成 null,子查询返回空集
* (实测 `WHERE id IN (SELECT user_id FROM o WHERE o.user_id = u.id)` 静默空结果)。
*/
private lookupOuterValue;
private bindWhereRefs;
/**
* 递归扫描 WHERE 条件,找到 $subquery 标记并执行子查询,
* 将结果替换为具体值。
* @param contextRow 关联子查询的外层行上下文(用于绑定 $col 引用)
*/
private resolveSubqueries;
/**
* 解析操作符值中嵌套的子查询
*/
/**
* 解析字段条件里的子查询。
*
* v0.8.0 根治(A10):接收外层行上下文并对子查询内的关联引用做绑定。
* 此前完全不传 contextRow —— 于是 `WHERE id IN (SELECT user_id FROM o WHERE o.user_id = u.id)`
* 里的 `u.id` 绑定为 undefined,子查询返回空集,最终 `$in: []` → **静默空结果**
* (而结构相同的 EXISTS 因为走另一条分支是正常的 —— 又一处"同类逻辑两条路径")。
*/
private resolveOperatorSubqueries;
}
/**
* metona-sqlark Query Builder — 链式查询构建器
* @module query/builder
*
* ============================================================================
* v0.8.0B-3):**只产出 AST,执行一律交给 Executor**
* ============================================================================
* 修复前每个 builder 的 `execute()` 都有自己的执行动作:
* - `SelectQueryBuilder`**无 JOIN 时直接调 `engine.find`**,只有 JOIN 才走
* executor
* - `UpdateQueryBuilder` / `DeleteQueryBuilder`:直接调 `engine.update/delete`。
*
* 于是"同一条语义"在 builder 路径与 SQL 路径上走两条管线,规则各写一份:
* - SELECT 的常量列/别名/CASE 投影在 builder 路径不存在
* `db.table('t').select(['t.n'])` 的输出键、`SELECT 1` 之类行为不同);
* - `maxRowsPerQuery`、`distinct`、`groupBy`/`having`、`fromSubquery` 等阶段
* 在直通路径上完全不经过;
* - 未解析的 `$subquery`/`$col` 在直通路径上无人解析 → 引擎判 UNKNOWN →
* **静默 0 行**(这正是 v0.7.4 要为此加"显式拒绝"防御的原因)。
*
* 现在 builder 只负责"拼 AST",执行统一走 `executor.execute(ast)`
* 需要直通时由 executor 内部判断(它本来就是唯一知道"能不能下推"的地方)。
*/
declare class SelectQueryBuilder {
private tableName;
private _columns;
private _where;
private _orderBy;
private _limit?;
private _offset?;
private _joins;
private _alias?;
private _executor;
constructor(tableName: string, executor: QueryExecutor, _columns?: string[]);
/** 主表别名 */
as(alias: string): this;
/** INNER JOIN */
innerJoin(table: string, on: WhereCondition, alias?: string): this;
/** LEFT JOIN */
leftJoin(table: string, on: WhereCondition, alias?: string): this;
/** RIGHT JOIN */
rightJoin(table: string, on: WhereCondition, alias?: string): this;
/** CROSS JOIN */
crossJoin(table: string, alias?: string): this;
/** 通用 JOIN */
join(table: string, on: WhereCondition, alias?: string): this;
private _addJoin;
/** 添加过滤条件 */
where(condition: WhereCondition): this;
/** 排序 */
orderBy(column: string, direction?: SortDirection): this;
/** 限制返回条数 */
limit(n: number): this;
/** 偏移量 */
offset(n: number): this;
/**
* 执行查询 —— 拼出 AST 后交给 Executor**唯一**执行管线)。
*
* 注意不要再为"无 JOIN"加一条 `engine.find` 快路:那条路会绕过
* 投影/列校验/LIMIT 下推判定/maxRowsPerQuery,从而与 `db.query()` 给出不同结果
* (B-3 修复前的实际状态)。executor 自己会在安全时下推到引擎,不需要 builder 代劳。
*/
execute(): Promise<Record<string, unknown>[]>;
/** 获取 AST */
toAST(): SelectStatement;
}
declare class UpdateQueryBuilder {
private tableName;
private _updates;
private executor;
/**
* TABLE API 的生命周期回调(`beforeUpdate` / `afterUpdate` / onWrite 广播)。
*
* 为什么由 `Table` 注入而不是 builder 自己触发:builder 的职责是**产出 AST**
* 它不该知道钩子/广播的存在(否则又会像修复前那样"builder 顺带把写入也做了"
* 从而绕过 Executor)。钩子被包裹在**唯一管线**之外,
* 顺序与修复前完全一致:before → executor → onWrite → after。
*
* 回调接收**实际执行的语句**(含 builder 上累积的 where),
* 而不是构造 builder 时的空 where —— 后者会让 `beforeUpdate` 的
* `query.where` 永远是 `{}`(钩子拿不到过滤条件,等于信息缺失)。
*/
private hooks?;
private _where;
constructor(tableName: string, _updates: Record<string, unknown>, executor: QueryExecutor,
/**
* TABLE API 的生命周期回调(`beforeUpdate` / `afterUpdate` / onWrite 广播)。
*
* 为什么由 `Table` 注入而不是 builder 自己触发:builder 的职责是**产出 AST**
* 它不该知道钩子/广播的存在(否则又会像修复前那样"builder 顺带把写入也做了"
* 从而绕过 Executor)。钩子被包裹在**唯一管线**之外,
* 顺序与修复前完全一致:before → executor → onWrite → after。
*
* 回调接收**实际执行的语句**(含 builder 上累积的 where),
* 而不是构造 builder 时的空 where —— 后者会让 `beforeUpdate` 的
* `query.where` 永远是 `{}`(钩子拿不到过滤条件,等于信息缺失)。
*/
hooks?: {
before?: (stmt: UpdateStatement) => Promise<void>;
after?: (count: number, stmt: UpdateStatement) => Promise<void>;
} | undefined);
where(condition: WhereCondition): this;
/**
* 执行更新 —— 走 Executor(唯一的写管线)。
*
* 修复前这里直接调 `engine.update``$subquery` / `$col` / `$exists` 无人解析,
* 引擎层 matchWhere 判 UNKNOWN → **静默影响 0 行**(返回 0 且无报错)。
* 引擎层为此加过"检测未解析标记就抛 NOT_SUPPORTED"的防御 —— 那是把
* "管线缺失"暴露成用户错误;正确做法是把请求送进唯一管线。
*/
execute(): Promise<number>;
toAST(): UpdateStatement;
}
declare class DeleteQueryBuilder {
private tableName;
private executor;
/** TABLE API 生命周期回调,语义见 `UpdateQueryBuilder` 的说明 */
private hooks?;
private _where;
constructor(tableName: string, executor: QueryExecutor,
/** TABLE API 生命周期回调,语义见 `UpdateQueryBuilder` 的说明 */
hooks?: {
before?: (stmt: DeleteStatement) => Promise<void>;
after?: (count: number, stmt: DeleteStatement) => Promise<void>;
} | undefined);
where(condition: WhereCondition): this;
/** 执行删除 —— 走 Executor(同 UpdateQueryBuilder.execute 的理由) */
execute(): Promise<number>;
toAST(): DeleteStatement;
}
declare class Table<T = Record<string, unknown>> {
readonly name: string;
private engine;
private schema;
private executor;
/** 写入回调(多标签页广播,v0.3.2) */
private onWrite?;
/** v0.5.1: CRUD 生命周期钩子触发回调(beforeInsert/afterInsert/... 真实接线) */
private onHooks?;
constructor(engine: IStorageEngine, tableName: string, executor?: QueryExecutor, onWrite?: (table: string) => void, onHooks?: (hook: HookName, args: unknown[]) => Promise<void>);
getSchema(): Promise<TableSchema>;
insert(row: T & Record<string, unknown>): Promise<string>;
insertMany(rows: (T & Record<string, unknown>)[]): Promise<string[]>;
select(columns?: string[]): SelectQueryBuilder;
/**
* v0.8.0(B-3):取执行器;缺失即**明确失败**。
*
* 修复前 builder 在拿不到 executor 时会退化为"直接调引擎" —— 于是
* `db.table('t')` 与 `db.query()` 两条路径的语义不同(投影/列校验/LIMIT 下推/
* maxRowsPerQuery 在直通路径上全部缺失)。现在只保留一条管线:
* 没有 executor 就没有可用的查询 API,显式报错而不是悄悄降级。
*/
private requireExecutor;
/** v0.4.0: 流式查询 — 逐行回调,不物化全部结果 */
stream(onRow: (row: T & Record<string, unknown>) => void, query?: {
where?: Record<string, unknown>;
limit?: number;
offset?: number;
columns?: string[];
}): Promise<number>;
/**
* v0.8.0B-3):写操作也走**唯一管线**(Executor),生命周期钩子由本方法注入。
*
* 修复前 `UpdateQueryBuilder` 直接调 `engine.update``$subquery`/`$col` 无人解析
* → 引擎判 UNKNOWN → 静默影响 0 行;`beforeUpdate`/`afterUpdate` 的触发点也因此
* 与 SQL 路径不同(一条在 builder 里、一条在 core 里)。
*/
update(updates: Partial<T> & Record<string, unknown>): UpdateQueryBuilder;
delete(): DeleteQueryBuilder;
count(where?: Record<string, unknown>): Promise<number>;
clear(): Promise<void>;
drop(): Promise<void>;
}
/**
* metona-sqlark Transaction — 事务管理
* @module transaction
*
* v0.1.13: 支持真正的回滚 — 利用引擎层 begin/commit/rollback 实现原子性。
*/
declare class Transaction {
private engine;
private tables;
private completed;
/**
* v0.8.0(B-3):事务内的表操作同样走**唯一执行管线**。
*
* `Table` 的 select/update/delete 现在必须拿到 executorbuilder 只产出 AST),
* 因此这里构造一个绑定到同一引擎的执行器 —— 事务的原子性由**引擎**提供
* begin/commit/rollback 作用在引擎上),执行器只是把 AST 翻译成引擎调用,
* 两者组合即可保持"事务内写入可回滚"这一语义不变。
*/
private executor;
constructor(engine: IStorageEngine);
/** 获取表操作对象 */
table(tableName: string): Table;
/** 标记事务完成(由 TransactionManager 调用) */
_markCompleted(): void;
/** 是否已完成 */
isCompleted(): boolean;
}
declare class TransactionManager {
private engine;
constructor(engine: IStorageEngine);
/** 执行事务 — 支持自动回滚 */
execute<T>(fn: (trx: Transaction) => Promise<T>): Promise<T>;
}
/** 变更类型(与 site/docs.html 承诺的 `event.type` 对齐) */
type ChangeType = 'insert' | 'update' | 'delete' | 'clear' | 'ddl' | 'external';
/** 变更事件 */
interface ChangeEvent {
/** 变更类型 */
type: ChangeType;
/** 表名 */
table: string;
/** 受影响的行(可用时提供) */
row?: Record<string, unknown>;
/** 受影响行的主键(可用时提供) */
key?: string;
/** 受影响行数 */
count?: number;
}
declare class MetonaSqlark {
/** 数据库名称 */
readonly name: string;
/**
* v0.8.0:释放一个连接引用(引用计数 -1,归零时自动关闭)。
*
* 由 `MetonaSqlark.connect()` 注入实现 —— 此前该方法是**运行时注入、类型上不存在**:
* README 与示例都在用 `await db.disconnect()`,但 `db` 的声明里没有它,
* TypeScript 使用者会直接编译失败(只能 `as any` 绕过)。
* 普通 `create()` 得到的实例没有这个方法,因此为可选:
* 只有经 `connect()` 取得的实例才有,直接调用会抛错(而不是静默无操作)。
*/
disconnect?: () => Promise<void>;
/**
* v0.8.0:连接池静态 API 的类型声明。
*
* 这些方法由 `src/connection-manager.ts` **运行时注入**`MetonaSqlark.connect = ...`)。
* 此前注入侧用 `as unknown as Record<string, unknown>` 绕过类型检查,
* 于是 README「连接池」一节里的 `MetonaSqlark.connect(...)` /
* `MetonaSqlark.disconnectAll()` 在 TypeScript 下全部报 TS2339
*"属性不存在"),使用者只能 `as any`。
*
* 声明为 `?` 可选是因为它们**只在 import 了 connection-manager 的构建里存在**
* 核心入口不 import 它(避免无谓的模块副作用)。真正常用的路径是
* `MetonaSqlark.create()`。
*/
static connect?: (config: DatabaseConfig) => Promise<MetonaSqlark>;
/** 按库名释放一个连接引用(等价于实例上的 `disconnect()` */
static disconnect?: (dbName: string) => Promise<void>;
/** 关闭全部连接 */
static disconnectAll?: () => Promise<void>;
/** 当前活跃连接名列表 */
static getActiveConnections?: () => string[];
/**
* v0.7.1: 静态工厂(与 connect/disconnect 同一入口风格)。
* 此前 create 仅存在于 api 对象 / window 挂载 —— README/站点示例的
* `MetonaSqlark.create({...})` 在 ESM/Node 下是 undefinedTypeError)。
*/
static create(config: DatabaseConfig): Promise<MetonaSqlark>;
/** 存储模式 */
readonly mode: string;
/** 版本号 */
private _version;
/** 获取版本号 */
get version(): number;
private engine;
private executor;
private transactionManager;
private pluginManager;
private config;
private ready;
private tableCache;
/** 查询结果行数上限 */
get maxRowsPerQuery(): number;
/** 调试模式 */
get debug(): boolean;
/** 多标签页同步通道(v0.3.2 */
private channel;
constructor(config: DatabaseConfig);
/** 初始化数据库(创建引擎、打开连接) */
init(): Promise<void>;
/** 检查是否就绪 */
isReady(): boolean;
/** 创建表 */
defineTable(name: string, columns: Record<string, ColumnDef>): Promise<void>;
/** 获取表操作对象 */
table(name: string): Table;
/** 删除表 */
dropTable(name: string): Promise<void>;
/** 获取所有表名 */
getTableNames(): Promise<string[]>;
/**
* 执行 SQL 字符串查询。
* v0.7.0: 支持位置参数(`?`)—— `db.query('SELECT * FROM t WHERE id = ?', ['1'])`。
* 参数按 SQL 字面量安全编码(字符串 '' 转义),杜绝 SQL 注入。
*/
query(sql: string, params?: unknown[]): Promise<unknown>;
/**
* 流式查询:逐行回调,不一次性物化全部结果(大表友好)。
* 支持简单 SELECTWHERE/LIMIT/OFFSET/列投影);
* JOIN/GROUP BY/UNION/聚合/ORDER BY 自动回退为物化查询后逐行回调。
*
* @example
* ```ts
* let total = 0;
* await db.queryStream('SELECT * FROM logs WHERE level = \'error\'', (row) => {
* total++;
* processRow(row);
* });
* ```
*/
/**
* 流式查询:逐行回调,尽量不物化全部结果(大表友好)。
*
* v0.8.0 根治:**快路径与物化路径的结果必须逐值相等**。
*
* 此前 core.ts 自己重写了一套"能不能走引擎快路径 / 列投影怎么算"的规则,
* 与 executor 的规则各写一份并发生漂移,实测四类静默不一致:
* SELECT id AS x FROM t → query 返回 [{x}]stream 返回 [{id,v}](全列 + 原列名)
* SELECT t.id FROM t → query 返回 [{id}]stream 返回 [{}](空对象)
* ... LIMIT 2 OFFSET 1 → query 1 行,stream 2 行
* ... LIMIT 0 → query 0 行,stream 1 行(Aria 又是 0 行,跨引擎也不同)
* 另:流式路径既不触发 beforeQuery/afterQuery 钩子,也不受 maxRowsPerQuery 约束。
*
* 现在的规则:
* 1. 是否可流式、如何投影,全部由 `executor.analyzeSelect()` 判定(单一事实来源);
* 2. 不可流式(以及任何不确定的情况)一律回退到 `query()` 物化后逐行回调 ——
* 这条路径天然与 `query()` 同语义,是正确性的兜底保证;
* 3. 快路径只覆盖"引擎层投影与 executor 投影语义等价"的简单 SELECT
* 4. 回调返回 Promise 时不再靠 `constructor.name` 猜(此前对普通函数返回 Promise
* 的情况完全失效),而是直接检测返回值并显式报错,避免 Promise 被静默丢弃。
*/
queryStream<T extends Record<string, unknown> = Record<string, unknown>>(sql: string, onRow: (row: T) => void): Promise<number>;
/**
* v0.8.0: 判断流式回调是否为 async(或声明返回 Promise)。
*
* 此前用 `onRow.constructor.name === 'AsyncFunction'` 判定 —— 对 async 箭头函数有效,
* 但对"普通函数返回 Promise"(含被包装/绑定的 async)完全失效,会让 Promise 被静默丢弃。
* 这里用**双条件**:既看是否声明为 async 函数(源码/转译后仍可识别),
* 也看其返回类型标注;两者任一成立即走物化 + await 路径。
*/
private isAsyncCallback;
/**
* v0.8.0: 剥离列引用上的主表别名前缀(`t.id` → `id`)。
* 与 executor 非 JOIN 路径的 `stripAlias` 语义保持一致。
*/
private stripAliasPrefix;
/** 流式查询用:剥离主表别名前缀(复用 query 路径的规范化逻辑) */
private normalizeWhereForStream;
/** 执行事务 */
transaction<T>(fn: (trx: Transaction) => Promise<T>): Promise<T>;
/** 导出表数据为 JSON */
exportTable(tableName: string): Promise<Record<string, unknown>[]>;
/** 导入 JSON 数据到表 */
importTable(tableName: string, data: Record<string, unknown>[]): Promise<string[]>;
/** 导出整个数据库为 JSON */
exportAll(): Promise<Record<string, Record<string, unknown>[]>>;
/**
* v0.5.1: 在线备份 — 导出全库数据。
*
* v0.8.0 修正表述:此前注释与 README 宣称"全库**一致性**快照",但引擎层
* 并没有跨表快照原语 —— 实现是**逐表读取**(Aria 走引擎级 `backup()`
* 其余引擎回退 `exportAll()`)。备份过程中的并发写入会让不同表来自不同
* 时间点(单表内部仍是一致的)。需要强一致时先 `close()`,或用
* `db.transaction()` 包住调用(事务期间并发写被 `TX_ACTIVE` 拒绝)。
* 真正的跨表快照需要 COW 行所有权改造,列入后续版本。
*/
backup(): Promise<Record<string, Record<string, unknown>[]>>;
/** 变更通知引擎(init 后可用);未初始化时为 null */
private notifier;
private listeners;
/**
* 订阅表变更。
*
* v0.8.0 修复:此前**本地写入永不触发** —— 全库唯一调用 `emit` 的地方在
* BroadcastChannel 收到其它标签页消息的分支里,因此 README「订阅表变更」与
* site/docs.html 的 `event.type: 'insert' | 'update' | 'delete'` 示例全都不成立。
* 现在本地写入(SQL / Table API / QueryBuilder / 事务内)都会产生事件。
*
* 现在返回的函数是**同步**退订函数(与既有 API 兼容)。
*/
subscribe(tableName: string, callback: (event: ChangeEvent) => void | Promise<void>): () => void;
/**
* 手动触发变更事件(保留为公开 API:自定义写入路径可显式通知订阅者)。
* 现在也支持 await —— 订阅者的 Promise 会被等待。
*/
emit(tableName: string, event: Partial<ChangeEvent> & {
type: ChangeEvent['type'];
}): Promise<void>;
/**
* v0.8.0: 派发"来自其它标签页"的变更事件。
* 只走本地订阅者,不触发 onBroadcast(避免 A↔B 互相转发的无限循环)。
*/
private emitExternal;
/** 内部:把一次变更同时派发给本地订阅者与跨标签页广播 */
private dispatchChange;
/** 广播表变更到其他标签页(多标签页同步) */
broadcastChange(tableName: string): void;
/** 写语句对应的表名(多标签页广播用) */
private writeStatementTable;
/**
* v0.5.1: SQL 写语句触发 CRUD 生命周期钩子。
* INSERT/UPDATE/DELETE 分别触发 beforeInsert/afterInsert、beforeUpdate/afterUpdate、
* beforeDelete/afterDelete(参数与 Table API 路径一致)。
*/
private triggerStatementHooks;
private migrations;
/** 注册迁移 */
addMigration(version: number, up: (db: MetonaSqlark) => Promise<void>): void;
/** 执行迁移到指定版本 */
migrateTo(targetVersion: number): Promise<void>;
/**
* 崩溃恢复自愈 — 校验并清理损坏数据、恢复一致性。
* 检测到异常后调用,无需删库重建。
*/
repair(): Promise<void>;
/**
* 清空全部数据与表结构(保留库本身)。
* 支持后续继续使用本实例重建表。
*/
clearAll(): Promise<void>;
/** 获取插件管理器 */
getPluginManager(): PluginManager;
/** 注册钩子 */
on(hook: HookName, callback: HookCallback): void;
/** 关闭数据库 */
close(): Promise<void>;
/**
* 获取底层存储引擎。
*
* v0.8.0:返回**未装饰**的真实引擎。
*
* 变更通知用的 ChangeNotifierEngine 只是内部接线细节;若把它暴露出去,
* 调用方(以及测试)依赖的引擎特有能力(`lsm`、`secondaryIndexes`、
* `getDiskEngineType` 等)会被静默隐藏 —— 本项目既有测试与文档都按
* "getEngine() 就是那个引擎"理解。因此这里保持原语义,装饰器只在 core 内部使用。
*/
getEngine(): IStorageEngine;
/**
* v0.8.0: 取**未装饰**的真实存储引擎。
*
* 引擎在 init 时被 ChangeNotifierEngine 包了一层,因此需要引擎特化能力
* (如 HybridEngine.reloadMemoryFromDisk)时必须先解包,否则 instanceof 恒 false。
*/
private unwrapEngine;
private createEngine;
private ensureReady;
/** 错误回调分发 */
private _onError;
/** 调试日志 */
private _debug;
}
declare class MemoryEngine implements IStorageEngine {
readonly name = "memory";
private tables;
private schemas;
private indexes;
private opened;
/** v0.4.2-fix: 库内元数据(迁移版本持久化用) */
private metaStore;
/**
* v0.7.4: 由 CREATE UNIQUE INDEX 添加的 unique 列(table:col)。
* 与建表 UNIQUE 约束区分:DROP INDEX 只允许解除索引来源的 unique,
* 建表约束需重建表(对齐 SQLite 语义,此前静默解除且不可恢复)。
*/
private uniqueIndexCols;
private snapshot;
open(_dbName: string, _version: number): Promise<void>;
close(): Promise<void>;
isOpen(): boolean;
/** 内存引擎无需修复(无持久化损坏概念) */
repair(): Promise<void>;
/** 清空全部数据与表结构 */
clearAll(): Promise<void>;
getMeta(key: string): Promise<string | null>;
setMeta(key: string, value: string): Promise<void>;
createTable(schema: TableSchema): Promise<void>;
dropTable(tableName: string): Promise<void>;
hasTable(tableName: string): Promise<boolean>;
getTableNames(): Promise<string[]>;
getTableSchema(tableName: string): Promise<TableSchema | null>;
/**
* v0.4.2-fix: 引擎级 ALTER TABLE — 直接修改内存 schema 引用并清理行数据。
* (此前走 executor 通用路径,行为相同;统一到引擎层保证 Hybrid/IndexedDB 委托一致性)
*/
alterTable(tableName: string, action: 'ADD' | 'DROP', column: ColumnDef & {
name: string;
}): Promise<void>;
insert(tableName: string, rows: Record<string, unknown>[]): Promise<string[]>;
/** v0.7.3: 按主键取已验证行(KVStoreEngine 持久化 validated 行用,含 default/类型归一) */
getRow(tableName: string, pkValue: string): Record<string, unknown> | null;
find(tableName: string, query: QueryPlan): Promise<Record<string, unknown>[]>;
/** v0.4.0: 流式查询 — 逐行回调(单次迭代,不物化结果数组) */
findStream(tableName: string, query: QueryPlan, onRow: (row: Record<string, unknown>) => void): Promise<number>;
update(tableName: string, query: QueryPlan, updates: Record<string, unknown>): Promise<number>;
/**
* v0.7.3: 插入唯一性预检 —— 批内互查(本批前几行写入同一唯一值)
* + 索引查(表中已有行)。与 update 的 checkUpdateUniqueness 对称,
* 两阶段 insert 预检阶段调用(索引尚未反映本批写入)。
*/
private checkInsertUniqueness;
/**
* v0.7.3: 更新唯一性预检 — 批内互查(多条行更新到同一唯一值)+ 索引查
* (排除自身旧条目)。阶段 1 中索引尚未更新,批内互查避免"两行同时改到
* 同一新值"绕过唯一约束。
*/
private checkUpdateUniqueness;
/**
* v0.7.2: ON UPDATE RESTRICT 预检 — 从 applyUpdateCascade 提取,
* 两阶段 update 在任何修改前调用(整体拒绝语义)。
*/
private checkUpdateRestrict;
/**
* v0.4.2-fix: ON UPDATE 外键级联 — 被引用表主键变更时处理引用表:
* RESTRICT 抛错 / CASCADE 更新 FK 值 / SET NULL 置空。
* v0.7.3-perf: 删除冗余的阶段 1 RESTRICT 扫描 —— checkUpdateRestrict 已在
* 两阶段 update 预检(阶段 1b)覆盖 RESTRICT 与 SET NULL+required
* 此处任何修改前重复全表扫描纯属浪费。直接执行 CASCADE / SET NULL。
*/
private applyUpdateCascade;
delete(tableName: string, query: QueryPlan): Promise<number>;
/**
* v0.6.3: RESTRICT 预检(delete 级联两阶段之一)。
* 递归沿 CASCADE 链检查引用表:RESTRICT 引用存在依赖行则抛 FOREIGN_KEY_VIOLATION。
*/
private checkCascadeRestrict;
count(tableName: string, query?: QueryPlan): Promise<number>;
clear(tableName: string): Promise<void>;
createIndex(tableName: string, column: string, unique?: boolean): Promise<void>;
dropIndex(tableName: string, column: string, _indexName?: string): Promise<void>;
beginTransaction(): Promise<void>;
commitTransaction(): Promise<void>;
rollbackTransaction(): Promise<void>;
private deepCloneMapMap;
private deepCloneIndexes;
private ensureTable;
private getPrimaryKey;
private validateRow;
/**
* 取该 schema 的行校验器(每次调用重新编译)。
*
* 不缓存在引擎字段上:`alterTable` 会原地修改 schema 对象,
* 长期缓存会继续用过期列定义("加了列却仍被当未知列"这类难查问题)。
* 编译本身只是 `Object.entries` + Set 构造,相对一次 INSERT 的索引维护可忽略。
*/
private rowValidator;
/**
* v0.8.0B-1):写入前置校验(见 `IStorageEngine.validatePayload` 契约)。
*
* 引擎在 `insert` / `update` 内部**同样**会校验 —— 本方法只是让 Executor 与
* QueryBuilder 能在"开始写入之前"拿到同一套判定结果,从而:
* - 多行 INSERT 的预检发生在任何副作用之前(错误信息带列名清单);
* - 直通路径与 SQL 路径不可能给出不同结论(同一个 `compileValidator`)。
*/
validatePayload(tableName: string, rows: Record<string, unknown>[], mode?: 'insert' | 'update'): Promise<void>;
/** 索引查找 */
private tryIndexLookup;
/** 更新索引 */
private updateIndexes;
/** v0.3.3: 从所有索引中移除一行的条目(update/delete 前调用,修复索引过期/残留) */
private removeIndexEntries;
/**
* 级联删除:查找引用 tableName.pkValue 的所有表的行并删除。
* v0.6.1-fix: 环路保护(A→B→A 级联环不再无限递归栈溢出,AriaEngine 同语义)。
* @returns 级联删除的行数
*/
private cascadeDelete;
}
declare class KVStoreEngine implements IStorageEngine {
readonly name = "kv";
private kv;
private memory;
private dbName;
private version;
private opened;
/** 活跃事务标记 */
private txActive;
/** 事务中写过的表(commit 时只 flush 这些表) */
private txDirtyTables;
/** 事务中发生 schema 变更(DDL)—— commit 时持久化 schema */
private txSchemaChanged;
/**
* v0.7.0: 事务行级变更记录(table → pk → put/delete)。
* commit 时按行增量 flush(此前整表 diff:大表事务改 1 行也重写全表)。
*/
private txChanges;
/** v0.7.0: 无法行级追踪的表(主键变更/级联影响表)→ commit 时整表 diff */
private txFullTables;
/** v0.7.0: 事务内 clear 的表 → commit 时清空 KV 行 */
private txClearedTables;
constructor(medium?: IStorageBackend, checkpointThreshold?: number);
private rowKey;
private rowPrefix;
open(dbName: string, version: number): Promise<void>;
close(): Promise<void>;
isOpen(): boolean;
/**
* v0.6.0: 从 KVStore 重新加载全部数据到内存(多标签页同步重载用)。
* Hybrid 引擎的 reloadMemoryFromDisk 依赖磁盘引擎"读穿透"
* KVStoreEngine 读内存 → 提供 reload 重新加载磁盘最新数据。
*/
reload(): Promise<void>;
/** v0.4.2-fix: 自愈 — 校验 KVStore 日志/快照完整性并重建内存 */
repair(): Promise<void>;
clearAll(): Promise<void>;
getMeta(key: string): Promise<string | null>;
setMeta(key: string, value: string): Promise<void>;
createTable(schema: TableSchema): Promise<void>;
dropTable(tableName: string): Promise<void>;
hasTable(tableName: string): Promise<boolean>;
getTableNames(): Promise<string[]>;
getTableSchema(tableName: string): Promise<TableSchema | null>;
alterTable(tableName: string, action: 'ADD' | 'DROP', column: ColumnDef & {
name: string;
}): Promise<void>;
/**
* v0.8.0(B-1):写入前置校验 —— 委托给内存引擎(两者共享同一 schema 表)。
*
* KVStore/Hybrid 的行校验一直"继承"自 MemoryEngine,这正是 A12 的成因:
* 三者共用一份**缺 maxLength/min/max** 的实现。现在共享的是
* `table/validation.ts` 的规范实现,继承关系不再影响约束覆盖面。
*/
validatePayload(tableName: string, rows: Record<string, unknown>[], mode?: 'insert' | 'update'): Promise<void>;
insert(tableName: string, rows: Record<string, unknown>[]): Promise<string[]>;
find(tableName: string, query: QueryPlan): Promise<Record<string, unknown>[]>;
findStream(tableName: string, query: QueryPlan, onRow: (row: Record<string, unknown>) => void): Promise<number>;
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>;
createIndex(tableName: string, column: string, unique?: boolean): Promise<void>;
dropIndex(tableName: string, column: string, indexName?: string): Promise<void>;
beginTransaction(): Promise<void>;
commitTransaction(): Promise<void>;
rollbackTransaction(): Promise<void>;
private ensureOpen;
private getPK;
/** 收集匹配查询的内存行主键(持久化差异计算用) */
private collectMatchingPks;
/**
* 计算外键级联影响的表集合(传递闭包:A 被 B 引用,B 被 C 引用 → {A, B, C})。
* 级联操作(delete/update 主键)需要把这些表一并重写持久化。
*/
private affectedTables;
/** 持久化 schema(全部表) */
private persistSchema;
/**
* v0.6.1: 整表 diff 收集(不落盘):内存行全部 put + KV 残留行删除。
* 调用方合并到单次原子 writeBatchput+delete 同一条日志记录,多表操作真原子)。
*/
private collectTableDiff;
}
/** v0.8.0:引擎恢复诊断(供上层展示/断言;不静默) */
interface AriaRecoveryReport {
/** 打开过程中被丢弃的 SSTable(按命名空间) */
droppedSSTables: {
namespace: string;
id: number;
level: number;
reason: string;
}[];
/** 是否怀疑已确认写入丢失(被丢弃的 SSTable 没有 WAL 兜底) */
dataLossSuspected: boolean;
/** WAL 活跃区间内的分片空洞(已提交事务记录缺失) */
walGaps: number[];
/** 本次打开是否从旧格式(__aria_lsm_meta/__aria_schemas)迁移而来 */
legacyImported: boolean;
/** 是否从更早的 manifest 世代回退(最新世代损坏) */
manifestFallback: boolean;
}
declare class AriaEngine implements IStorageEngine {
readonly name = "aria";
/**
* 生效配置。`testBackend` 与 `pageStorage`/`encryption` 一样是**可选**的
* (不参与 Required),否则 DEFAULT_ARIA_CONFIG 会被迫提供一个假后端。
*/
private config;
private lsm;
private wal;
private checkpointManager;
private backend;
private opened;
private dbName;
private fileManager;
private bufferPool;
private dbLock;
/**
* v0.8.0(B-6):存储层单一提交点。
*
* 页面水位 / 各命名空间 SSTable 列表 / 表结构 / WAL 起始位置 / 待落盘冻结表意图
* 全部收敛到 `__aria_manifest_<generation>`:数据先落盘,再提交 manifest,
* **提交成功后**才允许截断 WAL 或删除旧分片。恢复只认最后一份 CRC 通过的世代。
*/
private manifestStore;
private manifest;
/**
* v0.8.0:已确认落盘的 WAL 水位(LSN)。
*
* 只在"所有 LSM 都没有未落盘数据"时推进到当前 LSN —— 于是
* `lsn <= durableLsn` 的记录必然已存在于已提交的 SSTable 中,
* 恢复时可以安全跳过(也就允许删除对应分片)。
*/
private durableLsn;
/**
* v0.8.0:仍需保留的最小 WAL 分片号(每次 `checkpointBefore` 的返回值)。
* 写进 manifest 的 `wal.startSegment`:即便分片删除只完成一半,恢复也只从
* 这个分片开始读,不会把上一世代的旧记录排到新记录之后重放。
*/
private walStartSegment;
/** v0.8.0:恢复诊断 */
private recoveryReport;
private schemas;
private tablePKs;
private opCounter;
private secondaryIndexes;
/**
* v0.7.4: 由 CREATE UNIQUE INDEX 添加的 unique 列(table:col)。
* 与建表 UNIQUE 约束区分:DROP INDEX 只允许解除索引来源的 unique,
* 建表约束需重建表(对齐 SQLite 语义,此前静默解除且重启后永久消失)。
* 注:重启后无法区分历史来源,schema 中的 unique 一律按建表约束保护(保守)。
*/
private uniqueIndexCols;
private mvcc;
private currentTxnId;
private txnSnapshot;
private gcCounter;
constructor(config?: AriaEngineConfig);
open(dbName: string, _version: number): Promise<void>;
/** open 内部实现(错误包装在 open 外层) */
private openInternal;
close(): Promise<void>;
/**
* v0.4.2-fix: 崩溃恢复/自愈 — 校验并移除损坏 SSTable、截断 WAL、重建二级索引。
* v0.4.5 增强:清理 OPFS 残留临时文件、清理孤儿页面(meta 未引用的 pg_ 文件)。
* 应用层检测到异常后调用,无需删库重建。
*
* v0.8.0B-6):孤儿回收**必须**以"manifest 健康"为前提。
* 修复前 `cleanupOrphanPages` 只读裸 JSON meta,读不出来就当"没有任何引用",
* 于是元数据损坏时 repair 会把全部活页删掉(不可逆)。现在:
* - 只要本次打开出现过损坏世代/被丢弃的 SSTable → 直接跳过回收并告警;
* - 引用集合来自 manifest 的权威 meta 列表。
*/
repair(): Promise<void>;
/**
* v0.4.5: 清理孤儿页面 — 扫描全部 pg_* 文件,未被任何命名空间 meta 引用的删除。
* 孤儿页面来自:崩溃中断的 compaction/删除流程(旧 SSTable 页面残留)。
*
* v0.8.0B-6):引用集合取自 manifest;且**只有在没有损坏迹象时才执行** ——
* "任何引用不到的东西一律保留而非删除"在恢复路径上是不变量,只有显式 repair
* 且 manifest 完整可信时才允许回收空间。
*/
private cleanupOrphanPages;
/** v0.8.0: 读取某个 LSM 当前引用的层结构(诊断/孤儿回收用) */
private getLsmLevels;
/**
* v0.4.1: 重置数据库 — 清空全部数据与表结构(演示页刷新/重新初始化用)。
* 清空存储后端、LSM、WAL、MVCC 与二级索引,后续可继续使用本实例。
*/
clearAll(): Promise<void>;
isOpen(): boolean;
getMeta(key: string): Promise<string | null>;
setMeta(key: string, value: string): Promise<void>;
createTable(schema: TableSchema): Promise<void>;
/**
* v0.8.0A41):追加一条 DDL 意图记录并立即刷盘。
*
* 为什么独立成函数:两条 DDL 路径必须共用同一套顺序与刷盘策略,
* 否则将来只改一处又会漂移 —— 这正是本项目反复出现的缺陷模式。
*
* DDL 不参与事务(`ensureNoDDLInTransaction` 已保证),因此 txnId 恒为 0,
* 不需要提交/回滚语义;但**必须先于生效**写入,否则崩溃会静默丢失 DDL。
* DDL 是低频操作,这里同步刷盘,避免"崩溃丢失 DDL"的窗口过大。
*/
private appendDDLRecord;
dropTable(tableName: string): Promise<void>;
/**
* v0.4.2-fix: 清理指定表的全部二级索引 LSM(内存 + 存储文件 + meta)。
* dropTable / DROP_TABLE 恢复 / alterTable DROP 索引列 共用。
*/
private cleanupTableIndexes;
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>;
/**
* v0.7.2: 批内唯一互查 — 两条行在同一语句中更新到同一唯一值时的兜底检查
* (阶段 1 中索引尚未反映本语句的变更)。
*/
private checkBatchUnique;
/**
* v0.7.2: ON UPDATE 外键预检 — 从 applyForeignKeyUpdateRules 提取(两阶段 update 用):
* RESTRICT 存在依赖行抛错;SET NULL 撞 required 列同样整体拒绝。
*/
private checkForeignKeyUpdateRestrict;
/**
* v0.4.2-fix: ON UPDATE 外键级联 — 主键 oldPk → newPk 时处理引用表。
* RESTRICT 抛错 / CASCADE 更新 FK / SET NULL 置空(含索引与 WAL 记录)。
* 两阶段:先全量 RESTRICT 检查,再执行级联。
*/
private applyForeignKeyUpdateRules;
delete(tableName: string, query: QueryPlan): Promise<number>;
/**
* v0.6.3: RESTRICT 预检(delete 级联两阶段之一,与 MemoryEngine 对齐)。
* 递归沿 CASCADE 链检查引用表:RESTRICT 引用存在依赖行则抛 FOREIGN_KEY_VIOLATION。
*/
private checkCascadeRestrict;
/**
* v0.4.1: 外键级联规则 — 对齐 MemoryEngine.cascadeDelete 行为。
* 删除 tableName 主键为 pkValue 的行前,检查引用它的所有表:
* - RESTRICT: 存在引用行 → 抛 FOREIGN_KEY_VIOLATION
* - CASCADE: 递归删除引用行(含索引/WAL)
* - SET NULL: 引用行外键列置 null(含索引/WAL)
* @returns 级联影响的行数(CASCADE 删除行数 + SET NULL 更新行数)
*/
private applyForeignKeyRules;
/**
* v0.4.0: 流式查询 — 逐行回调,不物化结果数组。
* 全表路径走 LSM rangeScanLazy 惰性扫描;索引等值/范围路径复用 tryIndexLookup。
* 事务中回退物化(快照合并需要全量行集)。
*/
findStream(tableName: string, query: QueryPlan, onRow: (row: Record<string, unknown>) => void): Promise<number>;
count(tableName: string, query?: QueryPlan): Promise<number>;
clear(tableName: string): Promise<void>;
/**
* v0.4.1: ALTER TABLE — 结构变更真正生效于存储:
* - ADD: 持久化 schemapersistSchemas),行无需修改
* - DROP: 持久化 schema + 遍历主 LSM 重写所有行(移除该列键)+ WAL UPDATE 记录
* (通用路径 getTableSchema 返回副本,Executor 的引用修改对 Aria 无效)
*/
alterTable(tableName: string, action: 'ADD' | 'DROP', column: ColumnDef & {
name: string;
}): Promise<void>;
createIndex(tableName: string, column: string, unique?: boolean): Promise<void>;
dropIndex(tableName: string, column: string, _indexName?: string): Promise<void>;
beginTransaction(): Promise<void>;
commitTransaction(): Promise<void>;
rollbackTransaction(): Promise<void>;
private savepoints;
/**
* v0.8.0: 当前事务在 WAL 中已成功追加的记录条数。
*
* 用途:保存点需要记录"回滚后应保留到哪一条",否则恢复时无法区分
* "保存点之前的写入"(应保留)与"保存点之后的写入"(应丢弃)。
*/
private txnWalRecordCount;
/** 保存点 → 该保存点时事务的 WAL 记录边界 */
private savepointWalBoundary;
savepoint(name: string): Promise<void>;
rollbackToSavepoint(name: string): Promise<void>;
releaseSavepoint(name: string): Promise<void>;
backup(): Promise<Record<string, Record<string, unknown>[]>>;
private getAllRows;
/**
* v0.3.3: 将事务未提交快照的变更合并到行列表(新增/更新/删除标记)。
* 幂等操作:行已是最新时不重复修改。
*/
private mergeTxnSnapshot;
private getPK;
private validateRow;
/**
* v0.8.0(B-1):写入前置校验 —— 见 `IStorageEngine.validatePayload` 契约。
*/
validatePayload(tableName: string, rows: Record<string, unknown>[], mode?: 'insert' | 'update'): Promise<void>;
/**
* v0.8.0B-6):表结构**随 manifest 一起提交**(单一提交点)。
*
* 修复前 DDL 结束时会单独写一份 `__aria_schemas`:结构变更与存储状态
* SSTable meta / 页面 / WAL 水位)各自落盘,中间崩溃就会留下"结构说加过列、
* 数据里没有"或反之的分裂状态。现在两者在同一次原子提交里生效。
*/
private persistSchemas;
private loadSchemas;
/**
* v0.8.0: 恢复诊断(打开时被丢弃的 SSTable、WAL 空洞、是否怀疑数据丢失)。
*
* 数据来自两处:引擎层(WAL 空洞 / 迁移 / manifest 回退)与各 LSM(被丢弃的
* SSTable + 其 `dataLossSuspected`),这里合并成一份对外的报告 —— 于是
* "这次打开到底自愈了什么、有没有真丢数据"是**可读的返回值**而不是只能翻日志。
*/
getRecoveryReport(): AriaRecoveryReport;
/**
* v0.8.0: LSM 的唯一构造点。
*
* 为什么集中:本项目最反复的缺陷模式就是"同一语义在多处实现、只修一处"
* (审计结论的原话)。主 LSM 与二级索引 LSM 的配置必须完全一致地带上
* 命名空间、WAL LSN 提供者与 durable-coverage 语义,因此只能有一个工厂。
*/
private createLSM;
/** 全部 LSM(主 + 二级索引) */
private allLsms;
/**
* v0.8.0: 把全部 LSM 的数据落盘。
* @param memtablesOnly true = 只落 memtable,不等 compactioncheckpoint 用)
*/
private flushAllLsms;
/** v0.8.0: 是否存在任何未落盘的 LSM 数据(决定 WAL 水位能否推进) */
private hasPendingFlushData;
/** schema 的持久化形态(与旧 `__aria_schemas` 同形) */
private serializeSchemas;
/** 所有 LSM 的待落盘冻结表意图(manifest 记录,阻止 WAL 水位越过它们) */
private collectFrozenIntents;
/**
* v0.8.0B-6):**唯一的 manifest 提交入口**。
*
* 每次提交都重新计算权威字段,因此并发/交错场景下"最后一次提交"总是包含
* 完整的最新状态:
* - `pageIdWatermark`:单调推进、永不复用;
* - `schemas`:表结构的权威描述(DDL 不再依赖独立的 `__aria_schemas` 提交);
* - `frozen`:待落盘冻结表意图;
* - `wal.startLsn`**只有全部数据已落盘时才推进**(否则保持原值),
* 这是"截断 WAL 前必须先提交 manifest"的可验证形式。
*/
private commitManifest;
/**
* v0.8.0:计算"当前可以保证的落盘水位"(纯函数,不改状态)。
*
* 三条规则:
* - 有冻结表意图 → 水位不得越过最早的冻结表内容起点(它们的记录只在内存+WAL);
* - 有未落盘 memtable → 维持原水位;
* - 全部落盘 → 推进到当前 LSN(这些记录已存在于已提交的 SSTable 中)。
*/
private computeDurableLsn;
/**
* v0.8.0B-6):WAL 检查点的**唯一实现** —— "先算边界 → 提交 manifest → 再删除"。
*
* 顺序不可交换(见 `SegmentedWALStore.planKeepFrom` 的说明)。所有需要回收 WAL
* 空间的路径(打开恢复后、close、repair、周期 checkpoint)都必须走这里,
* 否则又会出现"同一语义多处实现、只改一处"的老问题。
*/
private advanceWalCheckpoint;
/**
* v0.8.0:旧格式(v0.8.0 之前)状态导入。
*
* 导入必须是**全有或全无**的:
* - 旧 meta 存在但无法解析 → 抛错(`ARIA_LEGACY_META_CORRUPT`)。
* 修复前 `readMetaList()` 遇到坏 JSON 返回 `[]`,于是"元数据损坏"直接
* 表现为"空库",随后 repair 还会把没人引用的活页全部删掉(不可逆)。
*/
private importLegacyState;
/**
* v0.8.0:恢复后校验冻结表意图。
*
* 语义:manifest 记录了"某张冻结表还没落盘"(意图),说明它的数据要么在 WAL 里,
* 要么已经在 SSTable 里。若本次打开**一条 WAL 记录都没有重放到**,而 manifest
* 又声称有未落盘数据,那么这些"已确认写入"就是真的丢了(WAL 被截断/介质丢失)。
* 此时抛错 —— 修复前这种丢失完全不可观测。
*/
private verifyFrozenIntentsAfterRecovery;
/**
* 创建命名空间隔离的 SSTableStore**manifest 权威**)。
*
* 主 LSM 与每个二级索引 LSM 各持有独立实例:
* - 文件 key 前缀隔离(sst_ / sst_idx_${table}_${col}_
* - 元数据在 manifest 的 `namespaces[ns]` 中隔离(不再各写一份裸 JSON)
* - id 序列独立且**单调推进**`nextSstableId` 记在 manifest 里,
* 即便某一代 SSTable 全部被删除,id 也不会被复用)
*
* v0.8.0B-6)两处结构变化:
* 1. **meta 不再走裸 JSON**`saveMeta`/`deleteMeta` 直接改 manifest 并提交。
* 修复前 `JSON.parse` 失败 → 返回 `[]` → 元数据损坏 = 静默空库
* (随后 repair 还会把没人引用的活页删掉);
* 2. `load/delete` 的页面映射改由 PageSSTableStore 自己维护
* (活跃 + 退休两张表)—— 于是"compaction 摘除 meta"与
* "在途读者按 id 读取"不再互相矛盾。
*/
private createSSTableStore;
/** v0.4.5: 是否启用页面化物理存储(默认 OPFS / KVStore 后端启用,显式配置可覆盖) */
private isPageStorage;
private applyWALRecord;
/**
* v0.3.3: DROP_TABLE 恢复 — 删除 schema 并清除主 LSM 中该表的所有残留数据。
*
* 此前 DROP_TABLE 在恢复时被忽略,而 CREATE_TABLE 回放会重建 schema
* 导致崩溃后"已删除的表和数据复活"(实证 P0 bug)。
*/
private applyDropTableRecovery;
/** v0.6.2: 表中有 unique 约束且索引 LSM 已建的列(唯一性检查范围) */
private uniqueColumns;
/**
* 唯一性检查。
*
* v0.8.0: 由 `checkUniqueSync` 改名并改为 async —— 此前命名为 "Sync" 是因为它
* 依赖"批次级 prefetchPrefixRanges 之后索引数据已在缓存中"这一约定。现在
* LSM 读取自洽(未命中即回源),因此这里可以、也必须 await。
* 索引不含 null 条目(null 值不受唯一约束,与 MemoryEngine 语义一致)。
* @param currentPk 当前行主键(更新路径用于排除自身旧索引条目;插入路径无自身条目)
*/
private checkUnique;
/** 更新行的二级索引条目 */
private updateSecondaryIndexes;
/** 通过二级索引快速查找 */
private tryIndexLookup;
/** 从索引扫描结果恢复完整行 */
private indexScanToRows;
/** 每 10 次 gc 计数器触发一次 MVCC 垃圾回收 */
private tryGC;
/** 回收主 LSM 与所有二级索引 LSM 的临时缓存超限 */
private trimAllCaches;
/** 检查内存预算,超出时强制 flush + GC */
private checkMemoryBudget;
/**
* ANALYZE: 收集表统计信息
* 返回行数、平均行大小、索引深度等
*/
analyzeTable(tableName: string): Promise<Record<string, unknown>>;
/**
* REINDEX: 重建指定表的所有二级索引
*/
reindexTable(tableName: string): Promise<number>;
/** v0.4.2-fix: 重建索引内部实现(不校验 opened,供 open 恢复流程调用) */
private reindexTableInternal;
/**
* VACUUM: 压缩 LSM + 清理碎片
*/
vacuum(): Promise<{
compactedLevels: number;
gcVersions: number;
}>;
private ensureOpen;
/** v0.4.2-fix: Aria 事务中 DDL 显式拒绝(结构变更无法通过行快照回滚) */
private ensureNoDDLInTransaction;
private ensureTable;
}
declare class HybridEngine implements IStorageEngine {
readonly name = "hybrid";
private memoryEngine;
private diskEngine;
private diskEngineType;
private dbName;
private version;
constructor(diskEngine?: DiskEngine);
open(dbName: string, version: number): Promise<void>;
/**
* 从磁盘重载内存缓存(v0.3.2:多标签页同步)。
* 其他标签页写入磁盘后调用,使本标签页读到最新数据。
*/
reloadMemoryFromDisk(): Promise<void>;
close(): Promise<void>;
isOpen(): boolean;
/** 自愈:修复磁盘引擎后重载内存缓存 */
repair(): Promise<void>;
/** 清空全部数据与表结构 */
clearAll(): Promise<void>;
getMeta(key: string): Promise<string | null>;
setMeta(key: string, value: string): Promise<void>;
createTable(schema: TableSchema): Promise<void>;
dropTable(tableName: string): Promise<void>;
hasTable(tableName: string): Promise<boolean>;
getTableNames(): Promise<string[]>;
getTableSchema(tableName: string): Promise<TableSchema | null>;
/** v0.4.2-fix: 引擎级 ALTER TABLE — 双引擎同步(磁盘持久化 + 内存引用) */
alterTable(tableName: string, action: 'ADD' | 'DROP', column: ColumnDef & {
name: string;
}): Promise<void>;
/**
* v0.7.2: 磁盘写失败补偿 — 内存已先行写入、磁盘失败 → 内存与磁盘不一致
* (重启后数据丢失且调用方已收到错误)。从磁盘重载内存对齐真实状态
* (内存=磁盘),再重新抛出原始错误。事务路径由双引擎快照回滚保证,
* 无需此补偿。
*/
private recoverMemoryAfterDiskError;
/**
* v0.8.0(B-1):写入前置校验 —— 委托给内存引擎(与磁盘引擎同 schema)。
*
* 关键点:**只判定一次**。Hybrid 的 write-through 会把同一批行先写内存再写磁盘,
* 两个引擎各自校验会给出同一结论(现在共享同一个 `compileValidator`),
* 但由本方法统一前置,可保证多行批量在任何副作用之前整体失败。
*/
validatePayload(tableName: string, rows: Record<string, unknown>[], mode?: 'insert' | 'update'): Promise<void>;
insert(tableName: string, rows: Record<string, unknown>[]): Promise<string[]>;
find(tableName: string, query: QueryPlan): Promise<Record<string, unknown>[]>;
/** v0.4.0: 流式查询(内存引擎逐行回调) */
findStream(tableName: string, query: QueryPlan, onRow: (row: Record<string, unknown>) => void): Promise<number>;
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>;
createIndex(tableName: string, column: string, unique?: boolean): Promise<void>;
dropIndex(tableName: string, column: string, indexName?: string): Promise<void>;
beginTransaction(): Promise<void>;
commitTransaction(): Promise<void>;
rollbackTransaction(): Promise<void>;
/** 获取磁盘引擎类型 */
getDiskEngineType(): DiskEngine;
/** 获取内存引擎(供内部使用) */
getMemoryEngine(): MemoryEngine;
}
/**
* metona-sqlark SQL Parser — 递归下降语法分析器
* @module sql/parser
*
* Token 流 → AST Statement。
* 支持的语法是标准 SQL 的子集。
*/
/** 解析 SQL 字符串为 AST Statement */
declare function parse(sql: string): Statement;
/** 解析 SQL 字符串为 AST Statement 数组(分号分隔的多语句支持,v0.3.0) */
declare function parseAll(sql: string): Statement[];
/** 解析独立 WHERE 条件表达式(CASE WHEN 求值等场景,v0.3.1 */
declare function parseWhereCondition(sql: string): WhereCondition;
/**
* metona-sqlark SQL Token Types — 词法单元定义
* @module sql/tokens
*/
declare 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",
REFERENCES = "REFERENCES",
CASCADE = "CASCADE",
BETWEEN = "BETWEEN",
IF = "IF",
EXISTS = "EXISTS",
FALSE = "FALSE",
ALTER = "ALTER",
ADD = "ADD",
TRUNCATE = "TRUNCATE",
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",
BEGIN = "BEGIN",
COMMIT = "COMMIT",
ROLLBACK = "ROLLBACK",
UNION = "UNION",
ALL = "ALL",
INDEX = "INDEX",
CASE = "CASE",
WHEN = "WHEN",
THEN = "THEN",
ELSE = "ELSE",
END = "END",
EXPLAIN = "EXPLAIN",
ANALYZE = "ANALYZE",
REINDEX = "REINDEX",
VACUUM = "VACUUM",
SAVEPOINT = "SAVEPOINT",
RELEASE = "RELEASE",
TO = "TO",
IDENTIFIER = "IDENTIFIER",
/**
* v0.8.0: 分隔标识符(双引号包裹,SQL 标准 `"name"`)。
*
* 此前双引号被当作字符串定界符处理,`SELECT "name" FROM t` 会静默产出一个名为
* `'name'` 的**常量列**(行数正确、值全错、无任何报错),且该行为被
* tests/sql/lexer.test.ts 钉死为期望。现按 SQL 标准区分:
* 'x' → STRING(字符串字面量)
* "x" → QUOTED_IDENTIFIER(标识符,用于含特殊字符/保留字/大小写敏感的列名)
* 双引号内以 "" 表示一个双引号。
*/
QUOTED_IDENTIFIER = "QUOTED_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"
}
interface Token {
type: TokenType;
value: string;
position: number;
}
/**
* metona-sqlark SQL Lexer — 词法分析器
* @module sql/lexer
*
* 将 SQL 字符串切分为 Token 流。
*/
/** 将 SQL 字符串解析为 Token 列表 */
declare function tokenize(sql: string): Token[];
/**
* metona-sqlark SQL Parameters — 参数化查询绑定
* @module sql/params
*
* v0.7.0: `db.query(sql, params)` 位置参数(`?`)支持。
* 绑定在词法层面完成:仅替换字符串字面量之外的 `?`,
* 值按 SQL 字面量编码(字符串 `''` 转义、数字/布尔/JSON 直出),
* 从根上规避 SQL 注入(不经过字符串拼接由用户自行转义)。
*
* v0.7.2: 词法扫描感知注释 —— 行注释(`--`)与块注释(slash-star 包裹)中的 `?`
* 与引号不再参与占位符识别与字符串状态机(此前注释中的 `?` 计入占位符导致
* PARAM_ERROR 错位、注释中的单引号触发 "Unterminated string literal")。
*/
/**
* 将 SQL 中的位置参数 `?`(字符串字面量与注释之外)替换为编码后的字面量。
* @param sql 含 `?` 占位符的 SQL
* @param params 位置参数数组
* @throws PARAM_ERROR 参数数量不匹配
*/
declare function bindParameters(sql: string, params?: unknown[]): string;
/**
* AriaEngine OPFS Backend — 基于 Origin Private File System 的自研存储后端
* @module engine/aria/store/opfs_backend
*
* 零外部依赖,纯浏览器文件系统 API。
* 每个 key 对应 OPFS 目录下的一个二进制文件。
*
* 原子性与一致性保证(v0.4.5 固化):
* - 单文件 write/appendcreateWritable 为 copy-on-write —— close 前崩溃旧文件保持不变,
* close 后原子替换(单文件写入原子)
* - 多文件 writeMany/deleteManyOPFS 无跨文件事务,串行逐个落盘;调用方(WAL 分片)
* 已改为单文件语义,多键操作仅用于一次性的 schema/meta 写入
* - 所有写操作串行队列化(同源同进程顺序一致);单次任务失败不中断队列链,
* 错误如实返回给该次调用的调用方
* - close() 等待写队列排空后再释放目录句柄(杜绝 close 后挂起写丢失/读旧数据)
* - open() 自动清理崩溃残留临时文件(Chromium createWritable 的 .crswap 等)
*
* 浏览器要求:Chrome 102+ / Edge 102+Safari 15.2+ / Firefox 111+ 支持基础 OPFS
*/
declare class OPFSBackend implements IStorageBackend {
private root;
private dbDir;
private dbName;
private writeQueue;
open(name: string): Promise<void>;
close(): Promise<void>;
isOpen(): boolean;
/** 清理崩溃残留的临时文件(open 时自动调用,repair 也可调用) */
cleanupStaleFiles(): Promise<void>;
read(key: string): Promise<ArrayBuffer | null>;
/**
* 单文件原子写:createWritable 为 copy-on-writeclose 后原子替换;
* 写入期间崩溃 → 旧文件保持(原子性由浏览器 OPFS 实现保证)。
*/
write(key: string, data: ArrayBuffer): Promise<void>;
/**
* v0.4.5: 真追加写 — createWritable(keepExistingData) + seek 到文件末尾。
* 单文件 COW 原子(close 前崩溃旧文件保持),无需读旧内容即实现 O(chunk) 追加
* (WAL 分片高频写入用)。
*/
append(key: string, data: ArrayBuffer): Promise<void>;
/** v0.4.2-fix: 批量写入 — 串行队列内逐个落盘(OPFS 无跨文件事务,顺序保证一致) */
writeMany(entries: Record<string, ArrayBuffer>): Promise<void>;
delete(key: string): Promise<void>;
/** v0.4.2-fix: 批量删除 — 串行队列内逐个删除 */
deleteMany(keys: string[]): Promise<void>;
listKeys(): Promise<string[]>;
exists(key: string): Promise<boolean>;
clear(): Promise<void>;
}
/**
* migrateFromIndexedDB — 旧 IndexedDB 数据迁移到自研 KV 引擎
* @module migration/index
*
* v0.6.0: IndexedDB 从引擎中完全移除后,提供一次性迁移工具把旧库数据
* 导入新引擎(KVStoreEngine disk 模式 / AriaEngine)。
*
* 旧库命名:
* - disk 模式(IndexedDBEngine):库名 = dbName
* - aria 模式(IndexedDBBackend):库名 = `aria-${dbName}`
*
* 仅此模块保留原生 IndexedDB 读取代码(一次性迁移用途,不参与运行时)。
*/
interface MigrationOptions {
/** 旧库名(业务名,不含 aria- 前缀) */
dbName: string;
/**
* 旧引擎类型:仅支持 diskIndexedDBEngine,每表一个 objectStore,行数据可直接读取)。
* aria 旧库(IndexedDBBackend)数据为引擎私有格式(SSTable/WAL),无法按行迁移。
*/
engine: 'disk';
/** 目标数据库实例(已初始化,新引擎) */
target: MetonaSqlark;
/** 进度回调 */
onProgress?: (done: number, total: number, table?: string) => void;
}
interface MigrationResult {
/** 已迁移的表 */
migratedTables: string[];
/** 迁移的行总数 */
rowCount: number;
/** 跳过(无 schema 且无数据)的表 */
skippedTables: string[];
}
/**
* 将旧 IndexedDB 库迁移到目标引擎。
* @returns 迁移结果(表/行数统计)
*/
declare function migrateFromIndexedDB(opts: MigrationOptions): Promise<MigrationResult>;
/**
* metona-sqlark — 入口文件
* @module metona-sqlark
* @version 0.4.1
*
* 前端关系型数据库,内存与磁盘双模式。
* 支持 Query Builder 链式 API 和 SQL 字符串查询。
*/
/**
* 创建数据库实例并初始化
*
* @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');
* ```
*/
declare function create(config: DatabaseConfig): Promise<MetonaSqlark>;
declare const api: {
VERSION: string;
version: string;
create: typeof create;
MetonaSqlark: typeof MetonaSqlark;
MeSqlark: typeof MetonaSqlark;
};
declare global {
interface Window {
MetonaSqlark: typeof api;
MeSqlark: typeof api;
}
}
declare const MeSqlark: typeof MetonaSqlark;
export { AriaEngine, AriaEngineConfig, ColumnDef, DatabaseConfig, DatabaseError, DeleteStatement, DiskEngine, FieldType, HookCallback, HookName, HybridEngine, IStorageEngine, InsertStatement, KVStoreEngine, MeSqlark, MemoryEngine, MetonaPlugin, MetonaSqlark, MigrationOptions, MigrationResult, OPFSBackend, OrderBy, PluginManager, QueryPlan, SelectStatement, Statement, StorageMode, Table, TableSchema, Transaction, TransactionManager, UpdateStatement, VERSION, WhereCondition, WhereOperator, api, bindParameters, create, api as default, migrateFromIndexedDB, parse, parseAll, parseWhereCondition, tokenize };