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/元数据全覆盖)。 * 密钥由 PBKDF2(salt 持久化于库内 __aria_keymeta)派生,重启用同一密码即可解密。 */ encryption?: { /** 加密密码 */ password: string; }; /** * v0.4.5: SSTable 页面化存储(4KB 页面 + BufferPool/FileManager 管理,LRU 缓存)。 * 默认:storageBackend === 'opfs' 时自动启用(大文件随机读/写放大优化); * 显式 false 强制关闭(整 value 存储,兼容旧行为)。 */ pageStorage?: boolean; } /** * 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; } /** 数据库配置 */ interface DatabaseConfig { /** 数据库名称 */ name: string; /** 存储模式 */ mode?: StorageMode; /** 磁盘引擎(仅 mode='disk'|'hybrid' 时生效) */ 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>; /** 字段条件:简单值 | 操作符对象 */ type FieldCondition = SimpleCondition | OperatorCondition; /** Where 条件对象 */ type WhereCondition = Record; /** 排序方向 */ 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; /** 优先级,越大越先执行 */ 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; 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; /** 销毁所有插件 */ destroy(): void; } interface IStorageEngine { /** 引擎名称 */ readonly name: string; /** 打开数据库 */ open(dbName: string, version: number): Promise; /** 关闭数据库 */ close(): Promise; /** 检查数据库是否已打开 */ isOpen(): boolean; /** 创建表 */ createTable(schema: TableSchema): Promise; /** 删除表 */ dropTable(tableName: string): Promise; /** 检查表是否存在 */ hasTable(tableName: string): Promise; /** 获取所有表名 */ getTableNames(): Promise; /** 获取表结构 */ getTableSchema(tableName: string): Promise; /** 插入行,返回主键值列表 */ insert(tableName: string, rows: Record[]): Promise; /** * 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[], mode?: 'insert' | 'update'): Promise; /** 查询行 */ find(tableName: string, query: QueryPlan): Promise[]>; /** v0.4.0: 流式查询 — 逐行回调扫描(有 where/limit/projection,无 orderBy 语义;有 orderBy 时实现可回退物化) */ findStream?(tableName: string, query: QueryPlan, onRow: (row: Record) => void): Promise; /** 更新行,返回影响行数 */ update(tableName: string, query: QueryPlan, updates: Record): Promise; /** 删除行,返回影响行数 */ delete(tableName: string, query: QueryPlan): Promise; /** 计数 */ count(tableName: string, query?: QueryPlan): Promise; /** 清空表数据(保留结构) */ clear(tableName: string): Promise; /** v0.4.1: ALTER TABLE(可选)— 引擎级结构变更(Aria 需重写存储行,其余引擎走 Executor 通用路径) */ alterTable?(tableName: string, action: 'ADD' | 'DROP', column: ColumnDef & { name: string; }): Promise; /** 创建二级索引(CREATE INDEX) */ createIndex?(tableName: string, column: string, unique?: boolean): Promise; /** 删除二级索引(DROP INDEX) */ dropIndex?(tableName: string, column: string, indexName?: string): Promise; /** 开始事务 */ beginTransaction(): Promise; /** 提交事务 */ commitTransaction(): Promise; /** 回滚事务 */ rollbackTransaction(): Promise; /** 创建 Savepoint */ savepoint?(name: string): Promise; /** 回滚到 Savepoint */ rollbackToSavepoint?(name: string): Promise; /** 释放 Savepoint */ releaseSavepoint?(name: string): Promise; /** 在线备份:导出全库一致性快照 */ backup?(): Promise[]>>; /** 崩溃恢复自愈:校验并清理损坏数据、恢复一致性(检测到异常后调用,无需删库重建) */ repair?(): Promise; /** 清空全部数据与表结构(保留库本身,供演示页刷新/重建用) */ clearAll?(): Promise; /** 读取库内元数据(迁移版本持久化用) */ getMeta?(key: string): Promise; /** 写入库内元数据(迁移版本持久化用) */ setMeta?(key: string, value: string): Promise; } /** * 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; 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.0(A26):复合查询**整体**的 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; private dispatch; /** * 递归执行 UNION / UNION ALL,返回合并结果。 * * v0.8.0(A26):尾部 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.0(A22/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 对空集返回 null(SQL 标准), * COUNT 仍返回 number。最小/最大改为单次遍历(不再展开实参,消除栈溢出)。 */ private computeAggregate; /** * DISTINCT 聚合(`COUNT(DISTINCT col)` / `SUM(DISTINCT col)`)。 * * v0.8.0(A25):独立成函数而不是在 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/DELETE)WHERE 的子查询解析。 * 非关联子查询($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.0(A37):校验 JOIN ON 里引用的列在**参与连接的两张表**之一存在。 * * `ON a.x = b.y` 的 `a.x` 属于主表或已有 JOIN 表,`b.y` 属于当前 JOIN 表 —— * 两侧都要能找到归属;否则报 COLUMN_NOT_FOUND(而不是让连接静默产生空结果)。 * 裸列名只要求"某一侧存在"(`ON k = k` 的既有语义是取主表列)。 */ private validateJoinOnColumns; /** * v0.8.0(A37):校验 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`)。 * 别名列在引擎层投影前不存在,需投影后重新排序。 */ 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` 作为被引用列; * - 其余视为裸列引用。 * 存在性检查允许两种形态:精确匹配,或**唯一**以 `.` 结尾(JOIN 行以 `alias.col` 为键)。 * 若同一个后缀出现在多个表别名下则视为歧义,同样报错(符合"未限定列名歧义应报错"的语义)。 * * 结果集为空时无法判定,此时跳过(空表 + 未知列不会误报)。 */ private assertProjectionColumnsExist; /** * 列投影(v0.3.1):普通列走 projectColumns,CASE 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.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.0(A10):在外层行里取 `$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.0(B-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[]>; /** 获取 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, 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; after?: (count: number, stmt: UpdateStatement) => Promise; } | undefined); where(condition: WhereCondition): this; /** * 执行更新 —— 走 Executor(唯一的写管线)。 * * 修复前这里直接调 `engine.update`:`$subquery` / `$col` / `$exists` 无人解析, * 引擎层 matchWhere 判 UNKNOWN → **静默影响 0 行**(返回 0 且无报错)。 * 引擎层为此加过"检测未解析标记就抛 NOT_SUPPORTED"的防御 —— 那是把 * "管线缺失"暴露成用户错误;正确做法是把请求送进唯一管线。 */ execute(): Promise; 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; after?: (count: number, stmt: DeleteStatement) => Promise; } | undefined); where(condition: WhereCondition): this; /** 执行删除 —— 走 Executor(同 UpdateQueryBuilder.execute 的理由) */ execute(): Promise; toAST(): DeleteStatement; } declare class Table> { 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); getSchema(): Promise; insert(row: T & Record): Promise; insertMany(rows: (T & Record)[]): Promise; 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) => void, query?: { where?: Record; limit?: number; offset?: number; columns?: string[]; }): Promise; /** * v0.8.0(B-3):写操作也走**唯一管线**(Executor),生命周期钩子由本方法注入。 * * 修复前 `UpdateQueryBuilder` 直接调 `engine.update`:`$subquery`/`$col` 无人解析 * → 引擎判 UNKNOWN → 静默影响 0 行;`beforeUpdate`/`afterUpdate` 的触发点也因此 * 与 SQL 路径不同(一条在 builder 里、一条在 core 里)。 */ update(updates: Partial & Record): UpdateQueryBuilder; delete(): DeleteQueryBuilder; count(where?: Record): Promise; clear(): Promise; drop(): Promise; } /** * 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 现在必须拿到 executor(builder 只产出 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(fn: (trx: Transaction) => Promise): Promise; } /** 变更类型(与 site/docs.html 承诺的 `event.type` 对齐) */ type ChangeType = 'insert' | 'update' | 'delete' | 'clear' | 'ddl' | 'external'; /** 变更事件 */ interface ChangeEvent { /** 变更类型 */ type: ChangeType; /** 表名 */ table: string; /** 受影响的行(可用时提供) */ row?: Record; /** 受影响行的主键(可用时提供) */ key?: string; /** 受影响行数 */ count?: number; } declare class MetonaSqlark { /** 数据库名称 */ readonly name: string; /** * v0.7.1: 静态工厂(与 connect/disconnect 同一入口风格)。 * 此前 create 仅存在于 api 对象 / window 挂载 —— README/站点示例的 * `MetonaSqlark.create({...})` 在 ESM/Node 下是 undefined(TypeError)。 */ static create(config: DatabaseConfig): Promise; /** 存储模式 */ 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; /** 检查是否就绪 */ isReady(): boolean; /** 创建表 */ defineTable(name: string, columns: Record): Promise; /** 获取表操作对象 */ table(name: string): Table; /** 删除表 */ dropTable(name: string): Promise; /** 获取所有表名 */ getTableNames(): Promise; /** * 执行 SQL 字符串查询。 * v0.7.0: 支持位置参数(`?`)—— `db.query('SELECT * FROM t WHERE id = ?', ['1'])`。 * 参数按 SQL 字面量安全编码(字符串 '' 转义),杜绝 SQL 注入。 */ query(sql: string, params?: unknown[]): Promise; /** * 流式查询:逐行回调,不一次性物化全部结果(大表友好)。 * 支持简单 SELECT(WHERE/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 = Record>(sql: string, onRow: (row: T) => void): Promise; /** * 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(fn: (trx: Transaction) => Promise): Promise; /** 导出表数据为 JSON */ exportTable(tableName: string): Promise[]>; /** 导入 JSON 数据到表 */ importTable(tableName: string, data: Record[]): Promise; /** 导出整个数据库为 JSON */ exportAll(): Promise[]>>; /** * v0.5.1: 在线备份 — 导出全库一致性快照。 * Aria 引擎走引擎级 backup()(MVCC 一致性视图);其余引擎回退 exportAll()。 */ backup(): Promise[]>>; /** 变更通知引擎(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; /** * 手动触发变更事件(保留为公开 API:自定义写入路径可显式通知订阅者)。 * 现在也支持 await —— 订阅者的 Promise 会被等待。 */ emit(tableName: string, event: Partial & { type: ChangeEvent['type']; }): Promise; /** * 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; /** 执行迁移到指定版本 */ migrateTo(targetVersion: number): Promise; /** * 崩溃恢复自愈 — 校验并清理损坏数据、恢复一致性。 * 检测到异常后调用,无需删库重建。 */ repair(): Promise; /** * 清空全部数据与表结构(保留库本身)。 * 支持后续继续使用本实例重建表。 */ clearAll(): Promise; /** 获取插件管理器 */ getPluginManager(): PluginManager; /** 注册钩子 */ on(hook: HookName, callback: HookCallback): void; /** 关闭数据库 */ close(): Promise; /** * 获取底层存储引擎。 * * 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; close(): Promise; isOpen(): boolean; /** 内存引擎无需修复(无持久化损坏概念) */ repair(): Promise; /** 清空全部数据与表结构 */ clearAll(): Promise; getMeta(key: string): Promise; setMeta(key: string, value: string): Promise; createTable(schema: TableSchema): Promise; dropTable(tableName: string): Promise; hasTable(tableName: string): Promise; getTableNames(): Promise; getTableSchema(tableName: string): Promise; /** * v0.4.2-fix: 引擎级 ALTER TABLE — 直接修改内存 schema 引用并清理行数据。 * (此前走 executor 通用路径,行为相同;统一到引擎层保证 Hybrid/IndexedDB 委托一致性) */ alterTable(tableName: string, action: 'ADD' | 'DROP', column: ColumnDef & { name: string; }): Promise; insert(tableName: string, rows: Record[]): Promise; /** v0.7.3: 按主键取已验证行(KVStoreEngine 持久化 validated 行用,含 default/类型归一) */ getRow(tableName: string, pkValue: string): Record | null; find(tableName: string, query: QueryPlan): Promise[]>; /** v0.4.0: 流式查询 — 逐行回调(单次迭代,不物化结果数组) */ findStream(tableName: string, query: QueryPlan, onRow: (row: Record) => void): Promise; update(tableName: string, query: QueryPlan, updates: Record): Promise; /** * 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; /** * v0.6.3: RESTRICT 预检(delete 级联两阶段之一)。 * 递归沿 CASCADE 链检查引用表:RESTRICT 引用存在依赖行则抛 FOREIGN_KEY_VIOLATION。 */ private checkCascadeRestrict; count(tableName: string, query?: QueryPlan): Promise; clear(tableName: string): Promise; createIndex(tableName: string, column: string, unique?: boolean): Promise; dropIndex(tableName: string, column: string, _indexName?: string): Promise; beginTransaction(): Promise; commitTransaction(): Promise; rollbackTransaction(): Promise; private deepCloneMapMap; private deepCloneIndexes; private ensureTable; private getPrimaryKey; private validateRow; /** * 取该 schema 的行校验器(每次调用重新编译)。 * * 不缓存在引擎字段上:`alterTable` 会原地修改 schema 对象, * 长期缓存会继续用过期列定义("加了列却仍被当未知列"这类难查问题)。 * 编译本身只是 `Object.entries` + Set 构造,相对一次 INSERT 的索引维护可忽略。 */ private rowValidator; /** * v0.8.0(B-1):写入前置校验(见 `IStorageEngine.validatePayload` 契约)。 * * 引擎在 `insert` / `update` 内部**同样**会校验 —— 本方法只是让 Executor 与 * QueryBuilder 能在"开始写入之前"拿到同一套判定结果,从而: * - 多行 INSERT 的预检发生在任何副作用之前(错误信息带列名清单); * - 直通路径与 SQL 路径不可能给出不同结论(同一个 `compileValidator`)。 */ validatePayload(tableName: string, rows: Record[], mode?: 'insert' | 'update'): Promise; /** 索引查找 */ private tryIndexLookup; /** 更新索引 */ private updateIndexes; /** v0.3.3: 从所有索引中移除一行的条目(update/delete 前调用,修复索引过期/残留) */ private removeIndexEntries; /** * 级联删除:查找引用 tableName.pkValue 的所有表的行并删除。 * v0.6.1-fix: 环路保护(A→B→A 级联环不再无限递归栈溢出,AriaEngine 同语义)。 * @returns 级联删除的行数 */ private cascadeDelete; } /** * AriaEngine Storage Backend — 存储后端抽象层 * @module engine/aria/store/backend * * 封装底层浏览器存储 API(IndexedDB / OPFS / Memory 回退), * 供 Buffer Pool 的 PageIO 和 WAL 的 WALStore 使用。 */ interface IStorageBackend { /** 打开存储 */ open(name: string): Promise; /** 关闭存储 */ close(): Promise; /** 是否已打开 */ isOpen(): boolean; /** 读取数据块 */ read(key: string): Promise; /** 写入数据块 */ write(key: string, data: ArrayBuffer): Promise; /** * 追加写入(v0.4.5 WAL 分片用,可选): * - OPFS 后端实现真追加(createWritable keepExistingData + seek,O(chunk)) * - 未实现的后端由调用方回退 read+write(EncryptedBackend 包装时整体重写保正确性) * 语义:在 key 现有内容末尾追加 data;key 不存在时等同 write。 */ append?(key: string, data: ArrayBuffer): Promise; /** * 批量原子写入(v0.4.2-fix):多个 key 在单个底层事务中提交, * 中断时整体回滚,不留半写状态。WAL count 与记录同事务保证一致性。 */ writeMany(entries: Record): Promise; /** 删除数据块 */ delete(key: string): Promise; /** * 批量原子删除(v0.4.2-fix):多个 key 在单个底层事务中提交。 */ deleteMany(keys: string[]): Promise; /** 列出所有 key */ listKeys(): Promise; /** 检查 key 是否存在 */ exists(key: string): Promise; /** 清空所有数据 */ clear(): Promise; } 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; close(): Promise; isOpen(): boolean; /** * v0.6.0: 从 KVStore 重新加载全部数据到内存(多标签页同步重载用)。 * Hybrid 引擎的 reloadMemoryFromDisk 依赖磁盘引擎"读穿透", * KVStoreEngine 读内存 → 提供 reload 重新加载磁盘最新数据。 */ reload(): Promise; /** v0.4.2-fix: 自愈 — 校验 KVStore 日志/快照完整性并重建内存 */ repair(): Promise; clearAll(): Promise; getMeta(key: string): Promise; setMeta(key: string, value: string): Promise; createTable(schema: TableSchema): Promise; dropTable(tableName: string): Promise; hasTable(tableName: string): Promise; getTableNames(): Promise; getTableSchema(tableName: string): Promise; alterTable(tableName: string, action: 'ADD' | 'DROP', column: ColumnDef & { name: string; }): Promise; /** * v0.8.0(B-1):写入前置校验 —— 委托给内存引擎(两者共享同一 schema 表)。 * * KVStore/Hybrid 的行校验一直"继承"自 MemoryEngine,这正是 A12 的成因: * 三者共用一份**缺 maxLength/min/max** 的实现。现在共享的是 * `table/validation.ts` 的规范实现,继承关系不再影响约束覆盖面。 */ validatePayload(tableName: string, rows: Record[], mode?: 'insert' | 'update'): Promise; insert(tableName: string, rows: Record[]): Promise; find(tableName: string, query: QueryPlan): Promise[]>; findStream(tableName: string, query: QueryPlan, onRow: (row: Record) => void): Promise; update(tableName: string, query: QueryPlan, updates: Record): Promise; delete(tableName: string, query: QueryPlan): Promise; count(tableName: string, query?: QueryPlan): Promise; clear(tableName: string): Promise; createIndex(tableName: string, column: string, unique?: boolean): Promise; dropIndex(tableName: string, column: string, indexName?: string): Promise; beginTransaction(): Promise; commitTransaction(): Promise; rollbackTransaction(): Promise; 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 残留行删除。 * 调用方合并到单次原子 writeBatch(put+delete 同一条日志记录,多表操作真原子)。 */ private collectTableDiff; } declare class AriaEngine implements IStorageEngine { readonly name = "aria"; private config; private lsm; private wal; private checkpointManager; private backend; private opened; private dbName; private fileManager; private bufferPool; private dbLock; 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; /** open 内部实现(错误包装在 open 外层) */ private openInternal; close(): Promise; /** * v0.4.2-fix: 崩溃恢复/自愈 — 校验并移除损坏 SSTable、截断 WAL、重建二级索引。 * v0.4.5 增强:清理 OPFS 残留临时文件、清理孤儿页面(meta 未引用的 pg_ 文件)。 * 应用层检测到异常后调用,无需删库重建。 */ repair(): Promise; /** * v0.4.5: 清理孤儿页面 — 扫描全部 pg_* 文件,未被任何 LSM 命名空间 meta 引用的删除。 * 孤儿页面来自:崩溃中断的 compaction/删除流程(旧 SSTable 页面残留)。 */ private cleanupOrphanPages; /** * v0.4.1: 重置数据库 — 清空全部数据与表结构(演示页刷新/重新初始化用)。 * 清空存储后端、LSM、WAL、MVCC 与二级索引,后续可继续使用本实例。 */ clearAll(): Promise; isOpen(): boolean; getMeta(key: string): Promise; setMeta(key: string, value: string): Promise; createTable(schema: TableSchema): Promise; dropTable(tableName: string): Promise; /** * v0.4.2-fix: 清理指定表的全部二级索引 LSM(内存 + 存储文件 + meta)。 * dropTable / DROP_TABLE 恢复 / alterTable DROP 索引列 共用。 */ private cleanupTableIndexes; hasTable(tableName: string): Promise; getTableNames(): Promise; getTableSchema(tableName: string): Promise; insert(tableName: string, rows: Record[]): Promise; find(tableName: string, query: QueryPlan): Promise[]>; update(tableName: string, query: QueryPlan, updates: Record): Promise; /** * 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; /** * 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) => void): Promise; count(tableName: string, query?: QueryPlan): Promise; clear(tableName: string): Promise; /** * v0.4.1: ALTER TABLE — 结构变更真正生效于存储: * - ADD: 持久化 schema(persistSchemas),行无需修改 * - DROP: 持久化 schema + 遍历主 LSM 重写所有行(移除该列键)+ WAL UPDATE 记录 * (通用路径 getTableSchema 返回副本,Executor 的引用修改对 Aria 无效) */ alterTable(tableName: string, action: 'ADD' | 'DROP', column: ColumnDef & { name: string; }): Promise; createIndex(tableName: string, column: string, unique?: boolean): Promise; dropIndex(tableName: string, column: string, _indexName?: string): Promise; beginTransaction(): Promise; commitTransaction(): Promise; rollbackTransaction(): Promise; private savepoints; /** * v0.8.0: 当前事务在 WAL 中已成功追加的记录条数。 * * 用途:保存点需要记录"回滚后应保留到哪一条",否则恢复时无法区分 * "保存点之前的写入"(应保留)与"保存点之后的写入"(应丢弃)。 */ private txnWalRecordCount; /** 保存点 → 该保存点时事务的 WAL 记录边界 */ private savepointWalBoundary; savepoint(name: string): Promise; rollbackToSavepoint(name: string): Promise; releaseSavepoint(name: string): Promise; backup(): Promise[]>>; private getAllRows; /** * v0.3.3: 将事务未提交快照的变更合并到行列表(新增/更新/删除标记)。 * 幂等操作:行已是最新时不重复修改。 */ private mergeTxnSnapshot; private getPK; private validateRow; /** * v0.8.0(B-1):写入前置校验 —— 见 `IStorageEngine.validatePayload` 契约。 */ validatePayload(tableName: string, rows: Record[], mode?: 'insert' | 'update'): Promise; private persistSchemas; private loadSchemas; /** * 创建命名空间隔离的 SSTableStore。 * * 主 LSM 与每个二级索引 LSM 各持有独立实例: * - 文件 key 前缀隔离(sst_ / sst_idx_${table}_${col}_) * - 元数据 key 隔离(__aria_lsm_meta / __aria_lsm_meta_${ns}) * - id 序列独立(避免 v0.2.4 共享 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>; /** * REINDEX: 重建指定表的所有二级索引 */ reindexTable(tableName: string): Promise; /** 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; /** * 从磁盘重载内存缓存(v0.3.2:多标签页同步)。 * 其他标签页写入磁盘后调用,使本标签页读到最新数据。 */ reloadMemoryFromDisk(): Promise; close(): Promise; isOpen(): boolean; /** 自愈:修复磁盘引擎后重载内存缓存 */ repair(): Promise; /** 清空全部数据与表结构 */ clearAll(): Promise; getMeta(key: string): Promise; setMeta(key: string, value: string): Promise; createTable(schema: TableSchema): Promise; dropTable(tableName: string): Promise; hasTable(tableName: string): Promise; getTableNames(): Promise; getTableSchema(tableName: string): Promise; /** v0.4.2-fix: 引擎级 ALTER TABLE — 双引擎同步(磁盘持久化 + 内存引用) */ alterTable(tableName: string, action: 'ADD' | 'DROP', column: ColumnDef & { name: string; }): Promise; /** * v0.7.2: 磁盘写失败补偿 — 内存已先行写入、磁盘失败 → 内存与磁盘不一致 * (重启后数据丢失且调用方已收到错误)。从磁盘重载内存对齐真实状态 * (内存=磁盘),再重新抛出原始错误。事务路径由双引擎快照回滚保证, * 无需此补偿。 */ private recoverMemoryAfterDiskError; /** * v0.8.0(B-1):写入前置校验 —— 委托给内存引擎(与磁盘引擎同 schema)。 * * 关键点:**只判定一次**。Hybrid 的 write-through 会把同一批行先写内存再写磁盘, * 两个引擎各自校验会给出同一结论(现在共享同一个 `compileValidator`), * 但由本方法统一前置,可保证多行批量在任何副作用之前整体失败。 */ validatePayload(tableName: string, rows: Record[], mode?: 'insert' | 'update'): Promise; insert(tableName: string, rows: Record[]): Promise; find(tableName: string, query: QueryPlan): Promise[]>; /** v0.4.0: 流式查询(内存引擎逐行回调) */ findStream(tableName: string, query: QueryPlan, onRow: (row: Record) => void): Promise; update(tableName: string, query: QueryPlan, updates: Record): Promise; delete(tableName: string, query: QueryPlan): Promise; count(tableName: string, query?: QueryPlan): Promise; clear(tableName: string): Promise; createIndex(tableName: string, column: string, unique?: boolean): Promise; dropIndex(tableName: string, column: string, indexName?: string): Promise; beginTransaction(): Promise; commitTransaction(): Promise; rollbackTransaction(): Promise; /** 获取磁盘引擎类型 */ 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/append:createWritable 为 copy-on-write —— close 前崩溃旧文件保持不变, * close 后原子替换(单文件写入原子) * - 多文件 writeMany/deleteMany:OPFS 无跨文件事务,串行逐个落盘;调用方(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; close(): Promise; isOpen(): boolean; /** 清理崩溃残留的临时文件(open 时自动调用,repair 也可调用) */ cleanupStaleFiles(): Promise; read(key: string): Promise; /** * 单文件原子写:createWritable 为 copy-on-write,close 后原子替换; * 写入期间崩溃 → 旧文件保持(原子性由浏览器 OPFS 实现保证)。 */ write(key: string, data: ArrayBuffer): Promise; /** * v0.4.5: 真追加写 — createWritable(keepExistingData) + seek 到文件末尾。 * 单文件 COW 原子(close 前崩溃旧文件保持),无需读旧内容即实现 O(chunk) 追加 * (WAL 分片高频写入用)。 */ append(key: string, data: ArrayBuffer): Promise; /** v0.4.2-fix: 批量写入 — 串行队列内逐个落盘(OPFS 无跨文件事务,顺序保证一致) */ writeMany(entries: Record): Promise; delete(key: string): Promise; /** v0.4.2-fix: 批量删除 — 串行队列内逐个删除 */ deleteMany(keys: string[]): Promise; listKeys(): Promise; exists(key: string): Promise; clear(): Promise; } /** * 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; 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, OPFSBackend, OrderBy, PluginManager, QueryPlan, SelectStatement, Statement, StorageMode, Table, TableSchema, Transaction, TransactionManager, UpdateStatement, VERSION, WhereCondition, WhereOperator, api, bindParameters, create, api as default, parse, parseAll, parseWhereCondition, tokenize };