Files
MetonaSqlark/dist/metona-sqlark.cjs
T
thzxx cff98b0903
CI / test (18.x) (push) Successful in 10m3s
CI / test (20.x) (push) Successful in 10m4s
CI / test (22.x) (push) Successful in 9m58s
CI / test (24.x) (push) Successful in 9m56s
release: v0.4.4 — SSTable 编码修复(UTF-8 字节估算 + u32 长度字段 + v1/v2 双格式兼容),大内容不再崩溃
2026-08-09 21:34:28 +08:00

11140 lines
430 KiB
JavaScript
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.
'use strict';
Object.defineProperty(exports, '__esModule', { value: true });
/**
* metona-sqlark Constants — 类型定义 / 默认配置 / 枚举
* @module constants
*/
/** 所有存储模式 */
/** 所有字段类型 */
const FIELD_TYPES = ['string', 'number', 'boolean', 'date', 'json'];
/** 数据库默认配置 */
const DB_DEFAULTS = Object.freeze({
name: 'metona-sqlark',
mode: 'hybrid',
diskEngine: 'indexeddb',
version: 1,
maxRowsPerQuery: 0, // 0 = 不限制
debug: false,
multiTabSync: false,
});
// ---------------------------------------------------------------------------
// 错误类型
// ---------------------------------------------------------------------------
/** 数据库错误 */
class DatabaseError extends Error {
constructor(message, code, details) {
super(message);
this.code = code;
this.details = details;
this.name = 'DatabaseError';
}
}
// ---------------------------------------------------------------------------
// 版本
// ---------------------------------------------------------------------------
const VERSION = '0.4.4';
/**
* metona-sqlark Shared WHERE Matcher — 统一的条件匹配逻辑
* @module query/where-matcher
*
* MemoryEngine / IndexedDBEngine / QueryExecutor 共享此模块,
* 消除 220+ 行重复代码,统一 $and/$or/$not/$col 行为。
*/
// ---------------------------------------------------------------------------
// LIKE 正则缓存
// ---------------------------------------------------------------------------
const likeCache = new Map();
function compileLikeRegex(pattern) {
const cached = likeCache.get(pattern);
if (cached)
return cached;
const escaped = pattern
.replace(/[.+^${}()|[\]\\]/g, '\\$&')
.replace(/%/g, '.*')
.replace(/_/g, '.');
const regex = new RegExp(`^${escaped}$`, 'i');
likeCache.set(pattern, regex);
return regex;
}
// ---------------------------------------------------------------------------
// WHERE 匹配(顶层入口)
// ---------------------------------------------------------------------------
/**
* 匹配完整 WHERE 条件
* @param row 当前数据行
* @param where WHERE 条件对象
* @param options.$col 是否启用 $col 列引用解析
*/
function matchWhere(row, where, options = {}) {
for (const [field, condition] of Object.entries(where)) {
// 顶层 $caseResultv0.3.2):由 Executor 对 CASE WHEN 表达式逐行求值后产生
if (field === '$caseResult') {
if (condition !== true)
return false;
continue;
}
// 顶层 $existsv0.3.0):由 Executor.resolveSubqueries 解析为 boolean
if (field === '$exists') {
if (condition !== true)
return false;
continue;
}
// 顶层 $and
if (field === '$and') {
const subs = condition;
if (!subs.every((sub) => matchWhere(row, sub, options)))
return false;
continue;
}
// 顶层 $or
if (field === '$or') {
const subs = condition;
if (!subs.some((sub) => matchWhere(row, sub, options)))
return false;
continue;
}
// 顶层 $notv0.3.2 修复:NOT (expr) 生成的 { $not: inner }
if (field === '$not') {
if (matchWhere(row, condition, options))
return false;
continue;
}
if (!matchField(row[field], condition, row, options))
return false;
}
return true;
}
// ---------------------------------------------------------------------------
// 字段匹配
// ---------------------------------------------------------------------------
function matchField(value, condition, row, options) {
// 嵌套 $and
if (typeof condition === 'object' && condition !== null && '$and' in condition) {
const subs = condition.$and;
return subs.every((sub) => matchWhere(row, sub, options));
}
// 嵌套 $or
if (typeof condition === 'object' && condition !== null && '$or' in condition) {
const subs = condition.$or;
return subs.some((sub) => matchWhere(row, sub, options));
}
// $not
if (typeof condition === 'object' && condition !== null && '$not' in condition) {
return !matchField(value, condition.$not, row, options);
}
// 简单值 => $eq
if (typeof condition !== 'object' || condition === null || Array.isArray(condition)) {
return value === condition;
}
const ops = condition;
// $col 简写: { $col: name } === { $eq: { $col: name } }(仅 JOIN ON 场景)
if (options.$col && '$col' in ops && Object.keys(ops).length === 1) {
return value === row[ops.$col];
}
// 遍历操作符
for (const [op, operand] of Object.entries(ops)) {
let actualOperand = operand;
// $col 列引用解析
if (options.$col && typeof operand === 'object' && operand !== null && '$col' in operand) {
actualOperand = row[operand.$col];
}
if (!matchOperator(value, op, actualOperand))
return false;
}
return true;
}
// ---------------------------------------------------------------------------
// 操作符匹配
// ---------------------------------------------------------------------------
function matchOperator(value, op, operand) {
switch (op) {
case '$eq': return value === operand;
case '$ne': return value !== operand;
case '$gt': return value > operand;
case '$gte': return value >= operand;
case '$lt': return value < operand;
case '$lte': return value <= operand;
case '$in': return Array.isArray(operand) && operand.includes(value);
case '$nin': return Array.isArray(operand) && !operand.includes(value);
case '$like': return compileLikeRegex(String(operand)).test(String(value));
default: return true;
}
}
// ---------------------------------------------------------------------------
// 排序
// ---------------------------------------------------------------------------
function applyOrderBy(rows, orderBy) {
return [...rows].sort((a, b) => {
for (const { column, direction, nulls } of orderBy) {
const aNull = a[column] === null || a[column] === undefined;
const bNull = b[column] === null || b[column] === undefined;
// v0.4.0: NULLS FIRST/LAST 时 NULL 位置固定,不受升降序反转
if (nulls && (aNull || bNull)) {
if (aNull && bNull)
continue;
const cmp = nulls === 'first' ? (aNull ? -1 : 1) : (aNull ? 1 : -1);
return cmp;
}
const cmp = compare(a[column], b[column]);
if (cmp !== 0)
return direction === 'desc' ? -cmp : cmp;
}
return 0;
});
}
function compare(a, b) {
if (a === b)
return 0;
if (a === null || a === undefined)
return 1;
if (b === null || b === undefined)
return -1;
if (typeof a === 'string' && typeof b === 'string')
return a.localeCompare(b);
if (typeof a === 'number' && typeof b === 'number')
return a - b;
return String(a).localeCompare(String(b));
}
// ---------------------------------------------------------------------------
// 列投影
// ---------------------------------------------------------------------------
function projectColumns(row, columns) {
const projected = {};
for (const col of columns) {
if (col in row) {
projected[col] = row[col];
}
else {
for (const key of Object.keys(row)) {
if (key.endsWith(`.${col}`) || key === col) {
projected[col] = row[key];
break;
}
}
}
}
return projected;
}
/**
* metona-sqlark Memory Engine — 基于 Map 的内存存储引擎
* @module engine/memory
*/
class MemoryEngine {
constructor() {
this.name = 'memory';
this.tables = new Map();
this.schemas = new Map();
this.indexes = new Map();
this.opened = false;
/** v0.4.2-fix: 库内元数据(迁移版本持久化用) */
this.metaStore = new Map();
// ---- 事务快照 ----
this.snapshot = null;
}
// ---- 生命周期 ----
async open(_dbName, _version) {
if (this.opened) {
// 幂等:已打开则忽略
return;
}
this.opened = true;
}
async close() {
this.tables.clear();
this.schemas.clear();
this.indexes.clear();
this.metaStore.clear();
this.opened = false;
}
isOpen() { return this.opened; }
// ---- v0.4.2-fix: 自愈 / 重置 / 元数据 ----
/** 内存引擎无需修复(无持久化损坏概念) */
async repair() { return; }
/** 清空全部数据与表结构 */
async clearAll() {
const names = Array.from(this.schemas.keys());
for (const name of names) {
await this.dropTable(name);
}
this.metaStore.clear();
}
async getMeta(key) {
return this.metaStore.get(key) ?? null;
}
async setMeta(key, value) {
this.metaStore.set(key, value);
}
// ---- 表管理 ----
async createTable(schema) {
if (this.schemas.has(schema.name))
throw new DatabaseError(`Table "${schema.name}" already exists`, 'TABLE_EXISTS');
// v0.4.2-fix: 存储 schema 深拷贝 — 此前 Hybrid.reloadMemoryFromDisk 直接存入
// disk 引擎的 schema 引用,内存/磁盘引擎共享同一对象,任一引擎 ALTER 都会污染对方
const copy = { name: schema.name, columns: {} };
for (const [colName, colDef] of Object.entries(schema.columns)) {
copy.columns[colName] = { ...colDef };
}
this.schemas.set(schema.name, copy);
this.tables.set(schema.name, new Map());
const tableIndexes = new Map();
for (const [colName, colDef] of Object.entries(copy.columns)) {
if (colDef.index || colDef.unique)
tableIndexes.set(colName, new Map());
}
this.indexes.set(schema.name, tableIndexes);
}
async dropTable(tableName) {
this.ensureTable(tableName);
this.schemas.delete(tableName);
this.tables.delete(tableName);
this.indexes.delete(tableName);
}
async hasTable(tableName) { return this.schemas.has(tableName); }
async getTableNames() { return Array.from(this.schemas.keys()); }
async getTableSchema(tableName) { return this.schemas.get(tableName) ?? null; }
/**
* v0.4.2-fix: 引擎级 ALTER TABLE — 直接修改内存 schema 引用并清理行数据。
* (此前走 executor 通用路径,行为相同;统一到引擎层保证 Hybrid/IndexedDB 委托一致性)
*/
async alterTable(tableName, action, column) {
this.ensureTable(tableName);
const schema = this.schemas.get(tableName);
if (action === 'ADD') {
if (schema.columns[column.name]) {
throw new DatabaseError(`Column "${column.name}" already exists in table "${tableName}"`, 'COLUMN_EXISTS');
}
schema.columns[column.name] = column;
return;
}
if (!schema.columns[column.name]) {
throw new DatabaseError(`Column "${column.name}" does not exist in table "${tableName}"`, 'COLUMN_NOT_FOUND');
}
delete schema.columns[column.name];
// 清理已有行中该列的值(find 返回行引用,直接删除生效)
const table = this.tables.get(tableName);
for (const row of table.values()) {
if (column.name in row)
delete row[column.name];
}
}
// ---- CRUD ----
async insert(tableName, rows) {
this.ensureTable(tableName);
const schema = this.schemas.get(tableName);
const table = this.tables.get(tableName);
const pkColumn = this.getPrimaryKey(schema);
const pks = [];
for (const row of rows) {
const validatedRow = this.validateRow(schema, row);
const pkValue = String(validatedRow[pkColumn]);
if (table.has(pkValue))
throw new DatabaseError(`Duplicate primary key "${pkValue}" in table "${tableName}"`, 'DUPLICATE_KEY');
this.checkUniqueness(schema, validatedRow);
table.set(pkValue, validatedRow);
this.updateIndexes(tableName, validatedRow, pkValue);
pks.push(pkValue);
}
return pks;
}
async find(tableName, query) {
this.ensureTable(tableName);
const table = this.tables.get(tableName);
let results = this.tryIndexLookup(tableName, table, query);
if (query.where && Object.keys(query.where).length > 0) {
results = results.filter((row) => matchWhere(row, query.where));
}
if (query.orderBy && query.orderBy.length > 0) {
results = applyOrderBy(results, query.orderBy);
}
const offset = query.offset ?? 0;
const limit = query.limit ?? results.length;
results = results.slice(offset, offset + limit);
if (query.columns && query.columns.length > 0 && query.columns[0] !== '*') {
results = results.map((row) => projectColumns(row, query.columns));
}
return results;
}
/** v0.4.0: 流式查询 — 逐行回调(单次迭代,不物化结果数组) */
async findStream(tableName, query, onRow) {
this.ensureTable(tableName);
const table = this.tables.get(tableName);
const hasWhere = !!(query.where && Object.keys(query.where).length > 0);
const limit = query.limit ?? Infinity;
const offset = query.offset ?? 0;
const project = query.columns && query.columns.length > 0 && query.columns[0] !== '*'
? (row) => projectColumns(row, query.columns)
: null;
let count = 0;
let skipped = 0;
for (const row of table.values()) {
if (hasWhere && !matchWhere(row, query.where))
continue;
if (skipped < offset) {
skipped++;
continue;
}
onRow(project ? project(row) : row);
count++;
if (count >= limit)
break;
}
return count;
}
async update(tableName, query, updates) {
this.ensureTable(tableName);
const schema = this.schemas.get(tableName);
const table = this.tables.get(tableName);
const pkCol = this.getPrimaryKey(schema);
let count = 0;
// v0.4.2-fix: 迭代期间会 delete/set 同一 Map(主键变更)→ 拷贝快照避免跳过/重复
for (const [pk, row] of [...table]) {
if (!query.where || Object.keys(query.where).length === 0 || matchWhere(row, query.where)) {
// v0.3.3: 先移除旧值索引条目(修复 update 后唯一约束被绕过、按新值查索引丢行)
this.removeIndexEntries(tableName, row, pk);
const updated = { ...row, ...updates };
this.validateRow(schema, updated);
this.checkUniqueness(schema, updated);
const newPk = String(updated[pkCol]);
// v0.4.2-fix: 主键变更 — 删除旧键 + 级联更新引用表 + 新键落表
if (newPk !== pk) {
await this.applyUpdateCascade(tableName, pk, newPk);
}
table.delete(pk);
table.set(newPk, updated);
this.updateIndexes(tableName, updated, newPk);
count++;
}
}
return count;
}
/**
* v0.4.2-fix: ON UPDATE 外键级联 — 被引用表主键变更时处理引用表:
* RESTRICT 抛错 / CASCADE 更新 FK 值 / SET NULL 置空。
* 分两阶段:先全量 RESTRICT 检查(任何修改前),再执行级联(防部分修改)。
*/
async applyUpdateCascade(tableName, oldPk, newPk) {
// 阶段 1: RESTRICT 检查(引用旧主键的行存在即拒绝)
for (const [refTableName, refSchema] of this.schemas) {
if (refTableName === tableName)
continue;
for (const [colName, colDef] of Object.entries(refSchema.columns)) {
if (!colDef.references || !colDef.onUpdate)
continue;
const [refTable] = colDef.references.split('.');
if (refTable !== tableName)
continue;
const refTableData = this.tables.get(refTableName);
if (!refTableData)
continue;
for (const [, refRow] of refTableData) {
if (String(refRow[colName]) === oldPk && colDef.onUpdate === 'RESTRICT') {
throw new DatabaseError(`Cannot update "${tableName}" key "${oldPk}": foreign key "${colName}" in "${refTableName}" has dependent rows`, 'FOREIGN_KEY_VIOLATION');
}
}
}
}
// 阶段 2: CASCADE / SET NULL
for (const [refTableName, refSchema] of this.schemas) {
if (refTableName === tableName)
continue;
for (const [colName, colDef] of Object.entries(refSchema.columns)) {
if (!colDef.references || !colDef.onUpdate)
continue;
const [refTable] = colDef.references.split('.');
if (refTable !== tableName)
continue;
const refTableData = this.tables.get(refTableName);
if (!refTableData)
continue;
if (colDef.onUpdate !== 'CASCADE' && colDef.onUpdate !== 'SET NULL')
continue;
for (const [refPk, refRow] of refTableData) {
if (String(refRow[colName]) !== oldPk)
continue;
this.removeIndexEntries(refTableName, refRow, refPk);
refRow[colName] = colDef.onUpdate === 'CASCADE' ? newPk : null;
this.updateIndexes(refTableName, refRow, refPk);
}
}
}
}
async delete(tableName, query) {
this.ensureTable(tableName);
const table = this.tables.get(tableName);
const toDelete = [];
for (const [pk, row] of table) {
if (!query.where || Object.keys(query.where).length === 0 || matchWhere(row, query.where)) {
// v0.3.3: 删除行前清理其索引条目(修复删除后索引残留)
this.removeIndexEntries(tableName, row, pk);
toDelete.push(pk);
}
}
// 级联删除:检查引用此表的其他表
let cascadeCount = 0;
for (const pk of toDelete) {
const row = table.get(pk);
if (row)
cascadeCount += await this.cascadeDelete(tableName, pk, row);
}
for (const pk of toDelete)
table.delete(pk);
return toDelete.length + cascadeCount;
}
async count(tableName, query) {
this.ensureTable(tableName);
const table = this.tables.get(tableName);
if (!query?.where || Object.keys(query.where).length === 0)
return table.size;
let result = 0;
for (const row of table.values()) {
if (matchWhere(row, query.where))
result++;
}
return result;
}
async clear(tableName) {
this.ensureTable(tableName);
this.tables.get(tableName).clear();
const tableIndexes = this.indexes.get(tableName);
if (tableIndexes)
for (const colIndex of tableIndexes.values())
colIndex.clear();
}
// ---- 动态索引(v0.3.0 ----
async createIndex(tableName, column, unique) {
this.ensureTable(tableName);
const schema = this.schemas.get(tableName);
const colDef = schema.columns[column];
if (!colDef)
throw new DatabaseError(`Column "${column}" does not exist in table "${tableName}"`, 'COLUMN_NOT_FOUND');
if (colDef.index || colDef.unique)
return; // 已存在
colDef.index = true;
if (unique)
colDef.unique = true;
const tableIndexes = this.indexes.get(tableName);
if (!tableIndexes.has(column))
tableIndexes.set(column, new Map());
const colIndex = tableIndexes.get(column);
const table = this.tables.get(tableName);
for (const [pk, row] of table) {
const value = row[column];
if (value !== undefined && value !== null) {
if (!colIndex.has(value))
colIndex.set(value, new Set());
colIndex.get(value).add(pk);
}
}
}
async dropIndex(tableName, column, _indexName) {
this.ensureTable(tableName);
const schema = this.schemas.get(tableName);
const colDef = schema.columns[column];
if (!colDef)
throw new DatabaseError(`Column "${column}" does not exist in table "${tableName}"`, 'COLUMN_NOT_FOUND');
// v0.4.1: DROP 不存在的索引应报错(此前静默成功)
if (!colDef.index && !colDef.unique) {
throw new DatabaseError(`Index on column "${column}" does not exist in table "${tableName}"`, 'INDEX_NOT_FOUND');
}
colDef.index = false;
colDef.unique = false;
const tableIndexes = this.indexes.get(tableName);
if (tableIndexes)
tableIndexes.delete(column);
}
// ---- 事务 ----
async beginTransaction() {
if (this.snapshot)
throw new DatabaseError('Transaction already in progress', 'TX_ACTIVE');
this.snapshot = {
tables: this.deepCloneMapMap(this.tables),
schemas: new Map(this.schemas),
indexes: this.deepCloneIndexes(this.indexes),
};
}
async commitTransaction() {
if (!this.snapshot)
throw new DatabaseError('No active transaction', 'TX_NONE');
this.snapshot = null;
}
async rollbackTransaction() {
if (!this.snapshot)
throw new DatabaseError('No active transaction', 'TX_NONE');
this.tables = this.snapshot.tables;
this.schemas = this.snapshot.schemas;
this.indexes = this.snapshot.indexes;
this.snapshot = null;
}
// ---- 事务快照辅助 ----
deepCloneMapMap(source) {
const clone = new Map();
for (const [k, v] of source) {
const innerClone = new Map();
for (const [ik, iv] of v)
innerClone.set(ik, { ...iv });
clone.set(k, innerClone);
}
return clone;
}
deepCloneIndexes(source) {
const clone = new Map();
for (const [tableName, tableIndexes] of source) {
const tableClone = new Map();
for (const [col, colIndex] of tableIndexes) {
const colClone = new Map();
for (const [val, pkSet] of colIndex)
colClone.set(val, new Set(pkSet));
tableClone.set(col, colClone);
}
clone.set(tableName, tableClone);
}
return clone;
}
// ---- 内部辅助 ----
ensureTable(tableName) {
if (!this.tables.has(tableName))
throw new DatabaseError(`Table "${tableName}" does not exist`, 'TABLE_NOT_FOUND');
}
getPrimaryKey(schema) {
for (const [name, col] of Object.entries(schema.columns)) {
if (col.primaryKey)
return name;
}
return Object.keys(schema.columns)[0];
}
validateRow(schema, row) {
const validated = {};
for (const [colName, colDef] of Object.entries(schema.columns)) {
let value = row[colName];
if (value === undefined && colDef.default !== undefined)
value = colDef.default;
if (colDef.required && (value === undefined || value === null)) {
throw new DatabaseError(`Column "${colName}" is required in table "${schema.name}"`, 'VALIDATION_ERROR');
}
if (value !== undefined && value !== null)
this.checkType(colName, colDef.type, value);
if (value !== undefined)
validated[colName] = value;
}
return validated;
}
checkType(colName, type, value) {
const jsType = typeof value;
switch (type) {
case 'string':
if (jsType !== 'string')
throw new DatabaseError(`Column "${colName}" expects string, got ${jsType}`, 'TYPE_ERROR');
break;
case 'number':
if (jsType !== 'number')
throw new DatabaseError(`Column "${colName}" expects number, got ${jsType}`, 'TYPE_ERROR');
break;
case 'boolean':
if (jsType !== 'boolean')
throw new DatabaseError(`Column "${colName}" expects boolean, got ${jsType}`, 'TYPE_ERROR');
break;
case 'date':
if (jsType !== 'string' || isNaN(Date.parse(value)))
throw new DatabaseError(`Column "${colName}" expects valid date`, 'TYPE_ERROR');
break;
case 'json':
if (jsType !== 'object')
throw new DatabaseError(`Column "${colName}" expects object/array, got ${jsType}`, 'TYPE_ERROR');
break;
}
}
/** O(1) 唯一性检查:利用哈希索引 */
checkUniqueness(schema, row) {
const tableIndexes = this.indexes.get(schema.name);
if (!tableIndexes)
return;
for (const [colName, colDef] of Object.entries(schema.columns)) {
if (!colDef.unique || row[colName] === undefined || row[colName] === null)
continue;
const colIndex = tableIndexes.get(colName);
if (colIndex && colIndex.has(row[colName])) {
throw new DatabaseError(`Unique constraint violation on column "${colName}" in table "${schema.name}"`, 'UNIQUE_VIOLATION');
}
}
}
/** 索引查找 */
tryIndexLookup(tableName, table, query) {
const tableIndexes = this.indexes.get(tableName);
if (!tableIndexes || !query.where)
return Array.from(table.values());
for (const [col, condition] of Object.entries(query.where)) {
// v0.4.1: 支持 { $eq: value } 形式(SQL 解析器生成的等值条件)走索引
let targetValue;
if (typeof condition !== 'object' || condition === null) {
targetValue = condition;
}
else if ('$eq' in condition && Object.keys(condition).length === 1) {
targetValue = condition.$eq;
}
else {
continue;
}
const colIndex = tableIndexes.get(col);
if (colIndex) {
const pks = colIndex.get(targetValue);
if (pks) {
const result = [];
for (const pk of pks) {
const r = table.get(pk);
if (r)
result.push(r);
}
return result;
}
return [];
}
}
return Array.from(table.values());
}
/** 更新索引 */
updateIndexes(tableName, row, pk) {
const tableIndexes = this.indexes.get(tableName);
if (!tableIndexes)
return;
for (const [colName, colIndex] of tableIndexes) {
const value = row[colName];
if (value !== undefined && value !== null) {
if (!colIndex.has(value))
colIndex.set(value, new Set());
colIndex.get(value).add(pk);
}
}
}
/** v0.3.3: 从所有索引中移除一行的条目(update/delete 前调用,修复索引过期/残留) */
removeIndexEntries(tableName, row, pk) {
const tableIndexes = this.indexes.get(tableName);
if (!tableIndexes)
return;
for (const [colName, colIndex] of tableIndexes) {
const value = row[colName];
if (value !== undefined && value !== null) {
const pks = colIndex.get(value);
if (pks) {
pks.delete(pk);
if (pks.size === 0)
colIndex.delete(value);
}
}
}
}
// ---- 外键级联 ----
/**
* 级联删除:查找引用 tableName.pkValue 的所有表的行并删除。
* @returns 级联删除的行数
*/
async cascadeDelete(tableName, pkValue, _row) {
let totalCascade = 0;
for (const [refTableName, refSchema] of this.schemas) {
if (refTableName === tableName)
continue;
for (const [colName, colDef] of Object.entries(refSchema.columns)) {
if (!colDef.references || !colDef.onDelete)
continue;
const [refTable] = colDef.references.split('.');
if (refTable !== tableName)
continue;
const refTableData = this.tables.get(refTableName);
if (!refTableData)
continue;
// 查找所有引用此主键的行
const toDelete = [];
for (const [refPk, refRow] of refTableData) {
if (String(refRow[colName]) === pkValue) {
toDelete.push(refPk);
}
}
// RESTRICT: 存在引用行时禁止删除
if (colDef.onDelete === 'RESTRICT' && toDelete.length > 0) {
throw new DatabaseError(`Cannot delete from "${tableName}": foreign key "${colName}" in "${refTableName}" has dependent rows`, 'FOREIGN_KEY_VIOLATION');
}
if (colDef.onDelete === 'CASCADE') {
// 递归级联
for (const refPk of toDelete) {
const refRow = refTableData.get(refPk);
if (refRow) {
// v0.3.3: 级联删除前清理索引条目
this.removeIndexEntries(refTableName, refRow, refPk);
totalCascade += await this.cascadeDelete(refTableName, refPk, refRow);
}
refTableData.delete(refPk);
totalCascade++;
}
}
else if (colDef.onDelete === 'SET NULL') {
for (const refPk of toDelete) {
const refRow = refTableData.get(refPk);
if (refRow) {
// v0.3.3: 外键列置空后同步更新索引
if (refRow[colName] !== undefined && refRow[colName] !== null) {
const pks = this.indexes.get(refTableName)?.get(colName);
if (pks) {
pks.get(refRow[colName])?.delete(refPk);
}
}
refRow[colName] = null;
}
}
}
}
}
return totalCascade;
}
}
/**
* metona-sqlark IndexedDB Engine — 基于 IndexedDB 的持久化存储引擎
* @module engine/indexeddb
*
* v0.1.13: 支持事务 — beginTransaction 延迟 IDB 写入,commit 批量刷盘,rollback 恢复快照。
*/
class IndexedDBEngine {
constructor() {
this.name = 'indexeddb';
this.db = null;
this.dbName = '';
this.version = 1;
this.memoryCache = new MemoryEngine();
// ---- 事务状态 ----
this.txActive = false;
}
async open(dbName, version) {
// v0.4.2-fix (P0-3): version < 1 归一化为 1indexedDB.open(name, 0) 抛原生 TypeError
const normalizedVersion = version >= 1 ? Math.floor(version) : 1;
this.dbName = dbName;
this.version = normalizedVersion;
await this.memoryCache.open(dbName, normalizedVersion);
// v0.4.2-fix (P0-2/P2-8): 版本自适应打开 + blocked 重试
this.db = await this.openDatabaseWithRetry(dbName, normalizedVersion);
this.setupVersionChangeHandler();
// v0.4.2-fix (P2-7): 确保 schema/meta 持久化 store 存在(新库或旧库升级时创建),
// 否则迁移版本等库内元数据无处落盘
await this.ensureSchemaStore();
try {
// v0.3.2: reopen 后从 IDB 重建 schemaschema 此前只存内存缓存,重开连接即丢失)
await this.rebuildSchemaFromIDB();
}
catch (error) {
throw new DatabaseError(`Failed to restore schema for "${dbName}"`, 'IDB_SCHEMA_RESTORE_ERROR', error);
}
}
/** v0.4.2-fix: 多标签页冲突处理 — 其他标签页升级版本时自动关闭当前连接 */
setupVersionChangeHandler() {
if (!this.db)
return;
this.db.onversionchange = () => {
if (this.db) {
this.db.close();
this.db = null;
// eslint-disable-next-line no-console
console.warn(`[metona-sqlark] Database "${this.dbName}" was upgraded in another tab. Connection closed. Please re-open.`);
}
};
}
/**
* v0.4.2-fix: 打开 IndexedDB 连接。
* - P0-2: 请求版本低于库实际版本(VersionError)时,先无版本参数探测库当前版本,
* 再以实际版本重开(建表每张表版本号 +1,config.version 会过期)
* - P2-8: onblocked 为瞬时状态(另一连接短暂持有),等待后重试多次,超时才抛 IDB_BLOCKED
*/
async openDatabaseWithRetry(dbName, requestedVersion) {
const BLOCKED_RETRIES = 10;
let effectiveVersion = requestedVersion;
for (let attempt = 0; attempt < BLOCKED_RETRIES; attempt++) {
try {
return await this.openRequest(dbName, effectiveVersion, 200 + attempt * 150);
}
catch (error) {
const err = error;
if (err && err.name === 'VersionError') {
const currentVersion = await this.resolveCurrentVersion(dbName);
if (currentVersion >= 1 && currentVersion !== effectiveVersion) {
effectiveVersion = currentVersion;
this.version = currentVersion;
continue;
}
throw new DatabaseError(`Failed to open IndexedDB "${dbName}": version mismatch`, 'IDB_VERSION_ERROR', error);
}
if (err && err.name === 'BlockedError') {
// 另一连接短暂持有 → 等待后重试
await new Promise((resolve) => setTimeout(resolve, 50 * (attempt + 1)));
continue;
}
throw new DatabaseError(`Failed to open IndexedDB "${dbName}"`, 'IDB_OPEN_ERROR', error);
}
}
throw new DatabaseError(`IndexedDB "${dbName}" is blocked`, 'IDB_BLOCKED');
}
/**
* 发起一次 indexedDB.open 请求(success/error/blocked 三态收敛)。
* onblocked 不立即失败:阻塞解除后 success 仍会触发,仅超时兜底判失败,
* 避免"拒绝后连接迟到成功"泄漏未关闭的数据库连接。
*/
openRequest(dbName, version, timeoutMs) {
return new Promise((resolve, reject) => {
const request = indexedDB.open(dbName, version);
let settled = false;
const timeout = setTimeout(() => {
if (settled)
return;
settled = true;
// 兼容无 DOMException 构造环境
const blockedError = typeof DOMException !== 'undefined'
? new DOMException('IndexedDB open is blocked', 'BlockedError')
: Object.assign(new Error('IndexedDB open is blocked'), { name: 'BlockedError' });
reject(blockedError);
}, timeoutMs);
request.onsuccess = () => {
if (settled) {
// 超时判失败后连接迟到成功:立即关闭,避免阻塞后续版本升级
request.result.close();
return;
}
settled = true;
clearTimeout(timeout);
resolve(request.result);
};
request.onerror = () => {
if (settled)
return;
settled = true;
clearTimeout(timeout);
reject(request.error ?? new Error('Unknown IndexedDB open error'));
};
request.onblocked = () => {
// 保持等待,不拒绝(阻塞解除后 success 会触发;超时由 timer 兜底)
};
});
}
/** 无版本参数打开库,解析其当前实际版本号(随后立即关闭) */
resolveCurrentVersion(dbName) {
return new Promise((resolve, reject) => {
const request = indexedDB.open(dbName);
request.onsuccess = () => {
const actualVersion = request.result.version;
request.result.close();
resolve(actualVersion);
};
request.onerror = () => {
reject(request.error ?? new Error('Failed to resolve IndexedDB version'));
};
});
}
/**
* v0.4.2-fix (P2-7): 确保 __metona_schema store 存在。
* 新库(或版本升级前创建的旧库)没有该 store 时,通过一次版本升级创建,
* 使 getMeta/setMeta(迁移版本持久化)始终可用。
*/
async ensureSchemaStore() {
if (!this.db)
return;
if (this.db.objectStoreNames.contains('__metona_schema'))
return;
const newVersion = this.db.version + 1;
this.db.close();
this.db = await new Promise((resolve, reject) => {
const request = indexedDB.open(this.dbName, newVersion);
request.onupgradeneeded = () => {
const idb = request.result;
if (!idb.objectStoreNames.contains('__metona_schema')) {
idb.createObjectStore('__metona_schema', { keyPath: 'name' });
}
};
request.onsuccess = () => {
this.db = request.result;
this.setupVersionChangeHandler();
resolve(request.result);
};
request.onerror = () => reject(new DatabaseError('Failed to create schema store', 'IDB_UPGRADE_ERROR', request.error));
});
}
/**
* 从 IDB 恢复内存 schema
* 1. 优先读取持久化的 schema 记录('__metona_schema' storev0.3.2
* 2. 旧数据回退:从 objectStore 主键 / 索引 / 样例数据推断
*/
async rebuildSchemaFromIDB() {
const db = this.ensureDB();
// 1. 持久化 schema
if (db.objectStoreNames.contains('__metona_schema')) {
const records = await new Promise((resolve, reject) => {
const req = db.transaction('__metona_schema', 'readonly').objectStore('__metona_schema').getAll();
req.onsuccess = () => resolve((req.result ?? []));
req.onerror = () => reject(req.error);
});
for (const rec of records) {
try {
const schema = JSON.parse(rec.schema);
if (!(await this.memoryCache.getTableSchema(schema.name))) {
await this.memoryCache.createTable(schema);
}
}
catch {
// 损坏的 schema 记录忽略
}
}
}
// 2. 回退:无持久化 schema 的表从 IDB 结构推断
const storeNames = Array.from(db.objectStoreNames).filter((n) => n !== '__metona_schema');
for (const tableName of storeNames) {
// 已有 schema(持久化恢复或连续 open)则跳过
const existing = await this.memoryCache.getTableSchema(tableName);
if (existing)
continue;
const columns = {};
const tx = db.transaction(tableName, 'readonly');
const store = tx.objectStore(tableName);
// 主键列
const pk = store.keyPath;
columns[pk] = { type: 'string', primaryKey: true };
// 索引列(idx_ 前缀约定)
for (const idxName of Array.from(store.indexNames)) {
if (idxName.startsWith('idx_')) {
const col = idxName.slice(4);
if (!columns[col]) {
columns[col] = { type: 'string', index: true };
}
}
}
// 从样例数据推断其余列的类型
const rows = await new Promise((resolve, reject) => {
const req = store.getAll();
req.onsuccess = () => resolve((req.result ?? []));
req.onerror = () => reject(req.error);
});
if (rows.length > 0) {
for (const [key, value] of Object.entries(rows[0])) {
if (!columns[key]) {
columns[key] = { type: inferFieldType(value) };
}
}
}
await this.memoryCache.createTable({ name: tableName, columns });
}
}
async close() {
// v0.4.3-fix: 活跃事务时先回滚(避免 commit 对已关闭连接报错)
if (this.txActive) {
try {
await this.rollbackTransaction();
}
catch { /* 回滚失败不阻塞关闭 */ }
}
if (this.db) {
this.db.onversionchange = null; // 清理监听器
this.db.close();
this.db = null;
}
await this.memoryCache.close();
}
/**
* v0.4.2-fix: 自愈 — 从磁盘重建内存 schema 与数据(schema 丢失/内存不一致时调用)。
* 无删库需求即可恢复可用的库。
*/
async repair() {
if (!this.db)
throw new DatabaseError('Database not opened', 'DB_NOT_OPEN');
await this.memoryCache.close();
await this.memoryCache.open(this.dbName, this.version);
await this.rebuildSchemaFromIDB();
}
/**
* v0.4.2-fix: 清空全部数据与表结构(含持久化 schema 记录),保留库本身。
* 单个版本升级事务内原子完成。
*/
async clearAll() {
const db = this.ensureDB();
// 先清内存缓存
const tableNames = await this.memoryCache.getTableNames();
for (const name of tableNames) {
await this.memoryCache.dropTable(name);
}
// 重置 IDB:删除所有表 store + 清空 schema/meta store
const newVersion = db.version + 1;
db.close();
await new Promise((resolve, reject) => {
const request = indexedDB.open(this.dbName, newVersion);
request.onupgradeneeded = (event) => {
const idb = event.target.result;
const toDelete = Array.from(idb.objectStoreNames).filter((n) => n !== '__metona_schema');
for (const name of toDelete) {
idb.deleteObjectStore(name);
}
if (!idb.objectStoreNames.contains('__metona_schema')) {
idb.createObjectStore('__metona_schema', { keyPath: 'name' });
}
else {
// 保留 store 但清空内容(含持久化 schema 与 meta 记录)
const tx = event.target.transaction;
tx.objectStore('__metona_schema').clear();
}
};
request.onsuccess = () => {
this.db = request.result;
this.setupVersionChangeHandler();
resolve();
};
request.onerror = () => reject(new DatabaseError('Failed to clear database', 'IDB_CLEAR_ERROR', request.error));
request.onblocked = () => reject(new DatabaseError(`IndexedDB "${this.dbName}" is blocked`, 'IDB_BLOCKED'));
});
}
// ---- 库内元数据(v0.4.2-fix:迁移版本持久化用,复用 __metona_schema store ----
async getMeta(key) {
const db = this.ensureDB();
if (!db.objectStoreNames.contains('__metona_schema'))
return null;
return new Promise((resolve, reject) => {
const req = db.transaction('__metona_schema', 'readonly')
.objectStore('__metona_schema').get(`__meta:${key}`);
req.onsuccess = () => {
const rec = req.result;
resolve(rec && typeof rec.schema === 'string' ? rec.schema : null);
};
req.onerror = () => reject(req.error);
});
}
async setMeta(key, value) {
const db = this.ensureDB();
if (!db.objectStoreNames.contains('__metona_schema'))
return;
await new Promise((resolve, reject) => {
const tx = db.transaction('__metona_schema', 'readwrite');
tx.objectStore('__metona_schema').put({ name: `__meta:${key}`, schema: value });
tx.oncomplete = () => resolve();
tx.onerror = () => reject(new DatabaseError(`Failed to persist meta "${key}"`, 'IDB_META_ERROR', tx.error));
});
}
isOpen() { return this.db !== null; }
// ---- 表管理 ----
async createTable(schema) {
await this.memoryCache.createTable(schema);
if (this.txActive)
return; // 事务中延迟 IDB 操作
await this.idbCreateTable(schema);
}
async dropTable(tableName) {
await this.memoryCache.dropTable(tableName);
if (this.txActive)
return;
await this.idbDropTable(tableName);
}
async hasTable(tableName) { return this.ensureDB().objectStoreNames.contains(tableName); }
async getTableNames() { return Array.from(this.ensureDB().objectStoreNames); }
async getTableSchema(tableName) { return this.memoryCache.getTableSchema(tableName); }
/**
* v0.4.2-fix: 引擎级 ALTER TABLE — schema 持久化到 __metona_schema store
* 重启后 ALTER 不丢失(此前通用路径只改内存引用,重启回退;DROP 的行数据也没真正删)。
*/
async alterTable(tableName, action, column) {
await this.memoryCache.alterTable(tableName, action, column);
if (this.txActive)
return; // 事务中:commit 时统一 flushToIDB 同步
const schema = await this.memoryCache.getTableSchema(tableName);
if (schema) {
await this.persistSchema(schema);
}
if (action === 'DROP') {
// 重写 IDB 存储行:移除该列键(store.put 经 keyPath 自动覆盖原行)
const db = this.ensureDB();
const rows = await this.idbFind(tableName, { table: tableName });
const rewritten = rows.map((row) => {
if (column.name in row) {
const copy = { ...row };
delete copy[column.name];
return copy;
}
return row;
});
if (rewritten.length > 0) {
await new Promise((resolve, reject) => {
const tx = db.transaction(tableName, 'readwrite');
const store = tx.objectStore(tableName);
for (const row of rewritten) {
store.put(row);
}
tx.oncomplete = () => resolve();
tx.onerror = () => reject(new DatabaseError(`Failed to rewrite "${tableName}" after DROP COLUMN`, 'IDB_TX_ERROR', tx.error));
});
}
}
}
// ---- CRUD ----
async insert(tableName, rows) {
const pks = await this.memoryCache.insert(tableName, rows);
if (this.txActive)
return pks; // 事务中延迟写入
await this.idbInsert(tableName, rows);
return pks;
}
async find(tableName, query) {
// 事务中从内存缓存读取(保证读到未提交的变更),否则走 IDB
if (this.txActive)
return this.memoryCache.find(tableName, query);
return this.idbFind(tableName, query);
}
/** v0.4.0: 流式查询 — IDB 批量读入后逐行回调(保持接口一致性) */
async findStream(tableName, query, onRow) {
if (this.txActive) {
return this.memoryCache.findStream(tableName, query, onRow);
}
const rows = await this.idbFind(tableName, { ...query, orderBy: undefined, limit: undefined, offset: undefined });
let count = 0;
for (const row of rows) {
onRow(row);
count++;
}
return count;
}
async update(tableName, query, updates) {
const count = await this.memoryCache.update(tableName, query, updates);
if (this.txActive)
return count;
await this.idbUpdate(tableName, query, updates);
return count;
}
async delete(tableName, query) {
const count = await this.memoryCache.delete(tableName, query);
if (this.txActive)
return count;
await this.idbDelete(tableName, query);
return count;
}
async count(tableName, query) {
if (this.txActive)
return this.memoryCache.count(tableName, query);
const results = await this.idbFind(tableName, { table: tableName, where: query?.where ?? {} });
return results.length;
}
async clear(tableName) {
await this.memoryCache.clear(tableName);
if (this.txActive)
return;
await this.idbClear(tableName);
}
// ---- 动态索引(v0.3.0):通过版本升级创建/删除 IDB 索引 ----
async createIndex(tableName, column, unique) {
await this.memoryCache.createIndex(tableName, column, unique);
if (this.txActive)
return;
const db = this.ensureDB();
return new Promise((resolve, reject) => {
const newVersion = db.version + 1;
db.close();
const request = indexedDB.open(this.dbName, newVersion);
request.onupgradeneeded = (event) => {
const idb = event.target.result;
const tx = idb.transaction(tableName, 'readwrite');
const store = tx.objectStore(tableName);
if (!store.indexNames.contains(`idx_${column}`)) {
store.createIndex(`idx_${column}`, column, { unique: unique ?? false });
}
};
request.onsuccess = () => { this.db = request.result; resolve(); };
request.onerror = () => reject(new DatabaseError(`Failed to create index "${tableName}.${column}"`, 'IDB_UPGRADE_ERROR', request.error));
});
}
async dropIndex(tableName, column, _indexName) {
await this.memoryCache.dropIndex(tableName, column);
if (this.txActive)
return;
const db = this.ensureDB();
return new Promise((resolve, reject) => {
const newVersion = db.version + 1;
db.close();
const request = indexedDB.open(this.dbName, newVersion);
request.onupgradeneeded = (event) => {
const idb = event.target.result;
const tx = idb.transaction(tableName, 'readwrite');
const store = tx.objectStore(tableName);
if (store.indexNames.contains(`idx_${column}`)) {
store.deleteIndex(`idx_${column}`);
}
};
request.onsuccess = () => { this.db = request.result; resolve(); };
request.onerror = () => reject(new DatabaseError(`Failed to drop index "${tableName}.${column}"`, 'IDB_UPGRADE_ERROR', request.error));
});
}
// ---- 事务 ----
async beginTransaction() {
if (this.txActive)
throw new DatabaseError('Transaction already in progress', 'TX_ACTIVE');
this.txActive = true;
// 保存内存快照到 MemoryEngine 内部的 beginTransaction
await this.memoryCache.beginTransaction();
}
async commitTransaction() {
if (!this.txActive)
throw new DatabaseError('No active transaction', 'TX_NONE');
// v0.4.2-fix: 先刷盘后提交内存快照 — 此前先 memoryCache.commitTransaction()
// 再 flushToIDBflush 失败时 snapshot 已丢,回滚报 TX_NONE 且内存数据已确认
await this.flushToIDB();
await this.memoryCache.commitTransaction();
this.txActive = false;
}
async rollbackTransaction() {
if (!this.txActive)
throw new DatabaseError('No active transaction', 'TX_NONE');
// 恢复内存层到快照状态
await this.memoryCache.rollbackTransaction();
this.txActive = false;
}
// ---- IDB 原生操作 ----
async idbCreateTable(schema) {
const db = this.ensureDB();
return new Promise((resolve, reject) => {
const newVersion = db.version + 1;
db.close();
const request = indexedDB.open(this.dbName, newVersion);
request.onupgradeneeded = (event) => {
const db = event.target.result;
const pkColumn = Object.entries(schema.columns).find(([, c]) => c.primaryKey)?.[0] ?? Object.keys(schema.columns)[0];
const store = db.createObjectStore(schema.name, { keyPath: pkColumn });
for (const [colName, colDef] of Object.entries(schema.columns)) {
if (colDef.index && colName !== pkColumn) {
store.createIndex(`idx_${colName}`, colName, { unique: colDef.unique ?? false });
}
}
// v0.3.2: schema 持久化 store(记录在升级完成后的 onsuccess 写入)
if (!db.objectStoreNames.contains('__metona_schema')) {
db.createObjectStore('__metona_schema', { keyPath: 'name' });
}
};
request.onsuccess = () => {
this.db = request.result;
// v0.3.2: 升级完成后持久化 schema(upgrade 事务内异步写会失败)
const schemaTx = this.db.transaction('__metona_schema', 'readwrite');
schemaTx.objectStore('__metona_schema').put({ name: schema.name, schema: JSON.stringify(schema) });
schemaTx.oncomplete = () => resolve();
schemaTx.onerror = () => reject(new DatabaseError(`Failed to persist schema for "${schema.name}"`, 'IDB_SCHEMA_ERROR', schemaTx.error));
};
request.onerror = () => reject(new DatabaseError(`Failed to create table "${schema.name}"`, 'IDB_UPGRADE_ERROR', request.error));
});
}
async idbDropTable(tableName) {
const db = this.ensureDB();
return new Promise((resolve, reject) => {
const newVersion = db.version + 1;
db.close();
const request = indexedDB.open(this.dbName, newVersion);
request.onupgradeneeded = (event) => {
const db = event.target.result;
if (db.objectStoreNames.contains(tableName))
db.deleteObjectStore(tableName);
};
request.onsuccess = () => {
this.db = request.result;
// v0.3.2: 清理持久化 schema 记录(upgrade 后执行,失败不阻塞删除)
if (this.db.objectStoreNames.contains('__metona_schema')) {
const schemaTx = this.db.transaction('__metona_schema', 'readwrite');
schemaTx.objectStore('__metona_schema').delete(tableName);
schemaTx.onerror = () => { };
}
resolve();
};
request.onerror = () => reject(new DatabaseError(`Failed to drop table "${tableName}"`, 'IDB_UPGRADE_ERROR', request.error));
});
}
async idbInsert(tableName, rows) {
const db = this.ensureDB();
return new Promise((resolve, reject) => {
const tx = db.transaction(tableName, 'readwrite');
const store = tx.objectStore(tableName);
for (const row of rows)
store.add(row);
tx.oncomplete = () => resolve();
tx.onerror = () => reject(new DatabaseError(`Insert failed for "${tableName}"`, 'IDB_TX_ERROR', tx.error));
});
}
async idbFind(tableName, query) {
const db = this.ensureDB();
// 尝试使用 IDB 索引进行等值查询
if (query.where) {
const indexResult = await this.tryIDBIndexLookup(db, tableName, query);
if (indexResult !== null)
return indexResult;
}
// 回退到全量 getAll + 内存过滤
return new Promise((resolve, reject) => {
const tx = db.transaction(tableName, 'readonly');
const req = tx.objectStore(tableName).getAll();
req.onsuccess = () => {
let results = req.result ?? [];
if (query.where && Object.keys(query.where).length > 0) {
results = results.filter((row) => matchWhere(row, query.where));
}
if (query.orderBy && query.orderBy.length > 0) {
results = applyOrderBy(results, query.orderBy);
}
const offset = query.offset ?? 0;
const limit = query.limit ?? results.length;
results = results.slice(offset, offset + limit);
if (query.columns && query.columns.length > 0 && query.columns[0] !== '*') {
results = results.map((r) => projectColumns(r, query.columns));
}
resolve(results);
};
req.onerror = () => reject(new DatabaseError(`Find failed for "${tableName}"`, 'IDB_READ_ERROR', req.error));
});
}
/** 尝试使用 IDB 索引进行等值查询,成功返回结果,不适用返回 null */
async tryIDBIndexLookup(db, tableName, query) {
if (!query.where)
return null;
for (const [col, condition] of Object.entries(query.where)) {
// 跳过逻辑组合符
if (col === '$and' || col === '$or' || col === '$not')
continue;
// 只处理等值查询
let targetValue;
if (typeof condition !== 'object' || condition === null) {
targetValue = condition;
}
else if ('$eq' in condition) {
targetValue = condition.$eq;
}
else {
continue;
}
// 检查是否有对应的 IDB 索引
const indexName = `idx_${col}`;
try {
return await new Promise((resolve, reject) => {
const tx = db.transaction(tableName, 'readonly');
const store = tx.objectStore(tableName);
if (!store.indexNames.contains(indexName)) {
resolve(null); // 没有索引,回退
return;
}
const index = store.index(indexName);
const req = index.getAll(targetValue);
req.onsuccess = () => {
let results = req.result ?? [];
// 如果有其他 WHERE 条件,继续过滤
const otherKeys = Object.keys(query.where).filter((k) => k !== col && k !== '$and' && k !== '$or' && k !== '$not');
if (otherKeys.length > 0) {
results = results.filter((row) => matchWhere(row, query.where));
}
if (query.orderBy && query.orderBy.length > 0) {
results = applyOrderBy(results, query.orderBy);
}
const offset = query.offset ?? 0;
const limit = query.limit ?? results.length;
results = results.slice(offset, offset + limit);
if (query.columns && query.columns.length > 0 && query.columns[0] !== '*') {
results = results.map((r) => projectColumns(r, query.columns));
}
resolve(results);
};
req.onerror = () => reject(new DatabaseError(`Index lookup failed for "${tableName}.${col}"`, 'IDB_READ_ERROR', req.error));
});
}
catch {
return null; // 索引不可用,回退
}
}
return null;
}
async idbUpdate(tableName, query, updates) {
const db = this.ensureDB();
return new Promise((resolve, reject) => {
const tx = db.transaction(tableName, 'readwrite');
const store = tx.objectStore(tableName);
const getAllReq = store.getAll();
getAllReq.onsuccess = () => {
for (const row of getAllReq.result ?? []) {
if (!query.where || Object.keys(query.where).length === 0 || matchWhere(row, query.where)) {
Object.assign(row, updates);
store.put(row);
}
}
};
tx.oncomplete = () => resolve();
tx.onerror = () => reject(new DatabaseError(`Update failed for "${tableName}"`, 'IDB_TX_ERROR', tx.error));
});
}
async idbDelete(tableName, query) {
const db = this.ensureDB();
const schema = await this.memoryCache.getTableSchema(tableName);
if (!schema)
throw new DatabaseError(`Table "${tableName}" not found`, 'TABLE_NOT_FOUND');
const pkColumn = Object.entries(schema.columns).find(([, c]) => c.primaryKey)?.[0] ?? Object.keys(schema.columns)[0];
return new Promise((resolve, reject) => {
const tx = db.transaction(tableName, 'readwrite');
const store = tx.objectStore(tableName);
const getAllReq = store.getAll();
getAllReq.onsuccess = () => {
for (const row of getAllReq.result ?? []) {
if (!query.where || Object.keys(query.where).length === 0 || matchWhere(row, query.where)) {
store.delete(row[pkColumn]);
}
}
};
tx.oncomplete = () => resolve();
tx.onerror = () => reject(new DatabaseError(`Delete failed for "${tableName}"`, 'IDB_TX_ERROR', tx.error));
});
}
async idbClear(tableName) {
const db = this.ensureDB();
return new Promise((resolve, reject) => {
const tx = db.transaction(tableName, 'readwrite');
tx.objectStore(tableName).clear();
tx.oncomplete = () => resolve();
tx.onerror = () => reject(new DatabaseError(`Clear failed for "${tableName}"`, 'IDB_TX_ERROR', tx.error));
});
}
/** 持久化单个表 schema 到 __metona_schema storev0.4.2-fix: ALTER TABLE 用) */
async persistSchema(schema) {
const db = this.ensureDB();
if (!db.objectStoreNames.contains('__metona_schema'))
return;
await new Promise((resolve, reject) => {
const tx = db.transaction('__metona_schema', 'readwrite');
tx.objectStore('__metona_schema').put({ name: schema.name, schema: JSON.stringify(schema) });
tx.oncomplete = () => resolve();
tx.onerror = () => reject(new DatabaseError(`Failed to persist schema for "${schema.name}"`, 'IDB_SCHEMA_ERROR', tx.error));
});
}
/** 将内存缓存中的所有表数据原子性刷新到 IndexedDB */
async flushToIDB() {
const tableNames = await this.memoryCache.getTableNames();
let db = this.ensureDB();
// v0.4.2-fix: 事务内 DDL 只更新内存,commit 时同步 IDB 的 objectStore 结构:
// 缺失的表 store 创建(并持久化 schema)、已删除的 store 移除(防止重启幽灵表)
const idbStores = Array.from(db.objectStoreNames);
const missing = tableNames.filter((t) => !idbStores.includes(t));
const stale = idbStores.filter((s) => s !== '__metona_schema' && !tableNames.includes(s));
if (missing.length > 0 || stale.length > 0) {
// 升级事务内是同步上下文,先异步收集缺失表的 schema(主键列定义)
const schemaMap = new Map();
for (const name of missing) {
const s = await this.memoryCache.getTableSchema(name);
if (s)
schemaMap.set(name, s);
}
const newVersion = db.version + 1;
db.close();
await new Promise((resolve, reject) => {
const request = indexedDB.open(this.dbName, newVersion);
request.onupgradeneeded = (event) => {
const idb = event.target.result;
for (const name of stale) {
idb.deleteObjectStore(name);
}
for (const name of missing) {
const schema = schemaMap.get(name);
const pkColumn = schema
? (Object.entries(schema.columns).find(([, c]) => c.primaryKey)?.[0] ?? Object.keys(schema.columns)[0])
: undefined;
idb.createObjectStore(name, { keyPath: pkColumn });
}
// 事务内 drop 的表:同步清理持久化 schema 记录(防止重启后幽灵表恢复)
if (idb.objectStoreNames.contains('__metona_schema') && stale.length > 0) {
const tx = event.target.transaction;
const store = tx.objectStore('__metona_schema');
for (const name of stale) {
store.delete(name);
}
}
};
request.onsuccess = () => {
this.db = request.result;
this.setupVersionChangeHandler();
resolve();
};
request.onerror = () => reject(new DatabaseError('Failed to sync stores after transaction', 'IDB_UPGRADE_ERROR', request.error));
request.onblocked = () => reject(new DatabaseError(`IndexedDB "${this.dbName}" is blocked`, 'IDB_BLOCKED'));
});
// 事务内新建表的 schema 一并持久化(此前只建 store 不存 schema → 重启后约束推断丢失)
for (const name of missing) {
const schema = await this.memoryCache.getTableSchema(name);
if (schema)
await this.persistSchema(schema);
}
// v0.4.2-fix: DDL 升级会 close 旧连接并重新 open — 重新取 db 引用,
// 否则下方数据 flush 用已关闭的连接抛 InvalidStateError
db = this.ensureDB();
}
// 每个表在一个单独的 IDB 事务中完成 clear+insert,保证原子性
for (const tableName of tableNames) {
const rows = await this.memoryCache.find(tableName, { table: tableName });
await new Promise((resolve, reject) => {
const tx = db.transaction(tableName, 'readwrite');
const store = tx.objectStore(tableName);
store.clear(); // 清空
for (const row of rows)
store.add(row); // 批量写入
tx.oncomplete = () => resolve();
tx.onerror = () => reject(new DatabaseError(`Flush failed for "${tableName}"`, 'IDB_FLUSH_ERROR', tx.error));
});
}
}
ensureDB() {
if (!this.db)
throw new DatabaseError('Database not opened', 'DB_NOT_OPEN');
return this.db;
}
}
/** 从存储值推断字段类型(schema 重建用,v0.3.2 */
function inferFieldType(value) {
if (typeof value === 'number')
return 'number';
if (typeof value === 'boolean')
return 'boolean';
if (typeof value === 'object' && value !== null)
return 'json';
if (typeof value === 'string') {
return isNaN(Date.parse(value)) ? 'string' : 'string';
}
return 'string';
}
/**
* metona-sqlark OPFS Engine — 基于 Origin Private File System 的持久化存储引擎
* @module engine/opfs
*
* 使用 JSON-per-table 文件存储方案。
* 目录结构:{dbName}/tables/{tableName}.json
*/
// ---------------------------------------------------------------------------
// OPFSEngine
// ---------------------------------------------------------------------------
class OPFSEngine {
constructor() {
this.name = 'opfs';
this.root = null;
this.tablesDir = null;
this.dbName = '';
// 运行时内存缓存(OPFS 文件读写有延迟)
this.memoryCache = new MemoryEngine();
/**
* v0.4.3-fix: 写操作串行队列 — 内存写 + 快照 + 文件持久化整体排队执行,
* close() 等待队列排空后再释放目录句柄(避免 close 后挂起写泄漏/读旧数据)。
* 前一个操作失败不阻塞后续(错误仍返回给调用方)。
*/
this.opQueue = Promise.resolve();
}
/** 将写操作加入串行队列(快照在队列内取,始终最新) */
enqueueOp(fn) {
const run = this.opQueue.then(fn, fn);
this.opQueue = run.then(() => undefined, () => undefined);
return run;
}
// ---- 生命周期 ----
async open(dbName, version) {
this.dbName = dbName;
await this.memoryCache.open(dbName, version);
// 获取 OPFS 根目录
this.root = await navigator.storage.getDirectory();
// 创建/打开数据库目录
this.tablesDir = await this.root.getDirectoryHandle(dbName, { create: true });
// 从 OPFS 恢复已有表数据到内存缓存
await this.loadExistingTables();
}
async close() {
// v0.4.3-fix: 等待所有挂起写操作完成(否则 close 后写仍在进行 → 重启读旧数据)
try {
await this.opQueue;
}
catch { /* 写失败已返回给调用方 */ }
this.root = null;
this.tablesDir = null;
await this.memoryCache.close();
}
isOpen() {
return this.tablesDir !== null;
}
// ---- v0.4.2-fix: 自愈 / 重置 / 元数据 ----
/** 自愈:重置内存缓存后从 OPFS 重新加载(单文件损坏不影响其他表) */
async repair() {
// v0.4.3-fix: 先等写队列排空(避免与挂起写竞态)
try {
await this.opQueue;
}
catch { /* ignore */ }
await this.memoryCache.close();
await this.memoryCache.open(this.dbName, 1);
await this.loadExistingTables();
}
/** 清空全部数据与表结构(删除目录内全部文件) */
async clearAll() {
// v0.4.3-fix: 先等写队列排空
try {
await this.opQueue;
}
catch { /* ignore */ }
await this.memoryCache.close();
await this.memoryCache.open(this.dbName, 1);
if (this.tablesDir) {
const dir = this.tablesDir;
for await (const [name] of dir.entries()) {
try {
await this.tablesDir.removeEntry(name);
}
catch { /* ignore */ }
}
}
}
async getMeta(key) {
if (!this.tablesDir)
return null;
try {
const fh = await this.tablesDir.getFileHandle(`__metona_${key}.meta`);
const file = await fh.getFile();
return await file.text();
}
catch {
return null;
}
}
async setMeta(key, value) {
if (!this.tablesDir)
return;
const fh = await this.tablesDir.getFileHandle(`__metona_${key}.meta`, { create: true });
const writable = await fh.createWritable();
await writable.write(value);
await writable.close();
}
// ---- 表管理 ----
async createTable(schema) {
return this.enqueueOp(async () => {
await this.memoryCache.createTable(schema);
// v0.4.2-fix: schema 持久化(此前仅写空数据文件 → 空表重启后消失、索引标记丢失)
await this.setMeta(`schema_${schema.name}`, JSON.stringify(schema));
// OPFS 中表以空 JSON 数组文件形式存在
await this.writeTableData(schema.name, []);
});
}
async dropTable(tableName) {
return this.enqueueOp(async () => {
await this.memoryCache.dropTable(tableName);
// v0.4.2-fix: 清理 schema meta(否则重启恢复幽灵表)
if (this.tablesDir) {
try {
await this.tablesDir.removeEntry(`__metona_schema_${tableName}.meta`);
}
catch {
// 文件不存在则忽略
}
try {
await this.tablesDir.removeEntry(`${tableName}.json`);
}
catch {
// 文件不存在则忽略
}
}
});
}
async hasTable(tableName) {
if (!this.tablesDir)
return false;
try {
await this.tablesDir.getFileHandle(`${tableName}.json`);
return true;
}
catch {
return false;
}
}
async getTableNames() {
if (!this.tablesDir)
return [];
const names = [];
for await (const [name] of this.tablesDir.entries()) {
if (name.endsWith('.json')) {
names.push(name.replace('.json', ''));
}
}
return names;
}
async getTableSchema(tableName) {
return this.memoryCache.getTableSchema(tableName);
}
/** v0.4.2-fix: 引擎级 ALTER TABLE — 内存 + schema 持久化 + 整表文件重写 */
async alterTable(tableName, action, column) {
return this.enqueueOp(async () => {
await this.memoryCache.alterTable(tableName, action, column);
const schema = await this.memoryCache.getTableSchema(tableName);
if (schema)
await this.setMeta(`schema_${tableName}`, JSON.stringify(schema));
const rows = await this.memoryCache.find(tableName, { table: tableName });
await this.writeTableData(tableName, rows);
});
}
// ---- CRUD ----
async insert(tableName, rows) {
return this.enqueueOp(async () => {
const pks = await this.memoryCache.insert(tableName, rows);
// 持久化到 OPFS(快照在队列内取,始终最新)
const allRows = await this.memoryCache.find(tableName, { table: tableName });
await this.writeTableData(tableName, allRows);
return pks;
});
}
async find(tableName, query) {
return this.memoryCache.find(tableName, query);
}
/** v0.4.0: 流式查询(委托内存缓存) */
async findStream(tableName, query, onRow) {
return this.memoryCache.findStream(tableName, query, onRow);
}
async update(tableName, query, updates) {
return this.enqueueOp(async () => {
const count = await this.memoryCache.update(tableName, query, updates);
const allRows = await this.memoryCache.find(tableName, { table: tableName });
await this.writeTableData(tableName, allRows);
return count;
});
}
async delete(tableName, query) {
return this.enqueueOp(async () => {
const count = await this.memoryCache.delete(tableName, query);
const allRows = await this.memoryCache.find(tableName, { table: tableName });
await this.writeTableData(tableName, allRows);
return count;
});
}
async count(tableName, query) {
return this.memoryCache.count(tableName, query);
}
async clear(tableName) {
return this.enqueueOp(async () => {
await this.memoryCache.clear(tableName);
await this.writeTableData(tableName, []);
});
}
// ---- 动态索引(v0.3.0 ----
async createIndex(tableName, column, unique) {
return this.enqueueOp(async () => {
await this.memoryCache.createIndex(tableName, column, unique);
// v0.4.2-fix: 索引标记持久化(重启后索引结构恢复)
const schema = await this.memoryCache.getTableSchema(tableName);
if (schema)
await this.setMeta(`schema_${tableName}`, JSON.stringify(schema));
});
}
async dropIndex(tableName, column, indexName) {
return this.enqueueOp(async () => {
await this.memoryCache.dropIndex(tableName, column, indexName);
const schema = await this.memoryCache.getTableSchema(tableName);
if (schema)
await this.setMeta(`schema_${tableName}`, JSON.stringify(schema));
});
}
// ---- 事务 ----
async beginTransaction() {
await this.memoryCache.beginTransaction();
}
async commitTransaction() {
return this.enqueueOp(async () => {
await this.memoryCache.commitTransaction();
// 将内存数据刷到 OPFS
const tableNames = await this.memoryCache.getTableNames();
for (const tableName of tableNames) {
const rows = await this.memoryCache.find(tableName, { table: tableName });
await this.writeTableData(tableName, rows);
}
});
}
async rollbackTransaction() {
await this.memoryCache.rollbackTransaction();
}
// ---- 内部辅助 ----
ensureDir() {
if (!this.tablesDir) {
throw new DatabaseError('Database not opened', 'DB_NOT_OPEN');
}
return this.tablesDir;
}
async writeTableData(tableName, data) {
// 由 enqueueOp 串行化调用,此处直接写文件
const dir = this.ensureDir();
const fileName = `${tableName}.json`;
const fileHandle = await dir.getFileHandle(fileName, { create: true });
const writable = await fileHandle.createWritable();
await writable.write(JSON.stringify(data));
await writable.close();
}
async readTableData(tableName) {
const dir = this.ensureDir();
const fileName = `${tableName}.json`;
try {
const fileHandle = await dir.getFileHandle(fileName);
const file = await fileHandle.getFile();
const text = await file.text();
return JSON.parse(text);
}
catch {
return [];
}
}
/**
* 从 OPFS 加载已有表到内存缓存。
* v0.4.2-fix: 优先从持久化 schema__metona_schema_*.meta)恢复 —
* 空表不再消失、索引标记/主键/约束完整;无 schema 记录的旧库从数据推断(兼容)。
*/
async loadExistingTables() {
if (!this.tablesDir)
return;
const dir = this.tablesDir;
const fileNames = [];
for await (const [name] of dir.entries()) {
if (name.endsWith('.json'))
fileNames.push(name);
}
for (const fileName of fileNames) {
const tableName = fileName.replace('.json', '');
try {
// 1. 优先:持久化 schema
const schemaRaw = await this.getMeta(`schema_${tableName}`);
if (schemaRaw) {
const schema = JSON.parse(schemaRaw);
await this.memoryCache.createTable(schema);
const rows = await this.readTableData(tableName);
for (const row of rows) {
try {
await this.memoryCache.insert(tableName, [row]);
}
catch {
// 单行损坏不影响整表恢复
}
}
continue;
}
// 2. 兼容旧库:从数据推断 schema(空表且无 schema 记录 → 跳过)
const data = await this.readTableData(tableName);
if (data.length > 0) {
const firstRow = data[0];
const columns = {};
for (const key of Object.keys(firstRow)) {
const val = firstRow[key];
const type = typeof val === 'number' ? 'number' :
typeof val === 'boolean' ? 'boolean' :
typeof val === 'object' ? 'json' : 'string';
columns[key] = { type, primaryKey: key === 'id' };
}
await this.memoryCache.createTable({ name: tableName, columns });
for (const row of data) {
await this.memoryCache.insert(tableName, [row]);
}
}
}
catch {
// 单个文件损坏不影响其他表
}
}
}
/** 从 OPFS 加载表数据到内存缓存 */
async loadTableIntoMemory(tableName, schema) {
await this.memoryCache.createTable(schema);
const rows = await this.readTableData(tableName);
if (rows.length > 0) {
// 直接用 Map 设置绕过 insert 校验
for (const row of rows) {
await this.memoryCache.insert(tableName, [row]);
}
}
}
}
/**
* metona-sqlark Schema — 表结构定义与校验
* @module table/schema
*/
// ---------------------------------------------------------------------------
// Schema 工具
// ---------------------------------------------------------------------------
/** 从列定义创建 TableSchema */
function createSchema(name, columns) {
validateColumns(columns);
return { name, columns };
}
/** 校验列定义 */
function validateColumns(columns) {
const colNames = Object.keys(columns);
if (colNames.length === 0) {
throw new DatabaseError('Table must have at least one column', 'SCHEMA_ERROR');
}
let primaryKeyCount = 0;
for (const [colName, colDef] of Object.entries(columns)) {
// 类型校验
if (!FIELD_TYPES.includes(colDef.type)) {
throw new DatabaseError(`Invalid type "${colDef.type}" for column "${colName}". Valid types: ${FIELD_TYPES.join(', ')}`, 'SCHEMA_ERROR');
}
// 主键计数
if (colDef.primaryKey) {
primaryKeyCount++;
}
}
// 至少需要一个主键
if (primaryKeyCount === 0) {
throw new DatabaseError('Table must have at least one primary key column', 'SCHEMA_ERROR');
}
}
/** 检查字段类型(含约束校验) */
function checkFieldType(tableName, colName, type, value, colDef) {
const jsType = typeof value;
switch (type) {
case 'string':
if (jsType !== 'string') {
throw new DatabaseError(`Column "${colName}" in table "${tableName}" expects string, got ${jsType}`, 'TYPE_ERROR');
}
if (colDef?.maxLength !== undefined && value.length > colDef.maxLength) {
throw new DatabaseError(`Column "${colName}" in table "${tableName}" exceeds max length ${colDef.maxLength}`, 'VALIDATION_ERROR');
}
break;
case 'number':
if (jsType !== 'number') {
throw new DatabaseError(`Column "${colName}" in table "${tableName}" expects number, got ${jsType}`, 'TYPE_ERROR');
}
if (colDef?.min !== undefined && value < colDef.min) {
throw new DatabaseError(`Column "${colName}" in table "${tableName}" value ${value} below minimum ${colDef.min}`, 'VALIDATION_ERROR');
}
if (colDef?.max !== undefined && value > colDef.max) {
throw new DatabaseError(`Column "${colName}" in table "${tableName}" value ${value} above maximum ${colDef.max}`, 'VALIDATION_ERROR');
}
break;
case 'boolean':
if (jsType !== 'boolean') {
throw new DatabaseError(`Column "${colName}" in table "${tableName}" expects boolean, got ${jsType}`, 'TYPE_ERROR');
}
break;
case 'date':
if (jsType !== 'string' || isNaN(Date.parse(value))) {
throw new DatabaseError(`Column "${colName}" in table "${tableName}" expects valid date string, got ${typeof value}`, 'TYPE_ERROR');
}
break;
case 'json':
if (jsType !== 'object') {
throw new DatabaseError(`Column "${colName}" in table "${tableName}" expects object/array, got ${jsType}`, 'TYPE_ERROR');
}
break;
}
}
/** 将 AST 列定义转换为 ColumnDef */
function astColumnToColumnDef(astCol) {
return {
type: astCol.type,
primaryKey: astCol.primaryKey,
unique: astCol.unique,
required: astCol.required,
default: astCol.default,
index: astCol.index,
maxLength: astCol.maxLength,
min: astCol.min,
max: astCol.max,
references: astCol.references,
onDelete: astCol.onDelete,
onUpdate: astCol.onUpdate,
};
}
/**
* AriaEngine Types — 内部类型定义
* @module engine/aria/types
*
* 页面式存储引擎的所有内部枚举、接口和常量。
*/
// =============================================================================
// 页面常量
// =============================================================================
/** 页面大小:4KB */
const PAGE_SIZE = 4096;
/** 页面头大小:16 字节 */
const PAGE_HEADER_SIZE = 16;
// =============================================================================
// 页面类型
// =============================================================================
var PageType;
(function (PageType) {
/** 数据页面:存储行数据 */
PageType[PageType["DATA"] = 1] = "DATA";
/** 索引页面:存储索引节点 */
PageType[PageType["INDEX"] = 2] = "INDEX";
/** 溢出页面:存储大字段 */
PageType[PageType["OVERFLOW"] = 3] = "OVERFLOW";
/** 元数据页面:存储表/库元信息 */
PageType[PageType["META"] = 4] = "META";
})(PageType || (PageType = {}));
/** 列类型(内部二进制编码用) */
var ColumnEncoding;
(function (ColumnEncoding) {
ColumnEncoding[ColumnEncoding["STRING"] = 1] = "STRING";
ColumnEncoding[ColumnEncoding["NUMBER"] = 2] = "NUMBER";
ColumnEncoding[ColumnEncoding["BOOLEAN"] = 3] = "BOOLEAN";
ColumnEncoding[ColumnEncoding["DATE"] = 4] = "DATE";
ColumnEncoding[ColumnEncoding["JSON"] = 5] = "JSON";
ColumnEncoding[ColumnEncoding["NULL"] = 6] = "NULL";
})(ColumnEncoding || (ColumnEncoding = {}));
// =============================================================================
// LSM-Tree
// =============================================================================
/** MemTable 最大大小(默认 4MB */
const DEFAULT_MEMTABLE_SIZE = 4 * 1024 * 1024;
/** Bloom Filter 每 key 的默认位数 */
const DEFAULT_BLOOM_BITS_PER_KEY = 10;
/** SSTable 最大层级 */
const MAX_LSM_LEVELS = 7;
/** 每层之间的大小倍数 */
const DEFAULT_LEVEL_SIZE_MULTIPLIER = 10;
// =============================================================================
// WAL (Write-Ahead Log)
// =============================================================================
/** WAL 记录类型 */
var WALRecordType;
(function (WALRecordType) {
WALRecordType[WALRecordType["INSERT"] = 1] = "INSERT";
WALRecordType[WALRecordType["UPDATE"] = 2] = "UPDATE";
WALRecordType[WALRecordType["DELETE"] = 3] = "DELETE";
WALRecordType[WALRecordType["BEGIN"] = 4] = "BEGIN";
WALRecordType[WALRecordType["COMMIT"] = 5] = "COMMIT";
WALRecordType[WALRecordType["ROLLBACK"] = 6] = "ROLLBACK";
WALRecordType[WALRecordType["CREATE_TABLE"] = 7] = "CREATE_TABLE";
WALRecordType[WALRecordType["DROP_TABLE"] = 8] = "DROP_TABLE";
})(WALRecordType || (WALRecordType = {}));
// =============================================================================
// MVCC
// =============================================================================
/** 事务隔离级别 */
var IsolationLevel;
(function (IsolationLevel) {
IsolationLevel[IsolationLevel["READ_COMMITTED"] = 1] = "READ_COMMITTED";
IsolationLevel[IsolationLevel["SNAPSHOT"] = 2] = "SNAPSHOT";
})(IsolationLevel || (IsolationLevel = {}));
/** 事务状态 */
var TransactionState;
(function (TransactionState) {
TransactionState[TransactionState["ACTIVE"] = 1] = "ACTIVE";
TransactionState[TransactionState["COMMITTED"] = 2] = "COMMITTED";
TransactionState[TransactionState["ABORTED"] = 3] = "ABORTED";
})(TransactionState || (TransactionState = {}));
// =============================================================================
// Buffer Pool
// =============================================================================
/** Buffer Pool 默认容量:256 页 ≈ 1MB */
const DEFAULT_BUFFER_POOL_PAGES = 256;
const DEFAULT_ARIA_CONFIG = {
pageSize: PAGE_SIZE,
bufferPoolPages: DEFAULT_BUFFER_POOL_PAGES,
memtableSizeThreshold: DEFAULT_MEMTABLE_SIZE,
levelSizeMultiplier: DEFAULT_LEVEL_SIZE_MULTIPLIER,
bloomFilterBitsPerKey: DEFAULT_BLOOM_BITS_PER_KEY,
walEnabled: true,
walSyncMode: 'full',
checkpointInterval: 1000,
compression: false,
storageBackend: 'indexeddb',
walSizeThreshold: 16 * 1024 * 1024, // 16MB
maxMemoryMB: 64,
};
/**
* AriaEngine MemTable — 基于红黑树的内存表
* @module engine/aria/index/memtable
*
* 写操作先进入 MemTable,达到阈值后冻结并 flush 成 SSTable。
*/
// ---------------------------------------------------------------------------
// RB-Tree Node
// ---------------------------------------------------------------------------
var Color;
(function (Color) {
Color[Color["RED"] = 0] = "RED";
Color[Color["BLACK"] = 1] = "BLACK";
})(Color || (Color = {}));
class RBNode {
constructor(key, value) {
this.color = Color.RED;
this.left = null;
this.right = null;
this.parent = null;
this.key = key;
this.value = value;
}
}
// ---------------------------------------------------------------------------
// Red-Black Tree
// ---------------------------------------------------------------------------
class RedBlackTree {
constructor() {
this.root = null;
this._size = 0;
}
get size() { return this._size; }
// ---- 插入 ----
insert(key, value) {
const node = new RBNode(key, value);
if (!this.root) {
this.root = node;
node.color = Color.BLACK;
this._size++;
return;
}
let parent = null;
let current = this.root;
while (current) {
parent = current;
if (key < current.key) {
current = current.left;
}
else if (key > current.key) {
current = current.right;
}
else {
// 更新已存在的 key
current.value = value;
return;
}
}
node.parent = parent;
if (key < parent.key) {
parent.left = node;
}
else {
parent.right = node;
}
this._size++;
this.fixInsert(node);
}
// ---- 查找 ----
find(key) {
let current = this.root;
while (current) {
if (key < current.key) {
current = current.left;
}
else if (key > current.key) {
current = current.right;
}
else {
return current.value;
}
}
return null;
}
// ---- 删除 ----
delete(key) {
// 简化实现:标记删除(实际改为找到并调整树)
const node = this.findNode(key);
if (!node)
return false;
this.deleteNode(node);
this._size--;
return true;
}
// ---- 遍历 ----
/** 中序遍历(有序) */
inorder(callback) {
this._inorder(this.root, callback);
}
/** 范围遍历 */
rangeScan(startKey, endKey, callback) {
this._rangeScan(this.root, startKey, endKey, callback);
}
/** 获取所有条目 */
getAllEntries() {
const entries = [];
this.inorder((k, v) => entries.push([k, v]));
return entries;
}
/** 清空 */
clear() {
this.root = null;
this._size = 0;
}
// ---- 内部方法 ----
findNode(key) {
let current = this.root;
while (current) {
if (key < current.key) {
current = current.left;
}
else if (key > current.key) {
current = current.right;
}
else {
return current;
}
}
return null;
}
deleteNode(node) {
// 简化:用左子树最大或右子树最小替换
// 完整实现较复杂,这里采用简化策略
if (!node.left && !node.right) {
this.transplant(node, null);
if (node.color === Color.BLACK)
this.fixDelete(null, node.parent);
}
else if (!node.left) {
this.transplant(node, node.right);
if (node.color === Color.BLACK)
this.fixDelete(node.right, node.right.parent);
}
else if (!node.right) {
this.transplant(node, node.left);
if (node.color === Color.BLACK)
this.fixDelete(node.left, node.left.parent);
}
else {
const successor = this.minimum(node.right);
if (successor.parent !== node) {
this.transplant(successor, successor.right);
successor.right = node.right;
successor.right.parent = successor;
}
this.transplant(node, successor);
successor.left = node.left;
successor.left.parent = successor;
const origColor = successor.color;
successor.color = node.color;
if (origColor === Color.BLACK)
this.fixDelete(successor.right, successor.right?.parent ?? null);
}
}
transplant(u, v) {
if (!u.parent) {
this.root = v;
}
else if (u === u.parent.left) {
u.parent.left = v;
}
else {
u.parent.right = v;
}
if (v)
v.parent = u.parent;
}
minimum(node) {
while (node.left)
node = node.left;
return node;
}
fixInsert(node) {
while (node.parent && node.parent.color === Color.RED) {
const parent = node.parent;
const grandparent = parent.parent;
if (!grandparent)
break;
if (parent === grandparent.left) {
const uncle = grandparent.right;
if (uncle && uncle.color === Color.RED) {
parent.color = Color.BLACK;
uncle.color = Color.BLACK;
grandparent.color = Color.RED;
node = grandparent;
}
else {
if (node === parent.right) {
node = parent;
this.rotateLeft(node);
}
if (node.parent)
node.parent.color = Color.BLACK;
if (node.parent?.parent)
node.parent.parent.color = Color.RED;
if (node.parent?.parent)
this.rotateRight(node.parent.parent);
}
}
else {
const uncle = grandparent.left;
if (uncle && uncle.color === Color.RED) {
parent.color = Color.BLACK;
uncle.color = Color.BLACK;
grandparent.color = Color.RED;
node = grandparent;
}
else {
if (node === parent.left) {
node = parent;
this.rotateRight(node);
}
if (node.parent)
node.parent.color = Color.BLACK;
if (node.parent?.parent)
node.parent.parent.color = Color.RED;
if (node.parent?.parent)
this.rotateLeft(node.parent.parent);
}
}
}
if (this.root)
this.root.color = Color.BLACK;
}
fixDelete(x, parent) {
// 标准 RB-Tree 删除修复(修复"双黑"问题)
let node = x;
let nodeParent = parent;
while ((!node || node.color === Color.BLACK) && node !== this.root) {
if (!nodeParent)
break;
if (node === nodeParent.left) {
let sibling = nodeParent.right;
if (!sibling)
break;
// Case 1: 兄弟是红色
if (sibling.color === Color.RED) {
sibling.color = Color.BLACK;
nodeParent.color = Color.RED;
this.rotateLeft(nodeParent);
sibling = nodeParent.right;
if (!sibling)
break;
}
// Case 2: 兄弟的两个子节点都是黑色
const sibLeft = sibling.left;
const sibRight = sibling.right;
if ((!sibLeft || sibLeft.color === Color.BLACK) &&
(!sibRight || sibRight.color === Color.BLACK)) {
sibling.color = Color.RED;
node = nodeParent;
nodeParent = node.parent;
}
else {
// Case 3: 兄弟右子黑色(左子红色)
if (!sibRight || sibRight.color === Color.BLACK) {
if (sibLeft)
sibLeft.color = Color.BLACK;
sibling.color = Color.RED;
this.rotateRight(sibling);
sibling = nodeParent.right;
if (!sibling)
break;
}
// Case 4: 兄弟右子红色
sibling.color = nodeParent.color;
nodeParent.color = Color.BLACK;
if (sibling.right)
sibling.right.color = Color.BLACK;
this.rotateLeft(nodeParent);
node = this.root;
}
}
else {
// 镜像:node 是父节点的右子
let sibling = nodeParent.left;
if (!sibling)
break;
if (sibling.color === Color.RED) {
sibling.color = Color.BLACK;
nodeParent.color = Color.RED;
this.rotateRight(nodeParent);
sibling = nodeParent.left;
if (!sibling)
break;
}
const sibLeft = sibling.left;
const sibRight = sibling.right;
if ((!sibLeft || sibLeft.color === Color.BLACK) &&
(!sibRight || sibRight.color === Color.BLACK)) {
sibling.color = Color.RED;
node = nodeParent;
nodeParent = node.parent;
}
else {
if (!sibLeft || sibLeft.color === Color.BLACK) {
if (sibRight)
sibRight.color = Color.BLACK;
sibling.color = Color.RED;
this.rotateLeft(sibling);
sibling = nodeParent.left;
if (!sibling)
break;
}
sibling.color = nodeParent.color;
nodeParent.color = Color.BLACK;
if (sibling.left)
sibling.left.color = Color.BLACK;
this.rotateRight(nodeParent);
node = this.root;
}
}
}
if (node)
node.color = Color.BLACK;
}
rotateLeft(x) {
const y = x.right;
if (!y)
return;
x.right = y.left;
if (y.left)
y.left.parent = x;
y.parent = x.parent;
if (!x.parent) {
this.root = y;
}
else if (x === x.parent.left) {
x.parent.left = y;
}
else {
x.parent.right = y;
}
y.left = x;
x.parent = y;
}
rotateRight(x) {
const y = x.left;
if (!y)
return;
x.left = y.right;
if (y.right)
y.right.parent = x;
y.parent = x.parent;
if (!x.parent) {
this.root = y;
}
else if (x === x.parent.right) {
x.parent.right = y;
}
else {
x.parent.left = y;
}
y.right = x;
x.parent = y;
}
_inorder(node, cb) {
if (!node)
return;
this._inorder(node.left, cb);
cb(node.key, node.value);
this._inorder(node.right, cb);
}
_rangeScan(node, start, end, cb) {
if (!node)
return;
if (node.key > start)
this._rangeScan(node.left, start, end, cb);
if (node.key >= start && node.key <= end)
cb(node.key, node.value);
if (node.key < end)
this._rangeScan(node.right, start, end, cb);
}
}
// ---------------------------------------------------------------------------
// MemTable
// ---------------------------------------------------------------------------
class MemTable {
constructor(maxSize = 4 * 1024 * 1024) {
this._estimatedSize = 0;
this.tree = new RedBlackTree();
this.maxSize = maxSize;
}
/** 插入或更新 */
put(key, value) {
const oldSize = this.estimateEntrySize(key, this.tree.find(key));
const newSize = this.estimateEntrySize(key, value);
this.tree.insert(key, value);
this._estimatedSize += newSize - oldSize;
}
/** 获取 */
get(key) {
return this.tree.find(key);
}
/** 删除 */
delete(key) {
const oldVal = this.tree.find(key);
if (oldVal) {
this._estimatedSize -= this.estimateEntrySize(key, oldVal);
}
return this.tree.delete(key);
}
/** 是否应刷盘 */
shouldFlush() {
return this._estimatedSize >= this.maxSize;
}
/** 获取所有有序条目 */
getAllEntries() {
return this.tree.getAllEntries();
}
/** 范围扫描 */
rangeScan(startKey, endKey) {
const entries = [];
this.tree.rangeScan(startKey, endKey, (k, v) => entries.push([k, v]));
return entries;
}
/** 条目数 */
getEntryCount() {
return this.tree.size;
}
/** 估计大小(字节) */
getEstimatedSize() {
return this._estimatedSize;
}
/** 清空 */
clear() {
this.tree.clear();
this._estimatedSize = 0;
}
/** 检查 key 是否存在 */
contains(key) {
return this.tree.find(key) !== null;
}
// -----------------------------------------------------------------------
// 内部
// -----------------------------------------------------------------------
estimateEntrySize(key, value) {
if (!value)
return 0;
let size = key.length * 2; // UTF-16
for (const entry of Object.entries(value)) {
size += entry[0].length * 2;
const v = entry[1];
if (typeof v === 'string')
size += v.length * 2;
else if (typeof v === 'number')
size += 8;
else if (typeof v === 'boolean')
size += 1;
else if (v === null || v === undefined)
size += 1;
else
size += 16; // rough estimate
}
return size;
}
}
/**
* AriaEngine Bloom Filter — 快速判定 key 是否可能存在
* @module engine/aria/index/bloom
*
* 使用双哈希函数 + Kirsch-Mitzenmacher 优化生成 k 个哈希值。
*/
// ---------------------------------------------------------------------------
// BloomFilter
// ---------------------------------------------------------------------------
class BloomFilter {
/**
* @param numKeys 预期插入的 key 数量
* @param bitsPerKey 每个 key 的位数(默认 10,误报率约 1%)
*/
constructor(numKeys, bitsPerKey = DEFAULT_BLOOM_BITS_PER_KEY) {
this._inserted = 0;
// ceil(numKeys * bitsPerKey / 8),最少 64 位
const numBits = Math.max(64, numKeys * bitsPerKey);
const numBytes = Math.ceil(numBits / 8);
this.bits = new Uint8Array(numBytes);
// k = bitsPerKey * ln(2) ≈ bitsPerKey * 0.69
this.numHashes = Math.max(1, Math.floor(bitsPerKey * 0.69));
}
/** 从现有数据恢复 */
static fromData(data, numHashes) {
const bf = new BloomFilter(1); // dummy
bf.bits = data;
bf.numHashes = numHashes;
return bf;
}
/** 插入 key */
insert(key) {
const hashes = this.getHashes(key);
for (const h of hashes) {
const byteIdx = Math.floor(h / 8);
const bitIdx = h % 8;
this.bits[byteIdx] |= (1 << bitIdx);
}
this._inserted++;
}
/** 检查 key 可能存在(false positive 可能,false negative 不可能) */
mayContain(key) {
const hashes = this.getHashes(key);
for (const h of hashes) {
const byteIdx = Math.floor(h / 8);
const bitIdx = h % 8;
if ((this.bits[byteIdx] & (1 << bitIdx)) === 0) {
return false; // 确定不存在
}
}
return true; // 可能存在
}
/** 获取序列化数据 */
serialize() {
return this.bits;
}
/** bit 数组大小 */
getBitSize() {
return this.bits.byteLength * 8;
}
/** 已插入 key 数量 */
getInsertedCount() {
return this._inserted;
}
/** hash 函数数量 */
getHashCount() {
return this.numHashes;
}
// -----------------------------------------------------------------------
// 哈希
// -----------------------------------------------------------------------
getHashes(key) {
const bits = this.bits.byteLength * 8;
const h1 = this.fnv1a(key);
const h2 = this.murmurSimple(key);
const hashes = [];
for (let i = 0; i < this.numHashes; i++) {
// Kirsch-Mitzenmacher: h_i = h1 + i * h2
const h = Math.abs((h1 + i * h2) % bits);
hashes.push(h);
}
return hashes;
}
/** FNV-1a 哈希 */
fnv1a(str) {
let hash = 0x811c9dc5;
for (let i = 0; i < str.length; i++) {
hash ^= str.charCodeAt(i);
hash = (hash * 0x01000193) >>> 0;
}
return hash;
}
/** 简化的 Murmur-like 哈希 */
murmurSimple(str) {
let hash = 0;
for (let i = 0; i < str.length; i++) {
const ch = str.charCodeAt(i);
hash = ((hash << 5) - hash + ch) | 0;
hash = (hash ^ (hash >>> 16)) >>> 0;
}
return Math.abs(hash);
}
}
/**
* AriaEngine SSTable Builder — 构建有序字符串表
* @module engine/aria/index/sstable_builder
*
* 将排序后的 key-value 数据写入 SSTable 格式。
*
* v0.4.4 格式 v2magic "SSTC")修复:
* - 块大小估算改用 UTF-8 字节长度(TextEncoder 预编码),
* 此前用字符串 .length(UTF-16 码元)估算而实际写入 UTF-8 字节,
* 中文内容(1 字 3 字节)导致缓冲区低估 → 写入越界崩溃
* - keyLen/valueLen 从 u16 升级为 u32(此前 >64KB 的 value 长度被截断,
* 线性格式整体错乱)
* - 大 value 单条独立成块(切分逻辑基于字节估算)
*
* SSTable 文件布局 (v2):
* ┌──────────────────────────────────────────────┐
* │ Data Block 0 │
* │ Data Block 1 │
* │ ... │
* │ Index Block (block offset → key range) │
* │ Bloom Filter │
* │ Footer (32 bytes) │
* │ - index_offset (u32) │
* │ - index_size (u32) │
* │ - bloom_offset (u32) │
* │ - bloom_size (u32) │
* │ - bloom_hash_count (u32) │
* │ - entry_count (u32) │
* │ - magic_number (u32, 0x53535443 ="SSTC")│
* │ - checksum (u32) │
* └──────────────────────────────────────────────┘
* 数据块条目: entryCount(u32) + [keyLen(u32) + key + valueLen(u32) + value]
* 索引块条目: [keyLen(u32) + key + blockOffset(u32) + blockSize(u32)]
*/
/** v1 格式魔数("SSTB",u16 长度字段,兼容旧文件读取) */
const SSTABLE_MAGIC_V1 = 0x53535442;
/** v2 格式魔数("SSTC"u32 长度字段 + 字节精确估算,v0.4.4) */
const SSTABLE_MAGIC_V2 = 0x53535443;
const SSTABLE_FOOTER_SIZE = 32;
class SSTableBuilder {
constructor(blockSizeLimit = 4096) {
this.entries = [];
this.blockSizeLimit = blockSizeLimit;
}
/** 添加一个 key-value 条目(必须按键排序添加) */
add(key, value) {
this.entries.push([key, value]);
}
/**
* 构建 SSTable 文件的二进制数据(v2 格式)。
* 返回 { data: Uint8Array, indexEntries: IndexEntry[] }
*/
build() {
// v0.4.4-fix: 预编码全部条目 — 块大小估算必须基于 UTF-8 字节长度,
// 字符串 .length 是 UTF-16 码元(中文 1 字 3 字节 vs 1 码元)→ 缓冲区低估越界
const encoder = new TextEncoder();
const encoded = this.entries.map(([key, value]) => ({
key,
keyBytes: encoder.encode(key),
valueBytes: encoder.encode(JSON.stringify(value)),
}));
const blocks = this.splitIntoBlocks(encoded);
const bloomFilter = new BloomFilter(this.entries.length);
// 预计算总大小(字节)
let totalSize = 0;
const blockOffsets = [];
for (const block of blocks) {
blockOffsets.push(totalSize);
totalSize += this.computeBlockSize(block);
}
// 索引块(块内最后一个 key 作为索引键)
const indexEntries = [];
for (let i = 0; i < blocks.length; i++) {
const block = blocks[i];
indexEntries.push({
key: block[block.length - 1].key,
blockOffset: blockOffsets[i],
blockSize: this.computeBlockSize(block),
});
}
const indexBlockSize = this.estimateIndexBlockSize(indexEntries);
// 序列化 bloom filter 以获取其大小
const bloomData = bloomFilter.serialize();
const bloomSize = bloomData.byteLength;
// 写入到 buffer(包含 bloom block
const finalSize = totalSize + indexBlockSize + bloomSize + SSTABLE_FOOTER_SIZE;
const buf = new ArrayBuffer(finalSize);
const view = new DataView(buf);
let offset = 0;
// ---- Data Blocks ----
for (const block of blocks) {
offset = this.writeDataBlock(view, offset, block, bloomFilter);
}
// ---- Index Block ----
const indexOffset = offset;
offset = this.writeIndexBlock(view, offset, indexEntries);
// ---- Bloom Filter Block ----
const bloomOffset = offset;
new Uint8Array(view.buffer).set(bloomData, offset);
offset += bloomSize;
// ---- Footer ----
const footerOffset = offset;
view.setUint32(footerOffset, indexOffset, false); // index_offset
view.setUint32(footerOffset + 4, indexBlockSize, false); // index_size
view.setUint32(footerOffset + 8, bloomOffset, false); // bloom_offset
view.setUint32(footerOffset + 12, bloomSize, false); // bloom_size
view.setUint32(footerOffset + 16, bloomFilter.getHashCount(), false);
view.setUint32(footerOffset + 20, this.entries.length, false);
view.setUint32(footerOffset + 24, SSTABLE_MAGIC_V2, false);
view.setUint32(footerOffset + 28, 0, false); // checksum (simplified: 0)
return {
sstableData: new Uint8Array(buf),
indexEntries,
};
}
/** 获取条目数 */
getEntryCount() {
return this.entries.length;
}
// -----------------------------------------------------------------------
// 内部
// -----------------------------------------------------------------------
/** 按 UTF-8 字节大小切分数据块;大 value 单条独立成块 */
splitIntoBlocks(encoded) {
const blocks = [];
let current = [];
for (const entry of encoded) {
current.push(entry);
// v0.4.4-fix: 基于字节估算;单条超大条目(length===1)独立成块不强行切分
if (this.computeBlockSize(current) >= this.blockSizeLimit && current.length > 1) {
blocks.push(current.slice(0, -1));
current = [entry];
}
}
if (current.length > 0)
blocks.push(current);
return blocks;
}
/** 块字节大小:entryCount(u32) + 每对 [keyLen(u32) + key + valueLen(u32) + value] */
computeBlockSize(block) {
let size = 4;
for (const e of block) {
size += 4 + e.keyBytes.length + 4 + e.valueBytes.length;
}
return size;
}
writeDataBlock(view, offset, block, bloomFilter) {
// entry count
view.setUint32(offset, block.length, false);
offset += 4;
for (const e of block) {
// v0.4.4-fix: 长度字段 u32(此前 u16 截断 >64KB 的 value
if (e.keyBytes.length > 0xFFFFFFFF || e.valueBytes.length > 0xFFFFFFFF) {
throw new Error('SSTable entry too large (exceeds u32 length field)');
}
view.setUint32(offset, e.keyBytes.length, false);
offset += 4;
new Uint8Array(view.buffer).set(e.keyBytes, offset);
offset += e.keyBytes.length;
view.setUint32(offset, e.valueBytes.length, false);
offset += 4;
new Uint8Array(view.buffer).set(e.valueBytes, offset);
offset += e.valueBytes.length;
// 插入 bloom filter
bloomFilter.insert(e.key);
}
return offset;
}
estimateIndexBlockSize(entries) {
// entryCount(u32) + each: keyLen(u32)+key+blockOffset(u32)+blockSize(u32)
let size = 4;
const encoder = new TextEncoder();
for (const entry of entries) {
size += 4 + encoder.encode(entry.key).byteLength + 8;
}
return size;
}
writeIndexBlock(view, offset, entries) {
view.setUint32(offset, entries.length, false);
offset += 4;
for (const entry of entries) {
const encoder = new TextEncoder();
const keyBytes = encoder.encode(entry.key);
view.setUint32(offset, keyBytes.length, false);
offset += 4;
new Uint8Array(view.buffer).set(keyBytes, offset);
offset += keyBytes.length;
view.setUint32(offset, entry.blockOffset, false);
offset += 4;
view.setUint32(offset, entry.blockSize, false);
offset += 4;
}
return offset;
}
}
/**
* AriaEngine SSTable Reader — 从 SSTable 二进制数据中读取
* @module engine/aria/index/sstable
*
* v0.4.4: 支持 v1"SSTB"u16 长度字段)与 v2"SSTC"u32 长度字段)双格式,
* 旧库 v1 文件仍可读(小 value 场景无缺陷),新写入使用 v2。
*/
// ---------------------------------------------------------------------------
// SSTableReader
// ---------------------------------------------------------------------------
class SSTableReader {
constructor(data, meta) {
this.indexEntries = [];
this.entryCount = 0;
this.bloomFilter = null;
/** 格式版本:1 = u16 长度字段(旧),2 = u32 长度字段(v0.4.4 */
this.format = 2;
this.data = data;
this.view = new DataView(data.buffer, data.byteOffset, data.byteLength);
this.meta = meta;
this.parseFooter();
}
/** 长度字段宽度:v2 = 4 字节 u32v1 = 2 字节 u16 */
lenFieldSize() {
return this.format === 2 ? 4 : 2;
}
readLen(offset) {
return this.format === 2 ? this.view.getUint32(offset, false) : this.view.getUint16(offset, false);
}
// -----------------------------------------------------------------------
// 查询
// -----------------------------------------------------------------------
/** 精确查找 key */
get(targetKey) {
// Bloom Filter 快速否定
if (this.bloomFilter && !this.bloomFilter.mayContain(targetKey))
return null;
const blockIdx = this.locateBlock(targetKey);
if (blockIdx < 0)
return null;
const entry = this.indexEntries[blockIdx];
const blockData = this.getBlockData(entry);
// v0.4.1-fix: 残缺文件(meta 偏移超出实际长度)跳过该块,而非抛 RangeError
if (!blockData)
return null;
const blockView = new DataView(blockData.buffer, blockData.byteOffset, blockData.byteLength);
const lenSize = this.lenFieldSize();
const entryCount = blockView.getUint32(0, false);
let offset = 4;
// 顺序扫描 block 内的条目(生产中应二分查找)
for (let i = 0; i < entryCount; i++) {
if (offset + lenSize > blockData.byteLength)
break;
const keyLen = this.format === 2 ? blockView.getUint32(offset, false) : blockView.getUint16(offset, false);
offset += lenSize;
if (offset + keyLen + lenSize > blockData.byteLength)
break;
const key = new TextDecoder().decode(blockData.slice(offset, offset + keyLen));
offset += keyLen;
const valLen = this.format === 2 ? blockView.getUint32(offset, false) : blockView.getUint16(offset, false);
offset += lenSize;
if (offset + valLen > blockData.byteLength)
break;
const valBytes = blockData.slice(offset, offset + valLen);
offset += valLen;
if (key === targetKey) {
try {
return JSON.parse(new TextDecoder().decode(valBytes));
}
catch {
return null;
}
}
}
return null;
}
/** 范围扫描 */
rangeScan(startKey, endKey, callback) {
if (this.indexEntries.length === 0)
return;
const startBlockIdx = Math.max(0, this.locateBlockGE(startKey));
const endBlockIdx = Math.min(this.indexEntries.length - 1, this.locateBlockLE(endKey));
if (startBlockIdx < 0 || endBlockIdx < 0 || startBlockIdx > endBlockIdx)
return;
const lenSize = this.lenFieldSize();
for (let bi = startBlockIdx; bi <= endBlockIdx && bi >= 0; bi++) {
const entry = this.indexEntries[bi];
const blockData = this.getBlockData(entry);
// v0.4.1-fix: 残缺块跳过(rangeScan 继续后续块,不抛异常)
if (!blockData)
continue;
const blockView = new DataView(blockData.buffer, blockData.byteOffset, blockData.byteLength);
const blockEntryCount = blockView.getUint32(0, false);
let offset = 4;
for (let i = 0; i < blockEntryCount; i++) {
if (offset + lenSize > blockData.byteLength)
break;
const keyLen = this.format === 2 ? blockView.getUint32(offset, false) : blockView.getUint16(offset, false);
offset += lenSize;
if (offset + keyLen + lenSize > blockData.byteLength)
break;
const key = new TextDecoder().decode(blockData.slice(offset, offset + keyLen));
offset += keyLen;
const valLen = this.format === 2 ? blockView.getUint32(offset, false) : blockView.getUint16(offset, false);
offset += lenSize;
if (offset + valLen > blockData.byteLength)
break;
const valBytes = blockData.slice(offset, offset + valLen);
offset += valLen;
if (key >= startKey && key <= endKey) {
try {
const value = JSON.parse(new TextDecoder().decode(valBytes));
callback(key, value);
}
catch {
// skip corrupted entry
}
}
}
}
}
/** 扫描所有条目 */
scanAll(callback) {
const lenSize = this.lenFieldSize();
for (const entry of this.indexEntries) {
const blockData = this.getBlockData(entry);
// v0.4.1-fix: 残缺块跳过(scanAll 继续后续块,不抛异常)
if (!blockData)
continue;
const blockView = new DataView(blockData.buffer, blockData.byteOffset, blockData.byteLength);
const blockEntryCount = blockView.getUint32(0, false);
let offset = 4;
for (let i = 0; i < blockEntryCount; i++) {
if (offset + lenSize > blockData.byteLength)
break;
const keyLen = this.format === 2 ? blockView.getUint32(offset, false) : blockView.getUint16(offset, false);
offset += lenSize;
if (offset + keyLen + lenSize > blockData.byteLength)
break;
const key = new TextDecoder().decode(blockData.slice(offset, offset + keyLen));
offset += keyLen;
const valLen = this.format === 2 ? blockView.getUint32(offset, false) : blockView.getUint16(offset, false);
offset += lenSize;
if (offset + valLen > blockData.byteLength)
break;
const valBytes = blockData.slice(offset, offset + valLen);
offset += valLen;
try {
const value = JSON.parse(new TextDecoder().decode(valBytes));
callback(key, value);
}
catch {
// skip corrupted entry
}
}
}
}
/** 获取元数据 */
getMeta() {
return this.meta;
}
/** 获取索引条目数 */
getIndexBlockCount() {
return this.indexEntries.length;
}
// -----------------------------------------------------------------------
// 内部
// -----------------------------------------------------------------------
parseFooter() {
if (this.data.byteLength < 32) {
throw new Error('SSTable too small: missing footer');
}
const footerOffset = this.data.byteLength - 32;
// 验证魔数(v1 "SSTB" / v2 "SSTC"
const magic = this.view.getUint32(footerOffset + 24, false);
if (magic === SSTABLE_MAGIC_V1) {
this.format = 1;
}
else if (magic === SSTABLE_MAGIC_V2) {
this.format = 2;
}
else {
throw new Error(`Invalid SSTable magic: expected ${SSTABLE_MAGIC_V1} or ${SSTABLE_MAGIC_V2}, got ${magic}`);
}
const indexOffset = this.view.getUint32(footerOffset, false);
const indexSize = this.view.getUint32(footerOffset + 4, false);
const bloomOffset = this.view.getUint32(footerOffset + 8, false);
const bloomSize = this.view.getUint32(footerOffset + 12, false);
const bloomHashCount = this.view.getUint32(footerOffset + 16, false);
this.entryCount = this.view.getUint32(footerOffset + 20, false);
// v0.4.1-fix: 完整性校验 — 索引块必须完全落在文件内,否则视为残缺文件跳过
if (indexOffset + 4 > this.data.byteLength || indexOffset + indexSize > this.data.byteLength) {
return; // 残缺文件:无索引块可读,get/rangeScan 均返回空
}
// 解析索引块
this.parseIndexBlock(indexOffset, indexSize);
// 加载 Bloom Filter
if (bloomOffset > 0 && bloomSize > 0 && bloomOffset + bloomSize <= this.data.byteLength) {
try {
const bloomBytes = this.data.slice(bloomOffset, bloomOffset + bloomSize);
this.bloomFilter = BloomFilter.fromData(bloomBytes, bloomHashCount || 10);
}
catch {
// 损坏的 bloom filter 不影响读取(仅跳过快速否定优化)
}
}
}
parseIndexBlock(offset, _size) {
const entryCount = this.view.getUint32(offset, false);
offset += 4;
const lenSize = this.lenFieldSize();
for (let i = 0; i < entryCount; i++) {
// v0.4.1-fix: 索引条目越界(keyLen/blockOffset/blockSize 超过文件长度)时中止解析,
// 已解析的有效条目仍可用于查询
if (offset + lenSize > this.data.byteLength)
break;
const keyLen = this.readLen(offset);
offset += lenSize;
if (offset + keyLen + 8 > this.data.byteLength)
break;
const key = new TextDecoder().decode(this.data.slice(offset, offset + keyLen));
offset += keyLen;
const blockOffset = this.view.getUint32(offset, false);
offset += 4;
const blockSize = this.view.getUint32(offset, false);
offset += 4;
// 跳过指向文件外的块(残缺写入产物),不抛异常
if (blockSize === 0 || blockOffset + blockSize > this.data.byteLength)
continue;
this.indexEntries.push({ key, blockOffset, blockSize });
}
}
/**
* v0.4.1-fix: 获取索引条目对应的数据块。
* 块偏移/大小越界(残缺 SSTable)时返回 null,由调用方跳过而非抛 RangeError。
*/
getBlockData(entry) {
if (entry.blockSize <= 0 || entry.blockOffset < 0)
return null;
if (entry.blockOffset + entry.blockSize > this.data.byteLength)
return null;
return new Uint8Array(this.data.buffer, this.data.byteOffset + entry.blockOffset, entry.blockSize);
}
/** 二分查找某 key 所在的 block 索引 */
locateBlock(key) {
let lo = 0;
let hi = this.indexEntries.length - 1;
while (lo <= hi) {
const mid = Math.floor((lo + hi) / 2);
const entry = this.indexEntries[mid];
if (key <= entry.key) {
// 检查是否在此 block 范围内
const firstKey = mid === 0 ? '' : this.indexEntries[mid - 1].key;
if (key > firstKey)
return mid;
hi = mid - 1;
}
else {
lo = mid + 1;
}
}
return -1;
}
locateBlockGE(key) {
let lo = 0, hi = this.indexEntries.length;
while (lo < hi) {
const mid = (lo + hi) >> 1;
if (this.indexEntries[mid].key < key)
lo = mid + 1;
else
hi = mid;
}
return lo < this.indexEntries.length ? lo : this.indexEntries.length - 1;
}
locateBlockLE(key) {
let lo = 0, hi = this.indexEntries.length;
while (lo < hi) {
const mid = (lo + hi) >> 1;
if (this.indexEntries[mid].key <= key)
lo = mid + 1;
else
hi = mid;
}
return lo > 0 ? lo - 1 : 0;
}
}
/**
* AriaEngine Merge Iterator — 多路归并迭代器
* @module engine/aria/index/merge_iterator
*
* 对多个有序 SSTable 或 MemTable 的结果进行归并去重(保留最新值)。
*/
/** 数组数据源的迭代器 */
class ArrayEntrySource {
constructor(entries) {
this.index = 0;
this.entries = entries;
}
next() {
if (this.index >= this.entries.length)
return null;
return this.entries[this.index++];
}
reset() {
this.index = 0;
}
}
/** 最小堆 */
class MinHeap {
constructor() {
this.heap = [];
}
push(node) {
this.heap.push(node);
this.bubbleUp(this.heap.length - 1);
}
pop() {
if (this.heap.length === 0)
return null;
if (this.heap.length === 1)
return this.heap.pop();
const result = this.heap[0];
this.heap[0] = this.heap.pop();
this.bubbleDown(0);
return result;
}
peek() {
return this.heap.length > 0 ? this.heap[0] : null;
}
get size() {
return this.heap.length;
}
bubbleUp(idx) {
while (idx > 0) {
const parent = Math.floor((idx - 1) / 2);
if (this.heap[idx].key >= this.heap[parent].key)
break;
[this.heap[idx], this.heap[parent]] = [this.heap[parent], this.heap[idx]];
idx = parent;
}
}
bubbleDown(idx) {
const n = this.heap.length;
while (true) {
let smallest = idx;
const left = 2 * idx + 1;
const right = 2 * idx + 2;
if (left < n && this.heap[left].key < this.heap[smallest].key)
smallest = left;
if (right < n && this.heap[right].key < this.heap[smallest].key)
smallest = right;
if (smallest === idx)
break;
[this.heap[idx], this.heap[smallest]] = [this.heap[smallest], this.heap[idx]];
idx = smallest;
}
}
}
// ---------------------------------------------------------------------------
// MergeIterator
// ---------------------------------------------------------------------------
/**
* 对多个有序数据源进行归并,重复 key 保留最新(后出现的)。
* 数据源按新鲜度排序:越新的数据源在下标越小(如 MemTable 在 SSTable 之前)。
*/
class MergeIterator {
constructor() {
this.sources = [];
this.heap = new MinHeap();
}
/** 添加数据源 */
addSource(source) {
this.sources.push(source);
this.seedFromSource(this.sources.length - 1);
}
/** 获取下一个归并后的条目 */
next() {
if (this.heap.size === 0)
return null;
const first = this.heap.pop();
const key = first.key;
let best = first;
// 刷新 first 来源的下一个值
this.seedFromSource(first.sourceIndex);
// 跳过重复 key:在多个来源中保留 sourceIndex 最小(最新)的条目
while (this.heap.peek() && this.heap.peek().key === key) {
const dup = this.heap.pop();
this.seedFromSource(dup.sourceIndex);
if (dup.sourceIndex < best.sourceIndex) {
best = dup;
}
}
return [best.key, best.value];
}
/** 耗尽管道,返回所有归并结果 */
drain() {
const result = [];
let entry = this.next();
while (entry) {
result.push(entry);
entry = this.next();
}
return result;
}
seedFromSource(sourceIndex) {
const entry = this.sources[sourceIndex].next();
if (entry) {
this.heap.push({
key: entry[0],
value: entry[1],
sourceIndex,
});
}
}
}
/**
* AriaEngine LSM-Tree — 日志结构合并树
* @module engine/aria/index/lsm
*
* 管理 MemTable + 多级 SSTable 的读写和 Compaction。
*
* v0.2.1: 完整持久化 — SSTable 元数据和数据均存入存储后端,
* 启动时自动扫描并加载所有 SSTable。
*/
// ---------------------------------------------------------------------------
// LSM
// ---------------------------------------------------------------------------
class LSM {
constructor(config) {
this.immutableMemtable = null;
/** 所有 pending flush 的 frozen memtable(含 immutableMemtable,旧→新) */
this.frozenMemtables = [];
this.levels = [];
this.sstableCache = new Map();
this.cacheSize = 0;
this.operationCount = 0;
this.initialized = false;
this.compacting = false; // 防止重复触发 compaction
/** 串行化 flush/compaction 链:保证持久化顺序与 id 分配顺序一致 */
this.flushChain = Promise.resolve();
/**
* v0.4.3-fix: 最近一次后台 flush/compaction 失败。
* 后台失败不卡死链(吞错防死锁),但在显式 flush()/close() 时报告(不静默)。
*/
this.lastBackgroundError = null;
this.memtableSizeThreshold = config.memtableSizeThreshold ?? DEFAULT_MEMTABLE_SIZE;
this.memtable = new MemTable(this.memtableSizeThreshold);
this.levelSizeMultiplier = config.levelSizeMultiplier ?? DEFAULT_LEVEL_SIZE_MULTIPLIER;
this.blockSize = config.blockSize ?? 4096;
this.sstableStore = config.sstableStore;
this.cacheLimitBytes = config.cacheLimitBytes ?? 64 * 1024 * 1024;
for (let i = 0; i < MAX_LSM_LEVELS; i++) {
this.levels.push([]);
}
}
// =======================================================================
// 初始化:从存储后端加载 SSTable 元数据
// =======================================================================
async init() {
if (this.initialized)
return;
const metas = await this.sstableStore.listMeta();
// v0.4.2-fix: 打开时完整性校验 — 验证每个 meta 引用的文件存在、可解析,
// 残缺/损坏的 SSTable 忽略并清理 meta,避免后续读取抛 RangeError 崩溃
const validMetas = [];
for (const meta of metas) {
if (await this.validateSSTable(meta)) {
validMetas.push(meta);
}
}
// 按层级分组
for (const meta of validMetas) {
if (meta.level >= 0 && meta.level < MAX_LSM_LEVELS) {
this.levels[meta.level].push(meta);
}
}
// 各层级按 id 降序排列:id 越大越新,保证读取时"最新优先"
// flush/compaction 产物均插入数组头部,数组头部即最新)
for (let i = 0; i < MAX_LSM_LEVELS; i++) {
this.levels[i].sort((a, b) => b.id - a.id);
}
// 注意:不在此处预加载全部 SSTable 数据。
// 缓存容量有上限(cacheLimitBytes),全部预加载会突破内存预算。
// 读取路径由 prefetchRange / prefetchKeys 在查询前异步加载兜底。
// id 序列由 sstableStore.allocateId() 按命名空间独立恢复。
this.initialized = true;
}
// =======================================================================
// 写入
// =======================================================================
put(key, value) {
// 写背压:level 0 SSTable 过多时排队 compaction 缓解压力
if (this.levels[0].length >= 8) {
this.enqueueCompact(0);
}
this.memtable.put(key, value);
this.operationCount++;
if (this.memtable.shouldFlush()) {
this.freezeMemtable();
}
}
delete(key) {
if (this.levels[0].length >= 8) {
this.enqueueCompact(0);
}
this.memtable.put(key, { __tombstone: true });
this.operationCount++;
if (this.memtable.shouldFlush()) {
this.freezeMemtable();
}
}
/** 获取估算内存使用(字节) */
getEstimatedMemory() {
let mem = this.memtable.getEstimatedSize();
for (const frozen of this.frozenMemtables)
mem += frozen.getEstimatedSize();
mem += this.cacheSize;
return mem;
}
/**
* 冻结当前 MemTable 为 immutable,并在串行链上排队异步刷盘。
* 冻结的 MemTable 通过闭包捕获,避免链中前一个 flush 错误处理后续冻结的表。
* 所有 pending frozen 记录在 frozenMemtables 中,flush 完成前读取路径仍可访问。
*
* v0.4.2-fix: 链上任务失败时吞错恢复链(否则 flushChain 永久 rejected
* 后续所有 flush/compaction 挂起,写路径卡死)。
*/
freezeMemtable() {
if (this.immutableMemtable) {
const frozen = this.immutableMemtable;
this.flushChain = this.enqueueOnChain(() => this.flushImmutableAsync(frozen));
}
this.immutableMemtable = this.memtable;
this.frozenMemtables.push(this.immutableMemtable);
// v0.4.2-fix: 新 memtable 用配置阈值(此前传旧表已用大小 → 阈值逐次衰减 → 频繁小文件 flush)
this.memtable = new MemTable(this.memtableSizeThreshold);
}
/** v0.4.2-fix: 在串行链上排队任务;任务失败吞错并记录,保证链不被单次失败卡死 */
enqueueOnChain(task) {
return this.flushChain
.then(task)
.catch((error) => {
// v0.4.3-fix: 记录失败(flush()/close() 时报告),不再完全静默吞错
this.lastBackgroundError = error;
// eslint-disable-next-line no-console
console.warn('[AriaEngine LSM] background flush/compaction failed:', error);
// catch 返回 undefined → 链恢复为 resolved,后续任务继续
});
}
/**
* v0.4.3-fix: 排空后台链 — 循环等待 flushChain 直到稳定。
* 任务完成时可能级联调度新任务(compaction 多级触发),单次 await 等不到。
* close/flush/clear 必须等待全部后台任务完成后才能安全关闭底层存储。
*/
async drainChain() {
while (true) {
const chain = this.flushChain;
await chain;
if (this.flushChain === chain)
return;
}
}
/** 将指定 Immutable MemTable 刷盘为 SSTableid 由 store 按命名空间分配) */
async flushImmutableAsync(frozen) {
const entries = frozen.getAllEntries();
if (entries.length === 0) {
if (this.immutableMemtable === frozen)
this.immutableMemtable = null;
return;
}
const id = await this.sstableStore.allocateId();
const builder = new SSTableBuilder(this.blockSize);
for (const [key, value] of entries) {
builder.add(key, value);
}
const { sstableData, indexEntries } = builder.build();
const meta = {
id,
level: 0,
minKey: entries[0][0],
maxKey: entries[entries.length - 1][0],
blockCount: indexEntries.length,
totalSize: sstableData.byteLength,
bloomData: null,
};
// 缓存
this.cacheSSTable(id, sstableData);
this.trimCache();
// 持久化:先存数据,再存元数据(串行链保证顺序与 id 一致)
await this.sstableStore.save(id, sstableData);
await this.sstableStore.saveMeta(meta);
if (this.immutableMemtable === frozen)
this.immutableMemtable = null;
this.levels[0].unshift(meta);
// 数据已落盘(levels 可见),从 pending frozen 移除
this.frozenMemtables = this.frozenMemtables.filter((f) => f !== frozen);
// 异步触发 compaction(不阻塞当前写入)
if (this.levels[0].length >= 4 && !this.compacting) {
this.scheduleCompact(0);
}
}
/**
* 异步调度 compaction。
* v0.4.3-fix: 去掉 setTimeout 分片 — 此前未触发的定时器在 close 后执行,
* 用已关闭的 backend 写存储(错误被吞),或 close 后 reopen 时旧闭包引用新 backend 交叉污染。
* 现在直接挂在 flushChain 上:串行执行、close 的 drainChain 能等到全部完成。
*/
scheduleCompact(level) {
if (level >= MAX_LSM_LEVELS - 1 || this.compacting)
return;
this.compacting = true;
this.flushChain = this.enqueueOnChain(() => this.compactLevelAsync(level).finally(() => {
this.compacting = false;
// 连续触发:如果 compaction 后仍然超标,继续调度
if (this.levels[level].length >= 4) {
this.scheduleCompact(level);
}
// 检查下一级是否需要 compaction
if (level + 1 < MAX_LSM_LEVELS - 1 && this.levels[level + 1].length >= 4) {
this.scheduleCompact(level + 1);
}
}));
}
/** 背压场景下排队 compaction(写入路径调用) */
enqueueCompact(level) {
if (level >= MAX_LSM_LEVELS - 1 || this.compacting)
return;
this.compacting = true;
this.flushChain = this.flushChain.then(async () => {
try {
await this.compactLevelAsync(level);
}
finally {
this.compacting = false;
}
}).catch((error) => {
this.compacting = false;
// v0.4.3-fix: 记录失败(flush()/close() 时报告)
this.lastBackgroundError = error;
// eslint-disable-next-line no-console
console.warn('[AriaEngine LSM] background compaction failed:', error);
});
}
// =======================================================================
// 读取
// =======================================================================
/**
* 预加载指定 key 范围内可能命中的所有 SSTable 到缓存。
* 在同步扫描/查找之前调用,保证 loadSSTableReader 不会因缓存未命中而返回 null。
* v0.4.3-fix: 等待链稳定(drainChain)— 后台 flush/compaction 在链上动态增长,
* 单次 await 后 compaction 仍可能合并 levels,导致扫描时新 meta 缓存未命中而跳块丢数据。
*/
async prefetchRange(startKey, endKey) {
await this.drainChain();
this.trimCache();
const toLoad = [];
for (let level = 0; level < MAX_LSM_LEVELS; level++) {
for (const meta of this.levels[level]) {
if (endKey < meta.minKey || startKey > meta.maxKey)
continue;
if (!this.sstableCache.has(meta.id))
toLoad.push(meta.id);
}
}
for (const id of toLoad) {
await this.preloadSSTable(id);
}
}
/** 预加载包含指定 key 的所有 SSTable 到缓存 */
async prefetchKeys(keys) {
if (keys.length === 0)
return;
// v0.4.3-fix: 等待链稳定(同 prefetchRange,防 compaction 竞态丢数据)
await this.drainChain();
this.trimCache();
const toLoad = new Set();
for (let level = 0; level < MAX_LSM_LEVELS; level++) {
for (const meta of this.levels[level]) {
if (this.sstableCache.has(meta.id))
continue;
for (const key of keys) {
if (key >= meta.minKey && key <= meta.maxKey) {
toLoad.add(meta.id);
break;
}
}
}
}
for (const id of toLoad) {
await this.preloadSSTable(id);
}
}
get(key) {
// 1. 活跃 MemTable
let result = this.memtable.get(key);
if (result !== null)
return this.unwrapTombstone(result);
// 2. pending frozen memtables(从新到旧)
for (let i = this.frozenMemtables.length - 1; i >= 0; i--) {
result = this.frozenMemtables[i].get(key);
if (result !== null)
return this.unwrapTombstone(result);
}
// 3. SSTable(从 Level 0 到 Level N-1
for (let level = 0; level < MAX_LSM_LEVELS; level++) {
for (const meta of this.levels[level]) {
if (key < meta.minKey || key > meta.maxKey)
continue;
const reader = this.loadSSTableReader(meta);
if (!reader)
continue;
const found = reader.get(key);
if (found !== null)
return this.unwrapTombstone(found);
}
}
return null;
}
rangeScan(startKey, endKey) {
const result = [];
this.rangeScanLazy(startKey, endKey, (k, v) => result.push([k, v]));
return result;
}
/** 惰性范围扫描:通过回调逐条返回,不一次性物化所有源 */
rangeScanLazy(startKey, endKey, callback) {
const mergeIter = new MergeIterator();
mergeIter.addSource(new ArrayEntrySource(this.memtable.rangeScan(startKey, endKey)));
// pending frozen memtables(从新到旧,新数据 sourceIndex 更小)
for (let i = this.frozenMemtables.length - 1; i >= 0; i--) {
mergeIter.addSource(new ArrayEntrySource(this.frozenMemtables[i].rangeScan(startKey, endKey)));
}
for (let level = 0; level < MAX_LSM_LEVELS; level++) {
for (const meta of this.levels[level]) {
if (endKey < meta.minKey || startKey > meta.maxKey)
continue;
const reader = this.loadSSTableReader(meta);
if (!reader)
continue;
reader.rangeScan(startKey, endKey, (k, v) => {
mergeIter.addSource(new ArrayEntrySource([[k, v]]));
});
}
}
const merged = mergeIter.drain();
for (const [k, v] of merged) {
if (!v.__tombstone) {
callback(k, v);
}
}
}
getAllEntries() {
const result = new Map();
// 从最旧层级开始聚合;同层级内从最旧到最新遍历,
// 保证 result.set 覆盖时最终保留最新值
for (let level = MAX_LSM_LEVELS - 1; level >= 0; level--) {
for (let i = this.levels[level].length - 1; i >= 0; i--) {
const meta = this.levels[level][i];
const reader = this.loadSSTableReader(meta);
if (!reader)
continue;
reader.scanAll((k, v) => result.set(k, v));
}
}
// MemTable 覆盖(最新)
for (const [k, v] of this.memtable.getAllEntries()) {
result.set(k, v);
}
for (let i = this.frozenMemtables.length - 1; i >= 0; i--) {
for (const [k, v] of this.frozenMemtables[i].getAllEntries()) {
result.set(k, v);
}
}
return Array.from(result.entries()).filter(([, v]) => !v.__tombstone);
}
// =======================================================================
// Compaction
// =======================================================================
/** 执行 Compactionpublic,供 VACUUM 等外部调用;VACUUM 期望 2 个文件即可压缩) */
async compactLevel(level) {
await this.compactLevelAsync(level, 2);
}
/**
* 串行执行 Compaction。
* @param minFiles 触发压缩的文件数门槛(自动调度用 4,VACUUM 用 2)
*
* v0.4.2-fix: 读取从存储兜底(不依赖缓存)——此前仅从缓存读,
* 缓存未命中(LRU 驱逐/单文件超缓存上限)时跳过全部文件并从 levels 移除,
* 运行中数据全部不可见。
*/
async compactLevelAsync(level, minFiles = 4) {
if (level >= MAX_LSM_LEVELS - 1)
return;
if (this.levels[level].length < minFiles)
return;
const sstables = this.levels[level].splice(0, this.levels[level].length);
const mergeIter = new MergeIterator();
const loadedMetas = [];
for (const meta of sstables) {
// 优先缓存,未命中则从存储加载(残缺文件经校验清理,跳过)
let data = this.sstableCache.get(meta.id) ?? null;
if (!data) {
try {
data = await this.sstableStore.load(meta.id);
}
catch {
data = null;
}
}
if (!data || data.byteLength < 32) {
await this.dropInvalidSSTable(meta);
continue;
}
let reader;
try {
reader = new SSTableReader(data, meta);
}
catch {
await this.dropInvalidSSTable(meta);
continue;
}
const entries = [];
reader.scanAll((k, v) => entries.push([k, v]));
mergeIter.addSource(new ArrayEntrySource(entries));
loadedMetas.push(meta);
}
const merged = mergeIter.drain();
if (merged.length === 0) {
// 没有有效数据(全部损坏):把有效 meta 放回 levels
// 避免文件从读取路径消失(数据仍在磁盘,重启可恢复)
for (const meta of loadedMetas) {
this.levels[level].push(meta);
}
this.levels[level].sort((a, b) => b.id - a.id);
return;
}
const id = await this.sstableStore.allocateId();
const builder = new SSTableBuilder(this.blockSize);
for (const [key, value] of merged) {
builder.add(key, value);
}
const { sstableData, indexEntries } = builder.build();
const meta = {
id,
level: level + 1,
minKey: merged[0][0],
maxKey: merged[merged.length - 1][0],
blockCount: indexEntries.length,
totalSize: sstableData.byteLength,
bloomData: null,
};
this.cacheSSTable(id, sstableData);
this.trimCache();
await this.sstableStore.save(id, sstableData);
await this.sstableStore.saveMeta(meta);
this.levels[level + 1].unshift(meta);
// 删除旧 SSTable
for (const old of sstables) {
this.sstableCache.delete(old.id);
await this.sstableStore.delete(old.id);
await this.sstableStore.deleteMeta(old.id);
}
}
/** 等待所有排队的 flush/compaction 完成,并将剩余数据刷盘 */
async flush() {
// v0.4.3-fix: 报告后台失败(消费一次,不永久吞错)
if (this.lastBackgroundError !== null) {
const error = this.lastBackgroundError;
this.lastBackgroundError = null;
throw new DatabaseError('AriaEngine background flush/compaction failed (data may be inconsistent)', 'ARIA_BACKGROUND_ERROR', error);
}
// v0.4.3-fix: 循环等待级联任务(flush 完成可能触发新的 compaction
await this.drainChain();
// 若仍有 frozen 数据未刷盘,在链尾追加
if (this.immutableMemtable) {
const frozen = this.immutableMemtable;
await this.flushImmutableAsync(frozen);
}
if (this.memtable.getEntryCount() > 0) {
this.freezeMemtable();
const frozen = this.immutableMemtable;
if (frozen)
await this.flushImmutableAsync(frozen);
}
this.frozenMemtables = [];
// 刷盘完成后级联调度可能触发 compaction → 排空到稳定
await this.drainChain();
}
async clear() {
// v0.4.3-fix: 清空前排空后台任务(避免 compaction 在清空后写回残留 meta/数据)
await this.drainChain();
this.memtable.clear();
this.immutableMemtable = null;
this.frozenMemtables = [];
for (const level of this.levels) {
for (const meta of level) {
this.sstableCache.delete(meta.id);
this.sstableStore.delete(meta.id).catch(() => { });
this.sstableStore.deleteMeta(meta.id).catch(() => { });
}
}
this.levels = [];
for (let i = 0; i < MAX_LSM_LEVELS; i++) {
this.levels.push([]);
}
this.sstableCache.clear();
this.cacheSize = 0;
}
getStats() {
return {
memtableSize: this.memtable.getEntryCount(),
sstableCount: this.levels.reduce((sum, l) => sum + l.length, 0),
levelCounts: this.levels.map((l) => l.length),
};
}
isInitialized() {
return this.initialized;
}
// =======================================================================
// 内部
// =======================================================================
/**
* v0.4.2-fix: 重新校验全部已加载 SSTable,移除损坏项(repair 自愈用)。
* @returns 移除的损坏 SSTable 数量
*/
async validateAll() {
let removed = 0;
for (let level = 0; level < MAX_LSM_LEVELS; level++) {
const valid = [];
for (const meta of this.levels[level]) {
if (await this.validateSSTable(meta)) {
valid.push(meta);
}
else {
this.sstableCache.delete(meta.id);
removed++;
}
}
this.levels[level] = valid;
}
return removed;
}
/**
* v0.4.2-fix: 校验单个 SSTable 的完整性。
* - 文件不存在 → 清理 meta,返回 false
* - 文件过小/魔数错误/索引越界(残缺写入产物)→ 清理 meta,返回 false
* 校验通过的数据不缓存(保持内存预算),读路径按需预加载。
*/
async validateSSTable(meta) {
try {
const data = await this.sstableStore.load(meta.id);
if (!data) {
this.dropInvalidSSTable(meta);
return false;
}
if (data.byteLength < 32) {
this.dropInvalidSSTable(meta);
return false;
}
try {
new SSTableReader(data, meta);
}
catch {
this.dropInvalidSSTable(meta);
return false;
}
return true;
}
catch {
this.dropInvalidSSTable(meta);
return false;
}
}
/** 清理无效 SSTable 的 meta 与文件(打开自愈路径) */
async dropInvalidSSTable(meta) {
// eslint-disable-next-line no-console
console.warn(`[AriaEngine LSM] Skipping corrupted SSTable id=${meta.id} (level=${meta.level})`);
try {
await this.sstableStore.deleteMeta(meta.id);
}
catch { /* 清理失败不阻塞打开 */ }
try {
await this.sstableStore.delete(meta.id);
}
catch { /* 清理失败不阻塞打开 */ }
}
unwrapTombstone(value) {
if (!value)
return null;
if (value.__tombstone)
return null;
return value;
}
/** 尝试从缓存或存储加载 SSTable,返回 Reader */
loadSSTableReader(meta) {
// 先检查缓存
const data = this.sstableCache.get(meta.id);
if (!data) {
// 缓存未命中:正常路径应在查询前通过 prefetchRange/prefetchKeys 预加载。
// 这里仅当缓存中有缺失且无兜底时返回 null(调用方跳过)。
return null;
}
// 刷新 LRU 顺序(近似:删除后重新插入使其成为最近使用)
this.sstableCache.delete(meta.id);
this.sstableCache.set(meta.id, data);
try {
return new SSTableReader(data, meta);
}
catch {
return null;
}
}
/** 预加载 SSTable 到缓存(受 cacheLimitBytes 上限约束) */
async preloadSSTable(id) {
if (this.sstableCache.has(id))
return;
const data = await this.sstableStore.load(id);
if (data) {
this.cacheSSTable(id, data);
}
}
/** 写入缓存。注意:不在加载时立即驱逐,避免破坏正在进行的同步扫描 */
cacheSSTable(id, data) {
// 已存在则先移除(保持"最近使用"语义)
if (this.sstableCache.has(id)) {
this.cacheSize -= this.sstableCache.get(id).byteLength;
this.sstableCache.delete(id);
}
this.sstableCache.set(id, data);
this.cacheSize += data.byteLength;
}
/**
* LRU 裁剪:从最早插入的条目开始驱逐,直到缓存总字节数不超过 cacheLimitBytes。
* 仅在查询/写入开始前调用,保证本次查询所需数据在同步扫描期间全部存活。
* 查询结束后由引擎调用一次,回收查询期间的临时超限。
*/
trimCache() {
while (this.cacheSize > this.cacheLimitBytes && this.sstableCache.size > 0) {
const eldestId = this.sstableCache.keys().next().value;
const evicted = this.sstableCache.get(eldestId);
this.cacheSize -= evicted.byteLength;
this.sstableCache.delete(eldestId);
}
}
/** 获取当前 SSTable 缓存大小(字节) */
getCacheSize() {
return this.cacheSize;
}
/** 获取 SSTable 缓存容量上限(字节) */
getCacheLimit() {
return this.cacheLimitBytes;
}
}
/**
* AriaEngine WAL — Write-Ahead Log
* @module engine/aria/wal/log
*
* 崩溃恢复前的写操作持久化日志。
*
* WAL 文件格式:
* ┌──────────┬──────────────┬──────────┐
* │ Record 1│ Record 2 │ ... │
* │ 4B LSN │ │ │
* │ 1B type │ │ │
* │ 4B txnId│ │ │
* │ 2B tblLen│ │ │
* │ N table│ │ │
* │ 2B keyLen│ │ │
* │ N key │ │ │
* │ 4B jsonLen│ │ │
* │ N json │ │ │
* │ 4B CRC │ │ │
* └──────────┴──────────────┴──────────┘
*/
// ---------------------------------------------------------------------------
// WAL
// ---------------------------------------------------------------------------
class WAL {
constructor(store, enabled = true, syncMode = 'batch') {
this.lsn = 0;
this.buffer = [];
/** v0.3.3: 未 checkpoint 的 WAL 累计字节数(full/batch/none 通用) */
this.bufferedBytes = 0;
this.store = store;
this.enabled = enabled;
this.syncMode = syncMode;
}
// =======================================================================
// 写入
// =======================================================================
/** 追加一条 WAL 记录(full 模式同步等待写入完成) */
async append(record) {
if (!this.enabled)
return;
this.lsn++;
const fullRecord = {
...record,
lsn: this.lsn,
checksum: 0, // 稍后计算
};
const bytes = this.encodeRecord(fullRecord);
if (this.syncMode === 'full') {
try {
await this.store.append(bytes);
this.bufferedBytes += bytes.byteLength;
}
catch {
// eslint-disable-next-line no-console
console.warn('[AriaEngine WAL] Failed to append record');
}
}
else if (this.syncMode === 'batch') {
this.buffer.push(bytes);
this.bufferedBytes += bytes.byteLength;
}
// 'none' mode: 不写 WAL
}
/** 批量追加多条 WAL 记录(组提交:合并为一次底层写入,v0.3.1) */
async appendBatch(records) {
if (!this.enabled || records.length === 0)
return;
const chunks = [];
for (const record of records) {
this.lsn++;
chunks.push(this.encodeRecord({ ...record, lsn: this.lsn, checksum: 0 }));
}
const combined = this.mergeChunks(chunks);
if (this.syncMode === 'full') {
try {
await this.store.append(combined);
this.bufferedBytes += combined.byteLength;
}
catch {
// eslint-disable-next-line no-console
console.warn('[AriaEngine WAL] Failed to append batch record');
}
}
else if (this.syncMode === 'batch') {
this.buffer.push(combined);
this.bufferedBytes += combined.byteLength;
}
// 'none' mode: 不写 WAL
}
/** 批量刷新缓冲的 WAL 记录 */
async flush() {
if (!this.enabled || this.buffer.length === 0)
return;
const combined = this.mergeChunks(this.buffer);
await this.store.append(combined);
this.buffer = [];
}
/** 合并多个字节块为一个连续缓冲区 */
mergeChunks(chunks) {
if (chunks.length === 1)
return chunks[0];
const totalLen = chunks.reduce((sum, b) => sum + b.byteLength, 0);
const combined = new Uint8Array(totalLen);
let offset = 0;
for (const buf of chunks) {
combined.set(buf, offset);
offset += buf.byteLength;
}
return combined;
}
// =======================================================================
// 恢复
// =======================================================================
/** 从 WAL 恢复未提交的事务数据 */
async recover(applyRecord) {
if (!this.enabled)
return 0;
const exists = await this.store.exists();
if (!exists)
return 0;
const data = await this.store.readAll();
if (data.byteLength === 0)
return 0;
const records = this.decodeAllRecords(data);
for (const record of records) {
applyRecord(record);
}
this.lsn = records.length > 0 ? records[records.length - 1].lsn : 0;
return records.length;
}
// =======================================================================
// Checkpoint
// =======================================================================
/** Checkpoint 后清空 WAL */
async checkpoint() {
if (!this.enabled)
return;
await this.flush();
await this.store.truncate();
this.lsn = 0;
this.bufferedBytes = 0;
}
// =======================================================================
// 统计
// =======================================================================
isEnabled() {
return this.enabled;
}
getLSN() {
return this.lsn;
}
getBufferedCount() {
return this.buffer.length;
}
/** v0.3.3: 未 checkpoint 的 WAL 累计字节数(full/batch/none 通用) */
getBufferedBytes() {
return this.bufferedBytes;
}
// -----------------------------------------------------------------------
// 编解码
// -----------------------------------------------------------------------
encodeRecord(record) {
const encoder = new TextEncoder();
const tableBytes = encoder.encode(record.tableName);
const keyBytes = encoder.encode(record.key);
const jsonStr = record.data ? JSON.stringify(record.data) : '';
const jsonBytes = encoder.encode(jsonStr);
const size = 4 + // LSN
1 + // type
4 + // txnId
2 + tableBytes.length + // table
2 + keyBytes.length + // key
4 + jsonBytes.length + // json
4; // CRC
const buf = new ArrayBuffer(size);
const view = new DataView(buf);
let offset = 0;
view.setUint32(offset, record.lsn, false);
offset += 4;
view.setUint8(offset, record.type);
offset += 1;
view.setUint32(offset, record.txnId, false);
offset += 4;
view.setUint16(offset, tableBytes.length, false);
offset += 2;
new Uint8Array(buf).set(tableBytes, offset);
offset += tableBytes.length;
view.setUint16(offset, keyBytes.length, false);
offset += 2;
new Uint8Array(buf).set(keyBytes, offset);
offset += keyBytes.length;
view.setUint32(offset, jsonBytes.length, false);
offset += 4;
new Uint8Array(buf).set(jsonBytes, offset);
offset += jsonBytes.length;
// 简单 CRC
let crc = 0;
const u8 = new Uint8Array(buf, 0, offset);
for (let i = 0; i < u8.length; i++) {
crc = ((crc << 5) - crc + u8[i]) | 0;
}
view.setUint32(offset, crc >>> 0, false);
return new Uint8Array(buf);
}
decodeAllRecords(data) {
const records = [];
const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
let offset = 0;
while (offset + 15 <= data.byteLength) {
try {
const recordStart = offset;
const lsn = view.getUint32(offset, false);
offset += 4;
const type = view.getUint8(offset);
offset += 1;
const txnId = view.getUint32(offset, false);
offset += 4;
const tableLen = view.getUint16(offset, false);
offset += 2;
if (offset + tableLen > data.byteLength)
break;
const tableName = new TextDecoder().decode(data.slice(offset, offset + tableLen));
offset += tableLen;
const keyLen = view.getUint16(offset, false);
offset += 2;
if (offset + keyLen > data.byteLength)
break;
const key = new TextDecoder().decode(data.slice(offset, offset + keyLen));
offset += keyLen;
const jsonLen = view.getUint32(offset, false);
offset += 4;
if (offset + jsonLen > data.byteLength)
break;
let recordData;
if (jsonLen > 0) {
const json = new TextDecoder().decode(data.slice(offset, offset + jsonLen));
try {
recordData = JSON.parse(json);
}
catch { /* ok */ }
}
offset += jsonLen;
// 验证 CRC(跨记录数据计算,不含 CRC 自身)
const storedCrc = view.getUint32(offset, false);
offset += 4;
const recordBytes = data.slice(recordStart, offset - 4);
let computedCrc = 0;
for (let i = 0; i < recordBytes.length; i++) {
computedCrc = ((computedCrc << 5) - computedCrc + recordBytes[i]) | 0;
}
if ((computedCrc >>> 0) !== storedCrc) {
// CRC 不匹配,跳过此损坏记录
// eslint-disable-next-line no-console
console.warn(`[AriaEngine WAL] CRC mismatch at record LSN=${lsn}, skipping`);
continue;
}
records.push({
lsn,
type,
txnId,
tableName,
key,
data: recordData,
checksum: storedCrc,
});
}
catch {
break;
}
}
return records;
}
}
/**
* AriaEngine Checkpoint — 检查点机制
* @module engine/aria/wal/checkpoint
*/
// ---------------------------------------------------------------------------
// CheckpointManager
// ---------------------------------------------------------------------------
class CheckpointManager {
constructor(lsm, wal, flushable = null, interval = 1000, walSizeThreshold = 16 * 1024 * 1024) {
this.opCount = 0;
this.lsm = lsm;
this.wal = wal;
this.flushable = flushable;
this.interval = interval;
this.walSizeThreshold = walSizeThreshold;
}
async tick() {
this.opCount++;
// 检查操作计数或 WAL 大小是否超阈值
if (this.opCount >= this.interval || this.getWALEstimatedSize() >= this.walSizeThreshold) {
await this.checkpoint();
}
}
/** 估算 WAL 大小(优先真实字节数,回退到缓冲计数估算) */
getWALEstimatedSize() {
const wal = this.wal;
if (typeof wal.getBufferedBytes === 'function') {
const bytes = wal.getBufferedBytes();
if (bytes > 0)
return bytes;
}
const count = typeof wal.getBufferedCount === 'function' ? wal.getBufferedCount() : 0;
return count * 200;
}
async checkpoint() {
await this.lsm.flush();
if (this.flushable) {
await this.flushable.flushAll();
}
await this.wal.checkpoint();
this.opCount = 0;
}
async forceCheckpoint() {
await this.checkpoint();
}
setInterval(ops) {
this.interval = ops;
}
getOpCount() {
return this.opCount;
}
}
/**
* AriaEngine Storage Backend — 存储后端抽象层
* @module engine/aria/store/backend
*
* 封装底层浏览器存储 APIIndexedDB / OPFS / Memory 回退),
* 供 Buffer Pool 的 PageIO 和 WAL 的 WALStore 使用。
*/
// =======================================================================
// IndexedDB Backend
// =======================================================================
class IndexedDBBackend {
constructor() {
this.db = null;
this.dbName = '';
this.storeName = 'data';
}
async open(name) {
this.dbName = `aria-${name}`;
return new Promise((resolve, reject) => {
const request = indexedDB.open(this.dbName, 1);
request.onupgradeneeded = () => {
const db = request.result;
if (!db.objectStoreNames.contains(this.storeName)) {
db.createObjectStore(this.storeName);
}
};
request.onsuccess = () => {
this.db = request.result;
resolve();
};
request.onerror = () => reject(new DatabaseError('Failed to open AriaEngine IndexedDB', 'ARIA_IDB_OPEN_ERROR'));
});
}
async close() {
if (this.db) {
this.db.close();
this.db = null;
}
}
isOpen() {
return this.db !== null;
}
async read(key) {
const db = this.ensureDB();
return new Promise((resolve, reject) => {
const tx = db.transaction(this.storeName, 'readonly');
const req = tx.objectStore(this.storeName).get(key);
req.onsuccess = () => resolve(req.result ?? null);
req.onerror = () => reject(new DatabaseError('Failed to read from AriaEngine store', 'ARIA_READ_ERROR'));
});
}
async write(key, data) {
const db = this.ensureDB();
return new Promise((resolve, reject) => {
const tx = db.transaction(this.storeName, 'readwrite');
tx.objectStore(this.storeName).put(data, key);
tx.oncomplete = () => resolve();
tx.onerror = () => reject(new DatabaseError('Failed to write to AriaEngine store', 'ARIA_WRITE_ERROR'));
});
}
/**
* v0.4.2-fix: 批量原子写入 — 单个 IDB 事务内写入多个 key。
* 中断时事务整体回滚,WAL 记录与 count 计数不会出现"记录在、计数丢"或反之的半写状态。
*/
async writeMany(entries) {
const keys = Object.keys(entries);
if (keys.length === 0)
return;
const db = this.ensureDB();
return new Promise((resolve, reject) => {
const tx = db.transaction(this.storeName, 'readwrite');
const store = tx.objectStore(this.storeName);
for (const key of keys) {
store.put(entries[key], key);
}
tx.oncomplete = () => resolve();
tx.onerror = () => reject(new DatabaseError('Failed to batch write to AriaEngine store', 'ARIA_WRITE_ERROR'));
});
}
async delete(key) {
const db = this.ensureDB();
return new Promise((resolve, reject) => {
const tx = db.transaction(this.storeName, 'readwrite');
tx.objectStore(this.storeName).delete(key);
tx.oncomplete = () => resolve();
tx.onerror = () => reject(new DatabaseError('Failed to delete from AriaEngine store', 'ARIA_DELETE_ERROR'));
});
}
/** v0.4.2-fix: 批量原子删除 — 单个 IDB 事务内删除多个 key */
async deleteMany(keys) {
if (keys.length === 0)
return;
const db = this.ensureDB();
return new Promise((resolve, reject) => {
const tx = db.transaction(this.storeName, 'readwrite');
const store = tx.objectStore(this.storeName);
for (const key of keys) {
store.delete(key);
}
tx.oncomplete = () => resolve();
tx.onerror = () => reject(new DatabaseError('Failed to batch delete from AriaEngine store', 'ARIA_DELETE_ERROR'));
});
}
async listKeys() {
const db = this.ensureDB();
return new Promise((resolve, reject) => {
const tx = db.transaction(this.storeName, 'readonly');
const req = tx.objectStore(this.storeName).getAllKeys();
req.onsuccess = () => resolve((req.result ?? []));
req.onerror = () => reject(new DatabaseError('Failed to list keys', 'ARIA_LIST_ERROR'));
});
}
async exists(key) {
const result = await this.read(key);
return result !== null;
}
async clear() {
const db = this.ensureDB();
return new Promise((resolve, reject) => {
const tx = db.transaction(this.storeName, 'readwrite');
tx.objectStore(this.storeName).clear();
tx.oncomplete = () => resolve();
tx.onerror = () => reject(new DatabaseError('Failed to clear AriaEngine store', 'ARIA_CLEAR_ERROR'));
});
}
ensureDB() {
if (!this.db)
throw new DatabaseError('AriaEngine storage not opened', 'ARIA_DB_NOT_OPEN');
return this.db;
}
}
// =======================================================================
// Memory Backend(回退 / 测试用)
// =======================================================================
class MemoryBackend {
constructor() {
this.store = new Map();
this.opened = false;
}
async open(_name) {
this.opened = true;
}
async close() {
this.store.clear();
this.opened = false;
}
isOpen() {
return this.opened;
}
async read(key) {
return this.store.get(key) ?? null;
}
async write(key, data) {
this.store.set(key, data);
}
async writeMany(entries) {
for (const [key, data] of Object.entries(entries)) {
this.store.set(key, data);
}
}
async delete(key) {
this.store.delete(key);
}
async deleteMany(keys) {
for (const key of keys) {
this.store.delete(key);
}
}
async listKeys() {
return Array.from(this.store.keys());
}
async exists(key) {
return this.store.has(key);
}
async clear() {
this.store.clear();
}
}
class OPFSBackend {
constructor() {
this.root = null;
this.dbDir = null;
this.dbName = '';
this.writeQueue = Promise.resolve();
}
async open(name) {
this.dbName = name;
this.root = await navigator.storage.getDirectory();
this.dbDir = await this.root.getDirectoryHandle(name, { create: true });
}
async close() {
this.dbDir = null;
this.root = null;
}
isOpen() {
return this.dbDir !== null;
}
async read(key) {
if (!this.dbDir)
return null;
try {
const fh = await this.dbDir.getFileHandle(key);
const file = await fh.getFile();
return await file.arrayBuffer();
}
catch {
return null;
}
}
async write(key, data) {
if (!this.dbDir)
return;
this.writeQueue = this.writeQueue.then(async () => {
const fh = await this.dbDir.getFileHandle(key, { create: true });
const writable = await fh.createWritable();
await writable.write(data);
await writable.close();
});
return this.writeQueue;
}
/** v0.4.2-fix: 批量写入 — 串行队列内逐个落盘(OPFS 无跨文件事务,顺序保证一致) */
async writeMany(entries) {
if (!this.dbDir)
return;
this.writeQueue = this.writeQueue.then(async () => {
for (const [key, data] of Object.entries(entries)) {
const fh = await this.dbDir.getFileHandle(key, { create: true });
const writable = await fh.createWritable();
await writable.write(data);
await writable.close();
}
});
return this.writeQueue;
}
async delete(key) {
if (!this.dbDir)
return;
this.writeQueue = this.writeQueue.then(async () => {
try {
await this.dbDir.removeEntry(key);
}
catch { /* ignore */ }
});
return this.writeQueue;
}
/** v0.4.2-fix: 批量删除 — 串行队列内逐个删除 */
async deleteMany(keys) {
if (!this.dbDir)
return;
this.writeQueue = this.writeQueue.then(async () => {
for (const key of keys) {
try {
await this.dbDir.removeEntry(key);
}
catch { /* ignore */ }
}
});
return this.writeQueue;
}
async listKeys() {
if (!this.dbDir)
return [];
const keys = [];
// FileSystemDirectoryHandle.entries() 返回 AsyncIterable,使用 any 绕过 dts 生成限制
const dir = this.dbDir;
for await (const [name] of dir.entries()) {
keys.push(name);
}
return keys;
}
async exists(key) {
if (!this.dbDir)
return false;
try {
await this.dbDir.getFileHandle(key);
return true;
}
catch {
return false;
}
}
async clear() {
if (!this.dbDir)
return;
const dir = this.dbDir;
for await (const [name] of dir.entries()) {
try {
await this.dbDir.removeEntry(name);
}
catch { /* ignore */ }
}
}
}
/**
* AriaEngine Page Header — 页面头部编解码
* @module engine/aria/page/header
*/
/**
* 初始化新页面的 Header。
*/
function initPageHeader(buf, pageId, type) {
const view = new DataView(buf);
view.setUint32(0, pageId, false);
view.setUint8(4, type);
view.setUint16(5, PAGE_HEADER_SIZE, false); // freeStart = header 之后
view.setUint16(7, buf.byteLength, false); // freeEnd = 页面末尾
view.setUint16(9, 0, false); // slotCount = 0
view.setUint32(11, 0, false); // checksum = 0
view.setUint8(15, 0);
}
/**
* AriaEngine File Manager — 页面文件管理 + PageIO 实现
* @module engine/aria/store/file_manager
*
* 负责管理页面文件的生命周期:分配/释放页面 ID,读写页面。
*/
// ---------------------------------------------------------------------------
// FileManager (implements PageIO)
// ---------------------------------------------------------------------------
class FileManager {
constructor(backend) {
this.nextPageId = 0;
this.metaLoaded = false;
this.dbName = '';
this.backend = backend;
}
/** 初始化:从存储中读取元数据 */
async init(dbName) {
this.dbName = dbName;
const meta = await this.backend.read('__aria_meta');
if (meta && meta instanceof ArrayBuffer && meta.byteLength >= 4) {
const view = new DataView(meta);
this.nextPageId = view.getUint32(0, false);
}
else {
this.nextPageId = 1;
await this.saveMeta();
}
this.metaLoaded = true;
}
// ---- PageIO ----
async readPage(pageId) {
const key = `pg_${pageId}`;
const data = await this.backend.read(key);
if (!data) {
// 第一次访问:创建新页面
return this.createEmptyPage(pageId, PageType.DATA);
}
// 确保大小正确
if (data.byteLength < PAGE_SIZE) {
const padded = new ArrayBuffer(PAGE_SIZE);
new Uint8Array(padded).set(new Uint8Array(data));
return padded;
}
return data;
}
async writePage(pageId, data) {
const key = `pg_${pageId}`;
await this.backend.write(key, data);
}
async allocatePageId() {
const id = this.nextPageId++;
await this.saveMeta();
return id;
}
async freePageId(_pageId) {
// 简化实现:不回收 pageId
const key = `pg_${_pageId}`;
await this.backend.delete(key);
}
// ---- 表页面分配 ----
/**
* 分配一个新的表元数据页面。
*/
async allocateTableRootPage() {
const pageId = await this.allocatePageId();
const data = new ArrayBuffer(PAGE_SIZE);
initPageHeader(data, pageId, PageType.META);
await this.writePage(pageId, data);
return pageId;
}
// ---- 辅助 ----
async saveMeta() {
const buf = new ArrayBuffer(8);
new DataView(buf).setUint32(0, this.nextPageId, false);
await this.backend.write('__aria_meta', buf);
}
createEmptyPage(pageId, type) {
const buf = new ArrayBuffer(PAGE_SIZE);
initPageHeader(buf, pageId, type);
return buf;
}
/** 清空所有数据 */
async clearAll() {
await this.backend.clear();
this.nextPageId = 1;
await this.saveMeta();
}
}
/**
* AriaEngine MVCC — 多版本并发控制
* @module engine/aria/transaction/mvcc
*
* 实现快照隔离 (Snapshot Isolation)。
* 每个事务看到数据库在事务开始时的快照。
*/
// ---------------------------------------------------------------------------
// MVCCManager
// ---------------------------------------------------------------------------
class MVCCManager {
constructor() {
/** 所有行版本的存储:tableName.key → 版本链 */
this.versionStore = new Map();
/** 活跃事务表:txnId → TxnEntry */
this.activeTxns = new Map();
/** 事务 ID 计数器 */
this.nextTxnId = 1;
/** 全局提交序列号(用于可见性判断) */
this.globalCommitLsn = 0;
/**
* v0.4.2-fix: 每个事务写入的 tableKey 集合 —
* commit/rollback 只遍历本事务写过的 key,避免全库版本链扫描(大表事务 O(N) → O(写入数))
*/
this.txnWriteKeys = new Map();
}
// =======================================================================
// 事务管理
// =======================================================================
/** 开始一个事务,返回事务 ID */
beginTransaction() {
const txnId = this.nextTxnId++;
this.activeTxns.set(txnId, {
txnId,
state: TransactionState.ACTIVE,
snapshotLsn: this.globalCommitLsn,
startTime: Date.now(),
});
this.txnWriteKeys.set(txnId, new Set());
return txnId;
}
/** 提交事务 */
commitTransaction(txnId) {
const txn = this.activeTxns.get(txnId);
if (!txn)
throw new Error(`Transaction ${txnId} not found`);
txn.state = TransactionState.COMMITTED;
this.globalCommitLsn++;
// v0.4.2-fix: 仅标记本事务写入的版本(此前遍历全库 versionStore
const writeKeys = this.txnWriteKeys.get(txnId);
if (writeKeys) {
for (const tableKey of writeKeys) {
const versions = this.versionStore.get(tableKey);
if (!versions)
continue;
for (const version of versions) {
if (version.txnId === txnId) {
version.committed = true;
}
}
}
}
// 清理已提交事务的记录
this.activeTxns.delete(txnId);
this.txnWriteKeys.delete(txnId);
}
/** 回滚事务 */
rollbackTransaction(txnId) {
const txn = this.activeTxns.get(txnId);
if (!txn)
throw new Error(`Transaction ${txnId} not found`);
txn.state = TransactionState.ABORTED;
// v0.4.2-fix: 仅移除本事务写入的版本(此前遍历全库 versionStore
const writeKeys = this.txnWriteKeys.get(txnId);
if (writeKeys) {
for (const tableKey of writeKeys) {
const versions = this.versionStore.get(tableKey);
if (!versions)
continue;
const filtered = versions.filter((v) => v.txnId !== txnId);
if (filtered.length === 0) {
this.versionStore.delete(tableKey);
}
else {
this.versionStore.set(tableKey, filtered);
}
}
}
this.activeTxns.delete(txnId);
this.txnWriteKeys.delete(txnId);
}
/** 检查事务是否活跃 */
isActive(txnId) {
const txn = this.activeTxns.get(txnId);
return txn !== undefined && txn.state === TransactionState.ACTIVE;
}
// =======================================================================
// 版本读写
// =======================================================================
/**
* 写入一行(创建新版本)。
*/
writeVersion(tableName, key, data, txnId) {
const tableKey = `${tableName}.${key}`;
const versions = this.versionStore.get(tableKey) ?? [];
const newVersion = {
txnId,
data,
prevVersion: versions.length > 0 ? versions[versions.length - 1] : null,
committed: false,
};
versions.push(newVersion);
this.versionStore.set(tableKey, versions);
// v0.4.2-fix: 记录本事务写过的 keycommit/rollback 精准清理)
this.txnWriteKeys.get(txnId)?.add(tableKey);
}
/**
* 读取一行(对指定事务可见的最新版本)。
*/
readVersion(tableName, key, txnId) {
const txn = this.activeTxns.get(txnId);
if (!txn)
return null;
const tableKey = `${tableName}.${key}`;
const versions = this.versionStore.get(tableKey);
if (!versions || versions.length === 0)
return null;
// 从最新版本向前遍历
for (let i = versions.length - 1; i >= 0; i--) {
const version = versions[i];
// 1. 如果是当前事务写入的(未提交),可见
if (version.txnId === txnId) {
return version.data;
}
// 2. 如果是已提交的版本,且在快照 LSN 之前提交,可见
if (version.committed) {
// 简化:所有已提交版本都可见
return version.data;
}
// 3. 其他事务的未提交版本,不可见,继续找更早的版本
}
return null;
}
/**
* 删除一行(创建墓碑版本)。
*/
deleteVersion(tableName, key, txnId) {
this.writeVersion(tableName, key, { __mvcc_tombstone: true }, txnId);
}
/**
* 获取所有行的最新已提交版本(用于非事务读取)。
*/
getLatestCommittedVersions(tableName) {
const result = {};
for (const [tableKey, versions] of this.versionStore) {
if (!tableKey.startsWith(`${tableName}.`))
continue;
const key = tableKey.slice(tableName.length + 1);
for (let i = versions.length - 1; i >= 0; i--) {
const version = versions[i];
if (version.committed && !version.data.__mvcc_tombstone) {
result[key] = version.data;
break;
}
}
}
return result;
}
/**
* v0.3.3: 丢弃指定事务的所有版本记录,但保留事务登记(Savepoint 回滚用)。
* 快照数据由调用方(引擎 txnSnapshot)负责恢复。
* v0.4.2-fix: 仅遍历本事务写过的 key(此前全库扫描)。
*/
discardVersions(txnId) {
const writeKeys = this.txnWriteKeys.get(txnId);
if (!writeKeys)
return;
for (const tableKey of writeKeys) {
const versions = this.versionStore.get(tableKey);
if (!versions)
continue;
const filtered = versions.filter((v) => v.txnId !== txnId);
if (filtered.length === 0) {
this.versionStore.delete(tableKey);
}
else {
this.versionStore.set(tableKey, filtered);
}
}
}
/**
* 清理过旧版本(GC)。
* 保留每个 key 的最新 N 个已提交版本。
*/
gc(maxVersionsPerKey = 100) {
for (const [tableKey, versions] of this.versionStore) {
if (versions.length <= maxVersionsPerKey)
continue;
// 保留最新的 maxVersionsPerKey 个版本
const pruned = versions.slice(versions.length - maxVersionsPerKey);
this.versionStore.set(tableKey, pruned);
}
}
/**
* 获取所有未提交事务中的 key 列表。
*/
getActiveWriteKeys(tableName, txnId) {
const keys = new Set();
const prefix = `${tableName}.`;
for (const [tableKey, versions] of this.versionStore) {
if (!tableKey.startsWith(prefix))
continue;
const latestVersion = versions[versions.length - 1];
if (latestVersion.txnId === txnId && !latestVersion.committed) {
keys.add(tableKey.slice(prefix.length));
}
}
return keys;
}
/**
* 清理指定表的所有版本。
*/
clearTable(tableName) {
const prefix = `${tableName}.`;
for (const [tableKey] of this.versionStore) {
if (tableKey.startsWith(prefix)) {
this.versionStore.delete(tableKey);
}
}
}
/**
* 获取活跃事务数。
*/
getActiveTxnCount() {
return this.activeTxns.size;
}
/**
* 获取全局 LSN。
*/
getGlobalLSN() {
return this.globalCommitLsn;
}
}
/**
* AriaEngine Page Format — 页面格式整合层
* @module engine/aria/page/format
*
* 组合 Header / Slot / Tuple 操作,提供统一的页面管理接口。
*/
// ---------------------------------------------------------------------------
// 页面创建
// ---------------------------------------------------------------------------
/** 创建一个新的空页面 */
function createPage(pageId, type) {
const data = new ArrayBuffer(PAGE_SIZE);
initPageHeader(data, pageId, type);
return {
pageId,
type,
data,
dirty: true,
pins: 0,
prev: null,
next: null,
lastAccess: Date.now(),
};
}
/**
* AriaEngine Buffer Pool Eviction — LRU 驱逐策略
* @module engine/aria/buffer/eviction
*/
// ---------------------------------------------------------------------------
// LRU 双向链表
// ---------------------------------------------------------------------------
/**
* LRU 链表管理器 — 双向链表,头部是 most recently used,尾部是 least recently used。
*/
class LRUList {
constructor() {
this.head = null;
this.tail = null;
this._size = 0;
}
get size() {
return this._size;
}
/** 将页面移到链表头部。如果是新页面则插入,已存在则移动。 */
moveToHead(page) {
// 如果已经在头部,无需操作
if (this.head === page)
return;
// 检测是否在链表中
const inList = page.prev !== null || page.next !== null || this.head === page || this.tail === page;
if (inList) {
// 先从当前位置移除
this.detach(page);
}
else {
this._size++;
}
// 插入头部
page.prev = null;
page.next = this.head;
if (this.head) {
this.head.prev = page;
}
this.head = page;
if (!this.tail) {
this.tail = page;
}
}
/** 从链表中移除页面 */
remove(page) {
const inList = page.prev !== null || page.next !== null || this.head === page || this.tail === page;
if (!inList)
return;
this.detach(page);
this._size = Math.max(0, this._size - 1);
}
/** 内部:只调整指针,不修改 _size */
detach(page) {
if (page.prev) {
page.prev.next = page.next;
}
else if (this.head === page) {
this.head = page.next;
}
if (page.next) {
page.next.prev = page.prev;
}
else if (this.tail === page) {
this.tail = page.prev;
}
page.prev = null;
page.next = null;
}
/** 获取 LRU 尾部(最久未使用的页面) */
getLRU() {
return this.tail;
}
/** 弹出 LRU 尾部 */
popLRU() {
const lru = this.tail;
if (lru) {
this.remove(lru);
}
return lru;
}
/** 清空链表 */
clear() {
this.head = null;
this.tail = null;
this._size = 0;
}
/** 获取所有页面(用于迭代) */
getAllPages() {
const pages = [];
let current = this.head;
while (current) {
pages.push(current);
current = current.next;
}
return pages;
}
}
/**
* 驱逐管理器 — 当 Buffer Pool 满时驱逐页面。
*/
class EvictionManager {
constructor(capacity, onEvict) {
this.lru = new LRUList();
this.capacity = capacity;
this.onEvict = onEvict;
}
/** 访问页面,更新 LRU */
access(page) {
page.lastAccess = Date.now();
this.lru.moveToHead(page);
}
/** 添加新页面到池中 */
add(page) {
this.access(page);
}
/** 移除指定页面 */
remove(page) {
this.lru.remove(page);
}
/**
* 驱逐页面直到池中有足够空间。
* 只驱逐未 pin 的干净页面(dirty=false)。
* 如果没有干净页面可驱逐,尝试刷脏页。
*/
async evictIfNeeded(count) {
let evicted = 0;
while (this.lru.size + count > this.capacity && this.lru.size > 0) {
// 找到可驱逐的页面
const victim = this.findEvictionCandidate();
if (!victim)
break;
// 脏页先刷盘
if (victim.dirty) {
await this.onEvict(victim);
victim.dirty = false;
}
this.lru.remove(victim);
evicted++;
}
return evicted;
}
/** 查找驱逐候选(优先干净页面,然后最久未用的脏页) */
findEvictionCandidate() {
// 先从尾部找未 pin 的干净页面
let current = this.lru.getLRU();
while (current) {
if (current.pins === 0 && !current.dirty)
return current;
current = current.prev;
}
// 没有干净页,找未 pin 的脏页
current = this.lru.getLRU();
while (current) {
if (current.pins === 0)
return current;
current = current.prev;
}
return null;
}
/** 获取当前大小 */
getSize() {
return this.lru.size;
}
/** 获取容量 */
getCapacity() {
return this.capacity;
}
/** 清空 */
clear() {
this.lru.clear();
}
}
/**
* AriaEngine Buffer Pool — 页面缓存池
* @module engine/aria/buffer/pool
*
* 管理固定数量页面的 LRU 缓存,减少对底层储存的访问。
*/
// ---------------------------------------------------------------------------
// Buffer Pool
// ---------------------------------------------------------------------------
class BufferPool {
constructor(pageIO, capacity = DEFAULT_BUFFER_POOL_PAGES) {
this.pages = new Map();
this.nextPageId = 0;
this.pageIO = pageIO;
this.eviction = new EvictionManager(capacity, async (page) => {
if (page.dirty) {
await this.pageIO.writePage(page.pageId, page.data);
page.dirty = false;
}
});
}
// -----------------------------------------------------------------------
// 页面获取
// -----------------------------------------------------------------------
/**
* 获取页面(必要时从磁盘读取)。
* 返回 pin 的页面,使用完成后必须调用 unpin()。
*/
async getPage(pageId) {
// 已在池中
let page = this.pages.get(pageId);
if (page) {
this.eviction.access(page);
page.pins++;
return page;
}
// 需要从磁盘加载
const buffer = await this.pageIO.readPage(pageId);
if (!buffer)
return null;
// 确保有空间
await this.eviction.evictIfNeeded(1);
const type = new DataView(buffer).getUint8(4);
page = {
pageId,
type,
data: buffer,
dirty: false,
pins: 1,
prev: null,
next: null,
lastAccess: Date.now(),
};
this.pages.set(pageId, page);
this.eviction.add(page);
return page;
}
/**
* 创建新页面。
*/
async newPage(type = PageType.DATA) {
const pageId = await this.pageIO.allocatePageId();
await this.eviction.evictIfNeeded(1);
const page = createPage(pageId, type);
page.pins = 1;
this.pages.set(pageId, page);
this.eviction.add(page);
return page;
}
/**
* 释放页面的 pin。
*/
unpin(page) {
if (page.pins > 0) {
page.pins--;
}
}
/**
* 标记页面为脏(需要写回)。
*/
markDirty(page) {
page.dirty = true;
}
/**
* 将脏页面刷新到磁盘。
*/
async flushPage(pageId) {
const page = this.pages.get(pageId);
if (page && page.dirty) {
await this.pageIO.writePage(pageId, page.data);
page.dirty = false;
}
}
/**
* 刷新所有脏页面。
*/
async flushAll() {
for (const [, page] of this.pages) {
if (page.dirty) {
await this.pageIO.writePage(page.pageId, page.data);
page.dirty = false;
}
}
}
/**
* 从缓存中删除指定页面(不刷盘)。
*/
removePage(pageId) {
const page = this.pages.get(pageId);
if (page) {
this.eviction.remove(page);
this.pages.delete(pageId);
}
}
/**
* 清空缓存池(先刷脏页)。
*/
async clear() {
await this.flushAll();
this.pages.clear();
this.eviction.clear();
}
// -----------------------------------------------------------------------
// 统计
// -----------------------------------------------------------------------
/** 获取当前缓存页面数 */
getCachedPageCount() {
return this.pages.size;
}
/** 获取缓存容量 */
getCapacity() {
return this.eviction.getCapacity();
}
/** 获取脏页面数 */
getDirtyPageCount() {
let count = 0;
for (const [, page] of this.pages) {
if (page.dirty)
count++;
}
return count;
}
}
/**
* AriaEngine LZ4 Compression — 简化 LZ4 压缩/解压
* @module engine/aria/compression/lz4
*
* v0.2.6: 修复往返一致性
* - 匹配长度截断到 19 字节(matchField 上限 15 + MIN_MATCH),长匹配分段输出
* - 组合 token 的 matchField ∈ [1,15]matchField=0 且 lo=0 表示末尾纯字面量(无 offset)
* - 消除"matchField=0 的组合 token 与纯字面量 token 歧义"
*
* Token 格式(1 字节):
* hi 4bit = litLen (0-15)
* lo 4bit = matchField (1-15, 实际匹配 = field+4)
*
* 字面量-匹配序列: [token] [litLen bytes] [2B LE offset]
* 末尾纯字面量: [token with lo=0] [litLen bytes] ← 仅在流末尾出现
*/
const MIN_MATCH = 4;
const MAX_MATCH = MIN_MATCH + 15; // 19,匹配长度上限
function compressLZ4(input) {
// 空输入直接返回(无 token 可输出)
if (input.byteLength === 0)
return input;
// 最坏情况:纯字面量分块输出 len/15 个 token + 末尾 token
// 上限:len + ceil(len/15) + 8(组合 token 的 offset 开销已包含在内)
const maxOut = input.byteLength + Math.ceil(input.byteLength / 15) + 8;
const out = new Uint8Array(maxOut);
let si = 0, di = 0;
let litStart = 0;
while (si < input.byteLength) {
// 搜索最长 backward match(截断到 MAX_MATCH,避免 token 字段溢出)
let bestLen = 0, bestOff = 0;
const searchStart = Math.max(0, si - 65535);
for (let p = searchStart; p < si; p++) {
let ml = 0;
while (si + ml < input.byteLength && p + ml < si &&
input[p + ml] === input[si + ml] && ml < MAX_MATCH)
ml++;
if (ml >= MIN_MATCH && ml > bestLen) {
bestLen = ml;
bestOff = si - p;
}
}
// 仅当匹配完整可编码(field 1-15)且字面量不超过 15 时才输出组合 token
if (bestLen > MIN_MATCH && (si - litStart) <= 15) {
const litLen = si - litStart;
const matchField = bestLen - MIN_MATCH; // 1..15
out[di++] = ((litLen & 0x0F) << 4) | (matchField & 0x0F);
for (let j = 0; j < litLen; j++)
out[di++] = input[litStart + j];
out[di++] = bestOff & 0xFF;
out[di++] = (bestOff >> 8) & 0xFF;
si += bestLen;
litStart = si;
}
else {
// 无匹配 / 匹配长度 4(field=0 有歧义)→ 继续累积字面量
si++;
// 字面量达到 15 字节上限:结清为纯字面量 token(lo=0),
// 否则后续组合 token 的字面量长度会超过 token 字段上限
if (si - litStart >= 15) {
out[di++] = (15 & 0x0F) << 4; // lo=0 无匹配
for (let j = 0; j < 15; j++)
out[di++] = input[litStart + j];
litStart = si;
}
}
}
// 输出末尾纯字面量(matchField=0,无 offset
let remaining = si - litStart;
while (remaining > 0) {
const chunk = Math.min(remaining, 15);
out[di++] = (chunk & 0x0F) << 4; // lo=0 表示无匹配/无 offset
for (let j = 0; j < chunk; j++)
out[di++] = input[litStart + j];
remaining -= chunk;
litStart += chunk;
}
// 始终输出压缩流(即使比原数据略大)。
// 注意:不能返回原样 input —— 解压端无法区分"压缩流"与"原始数据"
// 原样返回会导致解压器将原始字节误解析为 token(v0.2.6 修复)
return out.slice(0, di);
}
function decompressLZ4(input, originalSize) {
const out = new Uint8Array(originalSize);
let si = 0, di = 0;
while (si < input.byteLength && di < originalSize) {
const token = input[si++];
const litLen = (token >> 4) & 0x0F;
const matchField = token & 0x0F;
// 复制字面量
for (let i = 0; i < litLen && si < input.byteLength && di < originalSize; i++) {
out[di++] = input[si++];
}
// matchField=0:纯字面量 token(无 offset 无匹配)。
// 可能出现在流中任意位置(超长字面量分块输出),不能 break
if (matchField === 0)
continue;
// 组合 token:读取 offset + 复制匹配(可能自重叠)
if (si + 1 >= input.byteLength)
break;
const offset = input[si++] | (input[si++] << 8);
const matchLen = matchField + MIN_MATCH;
for (let i = 0; i < matchLen && di < originalSize; i++) {
out[di] = out[di - offset];
di++;
}
}
return out;
}
/**
* AriaEngine Crypto — 页面级 AES-GCM 加密
* @module engine/aria/crypto
*
* v0.2.5: 改为实例化 CryptoManager,避免多实例共享全局状态。
* 保留全局函数兼容旧代码(委托给全局单例)。
*/
const ALGO = 'AES-GCM';
const IV_LENGTH = 12;
/**
* CryptoManager — 实例级加密管理器
* 每个 AriaEngine 实例可拥有独立的加密配置。
*/
class CryptoManager {
constructor() {
this.cryptoKey = null;
this._enabled = false;
}
get enabled() { return this._enabled; }
async init(password, salt) {
const enc = new TextEncoder();
const keyMaterial = await crypto.subtle.importKey('raw', enc.encode(password), 'PBKDF2', false, ['deriveKey']);
const actualSalt = salt || crypto.getRandomValues(new Uint8Array(16));
this.cryptoKey = await crypto.subtle.deriveKey({ name: 'PBKDF2', salt: actualSalt, iterations: 100000, hash: 'SHA-256' }, keyMaterial, { name: ALGO, length: 256 }, false, ['encrypt', 'decrypt']);
this._enabled = true;
return actualSalt;
}
async encryptPage(data) {
if (!this.cryptoKey)
throw new Error('Crypto not initialized');
const iv = crypto.getRandomValues(new Uint8Array(IV_LENGTH));
// 传 TypedArray 视图而非裸 ArrayBufferSubtleCrypto 通过 ArrayBuffer.isView 检查,
// 对跨 realm / 跨 vm 环境的 ArrayBuffer 兼容(Node 18/20 的 webcrypto 对裸 ArrayBuffer 检查严格)
const ciphertext = await crypto.subtle.encrypt({ name: ALGO, iv }, this.cryptoKey, new Uint8Array(data));
return { iv: iv, data: ciphertext };
}
async decryptPage(iv, data) {
if (!this.cryptoKey)
throw new Error('Crypto not initialized');
return crypto.subtle.decrypt({ name: ALGO, iv }, this.cryptoKey, new Uint8Array(data));
}
close() {
this.cryptoKey = null;
this._enabled = false;
}
}
// ---------------------------------------------------------------------------
// 全局兼容层(旧代码仍可使用全局函数)
// ---------------------------------------------------------------------------
const globalCrypto = new CryptoManager();
/** @deprecated 使用 CryptoManager 实例代替 */
function isCryptoEnabled() { return globalCrypto.enabled; }
/** @deprecated 使用 CryptoManager 实例代替 */
async function encryptPage(data) {
return globalCrypto.encryptPage(data);
}
/** @deprecated 使用 CryptoManager 实例代替 */
async function decryptPage(iv, data) {
return globalCrypto.decryptPage(iv, data);
}
/**
* AriaEngine — 自研页面式存储引擎主类
* @module engine/aria/index
*
* v0.4.1: 外键级联 + ALTER TABLE 重写 + clearAll 重置 + 崩溃恢复加固
*/
// ---------------------------------------------------------------------------
// AriaEngine
// ---------------------------------------------------------------------------
class AriaEngine {
constructor(config = {}) {
this.name = 'aria';
this.opened = false;
this.dbName = '';
// 表结构
this.schemas = new Map();
this.tablePKs = new Map();
this.opCounter = 0;
// 二级索引:table.colKey → LSM
this.secondaryIndexes = new Map();
// MVCC 事务
this.mvcc = new MVCCManager();
this.currentTxnId = null;
this.txnSnapshot = null;
this.gcCounter = 0;
// ---- Savepoint 嵌套事务 ----
this.savepoints = new Map();
this.config = { ...DEFAULT_ARIA_CONFIG, ...config };
}
// =======================================================================
// 生命周期
// =======================================================================
async open(dbName, _version) {
if (this.opened)
return;
// v0.4.2-fix: 引擎内部错误统一包装为 DatabaseErrorARIA_OPEN_ERROR),
// 应用层可拿到 code 分类处理,不再抛出原生 RangeError/TypeError
try {
await this.openInternal(dbName);
}
catch (error) {
if (error instanceof DatabaseError)
throw error;
throw new DatabaseError(`Failed to open AriaEngine database "${dbName}"`, 'ARIA_OPEN_ERROR', error);
}
}
/** open 内部实现(错误包装在 open 外层) */
async openInternal(dbName) {
this.dbName = dbName;
// 1. 存储后端
if (this.config.storageBackend === 'opfs') {
this.backend = new OPFSBackend();
}
else if (this.config.storageBackend === 'indexeddb') {
this.backend = new IndexedDBBackend();
}
else {
this.backend = new MemoryBackend();
}
await this.backend.open(dbName);
// 2a. FileManager (PageIO 实现) + Buffer Pool
const fileManager = new FileManager(this.backend);
await fileManager.init(dbName);
this.bufferPool = new BufferPool(fileManager, this.config.bufferPoolPages);
// 2. 构建 SSTableStore
const sstableStore = this.createSSTableStore('main');
// 3. 初始化主 LSMPK 索引)
this.lsm = new LSM({
memtableSizeThreshold: this.config.memtableSizeThreshold,
levelSizeMultiplier: this.config.levelSizeMultiplier,
blockSize: this.config.pageSize,
bloomBitsPerKey: this.config.bloomFilterBitsPerKey,
// SSTable 缓存上限 = BufferPool 页数 × 页面大小(默认 256 页 ≈ 1MB 可控内存)
cacheLimitBytes: this.config.bufferPoolPages * this.config.pageSize,
sstableStore,
});
// 4. 初始化 WAL
this.wal = new WAL({
append: async (data) => {
// v0.4.2-fix: 记录写入与 count 计数在同一底层事务中原子提交,
// 中断时整体回滚,杜绝"记录在、计数丢"导致恢复漏读的丢数据问题
const idx = await this.getWALCount();
const slice = new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
const copy = slice.buffer.slice(slice.byteOffset, slice.byteOffset + slice.byteLength);
await this.backend.writeMany({
[`__wal_${idx}`]: copy,
__wal_count: new TextEncoder().encode(String(idx + 1)).buffer,
});
},
readAll: async () => {
// v0.4.2-fix: 不依赖 count 计数,直接扫描全部 __wal_* 键,
// 避免 count 与实际记录不一致时漏读(与 checkpoint/并发写入竞态无关)
const keys = (await this.backend.listKeys())
.filter((k) => k.startsWith('__wal_') && k !== '__wal_count')
.sort((a, b) => {
const na = parseInt(a.slice('__wal_'.length), 10);
const nb = parseInt(b.slice('__wal_'.length), 10);
return (isNaN(na) ? 0 : na) - (isNaN(nb) ? 0 : nb);
});
if (keys.length === 0)
return new Uint8Array(0);
const chunks = [];
for (const key of keys) {
const d = await this.backend.read(key);
if (d)
chunks.push(new Uint8Array(d));
}
const total = chunks.reduce((s, c) => s + c.byteLength, 0);
const combined = new Uint8Array(total);
let off = 0;
for (const c of chunks) {
combined.set(c, off);
off += c.byteLength;
}
return combined;
},
truncate: async () => {
// v0.4.2-fix: 扫描删除全部 WAL 记录键 + count 键(单事务原子清理)
const keys = (await this.backend.listKeys())
.filter((k) => k.startsWith('__wal_'));
await this.backend.deleteMany(keys);
await this.setWALCount(0);
},
exists: async () => {
// v0.4.2-fix: 与 readAll 一致按 key 扫描判断(count 可能因崩溃截断而滞后)
const keys = (await this.backend.listKeys())
.filter((k) => k.startsWith('__wal_') && k !== '__wal_count');
return keys.length > 0;
},
}, this.config.walEnabled, this.config.walSyncMode);
// 5. 恢复 Schema
await this.loadSchemas();
// v0.4.2-fix: 为 schema 中带 index/unique 标记的列重建二级索引 LSM。
// 此前重开只恢复 schema 不恢复索引 LSM → 索引查询静默回退全表、
// createIndex 因 colDef 已有标记直接 return → 索引永久缺失。
// 索引数据已持久化在独立命名空间(sst_idx_* / meta),init() 直接加载。
for (const [tableName, schema] of this.schemas) {
const pkCol = this.tablePKs.get(tableName);
for (const [colName, colDef] of Object.entries(schema.columns)) {
if ((colDef.index || colDef.unique) && colName !== pkCol) {
const idxKey = `${tableName}:idx:${colName}`;
if (!this.secondaryIndexes.has(idxKey)) {
const idxLsm = new LSM({
memtableSizeThreshold: this.config.memtableSizeThreshold,
levelSizeMultiplier: this.config.levelSizeMultiplier,
blockSize: this.config.pageSize,
bloomBitsPerKey: this.config.bloomFilterBitsPerKey,
cacheLimitBytes: this.config.bufferPoolPages * this.config.pageSize,
sstableStore: this.createSSTableStore(`idx_${tableName}_${colName}`),
});
await idxLsm.init();
this.secondaryIndexes.set(idxKey, idxLsm);
}
}
}
}
// 6. 初始化 LSM(加载 SSTable 元数据)
await this.lsm.init();
// 7. WAL 恢复(两阶段:先扫描事务边界,仅回放已提交事务)
const committedTxns = new Set();
const allRecords = [];
await this.wal.recover((r) => allRecords.push(r));
// 第一遍:确定已提交事务
for (const r of allRecords) {
if (r.type === WALRecordType.COMMIT)
committedTxns.add(r.txnId);
if (r.type === WALRecordType.ROLLBACK)
committedTxns.delete(r.txnId);
}
// 第二遍:仅应用 txnId==0(非事务)或已提交事务的数据
for (const r of allRecords) {
if (r.txnId === 0 || committedTxns.has(r.txnId)) {
if (r.type === WALRecordType.DROP_TABLE) {
// v0.3.3: DROP_TABLE 回放(异步:需预加载 SSTable 后清除残留数据)
await this.applyDropTableRecovery(r.tableName);
}
else {
this.applyWALRecord(r);
}
}
}
// v0.3.3: 恢复完成后将回放数据落盘并截断 WAL,
// 避免每次重启重复回放 + WAL 无限膨胀
if (allRecords.length > 0) {
await this.lsm.flush();
await this.wal.checkpoint();
// v0.4.2-fix: WAL 回放只更新主 LSM,二级索引 LSM 未同步 →
// 崩溃前最后一批写入的索引缺失,重开时索引查询丢行。
// 恢复后全量重建所有表的二级索引(幂等)。
for (const tableName of this.schemas.keys()) {
await this.reindexTableInternal(tableName);
}
}
// 8. Checkpoint Manager(接入 WAL 大小阈值)
// v0.4.2-fix: 事务活跃时 checkpoint 不得截断 WAL —
// 否则 BEGIN/INSERT 记录被截断,COMMIT 后崩溃恢复丢失整个事务数据
this.checkpointManager = new CheckpointManager(this.lsm, {
checkpoint: async () => {
if (this.currentTxnId)
return;
await this.wal.checkpoint();
},
flush: async () => {
if (this.currentTxnId)
return;
await this.wal.flush();
},
getBufferedBytes: () => this.wal.getBufferedBytes(),
getBufferedCount: () => this.wal.getBufferedCount(),
}, { flushAll: async () => { await this.lsm.flush(); } }, this.config.checkpointInterval, this.config.walSizeThreshold);
this.opened = true;
}
async close() {
if (!this.opened)
return;
await this.persistSchemas();
await this.lsm.flush();
// v0.4.2-fix: 同步落盘全部二级索引 LSM — 此前只 flush 主 LSM
// 优雅关闭后索引 memtable 未落盘 → 重开索引为空 → 索引查询返回空结果
for (const idxLsm of this.secondaryIndexes.values()) {
await idxLsm.flush();
}
await this.wal.flush();
// v0.4.2-fix: close 前 checkpoint(截断 WAL)—
// 此前只 flush 不截断,下次打开会重放全部历史 WAL 记录(含已落盘 SSTable 的数据),
// 重复解析/重复 put 拖慢启动,并与恢复后 flush+checkpoint 竞争放大丢数据
await this.wal.checkpoint();
await this.backend.close();
// v0.4.2-fix: 清空运行期状态(此前 close 后 mvcc/txn 残留,
// 重开时 beginTransaction 报 TX_ACTIVE 或读到陈旧快照)
this.schemas.clear();
this.tablePKs.clear();
this.secondaryIndexes.clear();
this.mvcc = new MVCCManager();
this.currentTxnId = null;
this.txnSnapshot = null;
this.savepoints.clear();
this.opCounter = 0;
this.opened = false;
}
/**
* v0.4.2-fix: 崩溃恢复/自愈 — 校验并移除损坏 SSTable、截断 WAL、重建二级索引。
* 应用层检测到异常后调用,无需删库重建。
*/
async repair() {
this.ensureOpen();
// 1. 校验全部 SSTable,移除残缺项(打开时已做一次,此处兜底运行期损坏)
const removed = await this.lsm.validateAll();
// 2. 将 WAL 残留数据落盘并截断,避免无限重放
await this.lsm.flush();
await this.wal.checkpoint();
// 3. 重建所有表的二级索引(修复索引与主数据不一致)
for (const tableName of this.schemas.keys()) {
await this.reindexTable(tableName);
}
if (removed > 0) {
// eslint-disable-next-line no-console
console.warn(`[AriaEngine] repair: removed ${removed} corrupted SSTable(s)`);
}
}
/**
* v0.4.1: 重置数据库 — 清空全部数据与表结构(演示页刷新/重新初始化用)。
* 清空存储后端、LSM、WAL、MVCC 与二级索引,后续可继续使用本实例。
*/
async clearAll() {
this.ensureOpen();
// 清空存储后端(页面文件 / WAL 记录 / schema 记录 / 元数据)
await this.backend.clear();
this.schemas.clear();
this.tablePKs.clear();
this.secondaryIndexes.clear();
this.lsm.clear();
this.mvcc = new MVCCManager();
this.currentTxnId = null;
this.txnSnapshot = null;
this.savepoints.clear();
this.opCounter = 0;
// 持久化空 schema(防止旧 schema 记录残留)
await this.persistSchemas();
// 重置 WAL 状态(backend.clear 已清记录,同步内存计数)
await this.wal.checkpoint();
}
isOpen() { return this.opened; }
// ---- v0.4.2-fix: 库内元数据(迁移版本持久化用) ----
async getMeta(key) {
const raw = await this.backend.read(`__meta_${key}`);
return raw ? new TextDecoder().decode(raw) : null;
}
async setMeta(key, value) {
await this.backend.write(`__meta_${key}`, new TextEncoder().encode(value).buffer);
}
// =======================================================================
// 表管理
// =======================================================================
async createTable(schema) {
this.ensureOpen();
// v0.4.2-fix: Aria 事务中 DDL 显式拒绝(事务快照只覆盖行数据,
// 结构变更无法回滚;Memory/IndexedDB 引擎快照可回滚,行为不一致 → 明确报错而非静默)
this.ensureNoDDLInTransaction('CREATE TABLE');
if (this.schemas.has(schema.name)) {
throw new DatabaseError(`Table "${schema.name}" already exists`, 'TABLE_EXISTS');
}
this.schemas.set(schema.name, schema);
this.tablePKs.set(schema.name, this.getPK(schema));
// 为索引列创建二级索引 LSM(每个索引使用独立命名空间的 SSTableStore,避免 id/meta 冲突)
// v0.3.3: 主键列不建冗余二级索引(主 LSM 本身就是 PK 索引,范围查询走前缀扫描)
for (const [colName, colDef] of Object.entries(schema.columns)) {
if (colDef.index || colDef.unique) {
const idxKey = `${schema.name}:idx:${colName}`;
if (!this.secondaryIndexes.has(idxKey)) {
const idxLsm = new LSM({
memtableSizeThreshold: this.config.memtableSizeThreshold,
levelSizeMultiplier: this.config.levelSizeMultiplier,
blockSize: this.config.pageSize,
bloomBitsPerKey: this.config.bloomFilterBitsPerKey,
cacheLimitBytes: this.config.bufferPoolPages * this.config.pageSize,
sstableStore: this.createSSTableStore(`idx_${schema.name}_${colName}`),
});
await idxLsm.init();
this.secondaryIndexes.set(idxKey, idxLsm);
}
}
}
await this.persistSchemas();
await this.wal.append({
type: WALRecordType.CREATE_TABLE,
txnId: 0,
tableName: schema.name,
key: '',
data: { schema: JSON.stringify(schema) },
});
}
async dropTable(tableName) {
this.ensureOpen();
this.ensureNoDDLInTransaction('DROP TABLE');
this.ensureTable(tableName);
// 删除表中所有行
const rows = await this.getAllRows(tableName);
for (const row of rows) {
const pkCol = this.tablePKs.get(tableName);
this.lsm.delete(`${tableName}:${row[pkCol]}`);
}
// v0.4.2-fix: 清理该表的全部二级索引 LSM 与持久化文件 —
// 此前残留孤儿索引,重建同名表后旧索引数据污染新表(索引查询返回错误行)
await this.cleanupTableIndexes(tableName);
this.schemas.delete(tableName);
this.tablePKs.delete(tableName);
await this.persistSchemas();
await this.wal.append({
type: WALRecordType.DROP_TABLE,
txnId: 0,
tableName,
key: '',
});
}
/**
* v0.4.2-fix: 清理指定表的全部二级索引 LSM(内存 + 存储文件 + meta)。
* dropTable / DROP_TABLE 恢复 / alterTable DROP 索引列 共用。
*/
async cleanupTableIndexes(tableName) {
const prefix = `${tableName}:idx:`;
const toDelete = [];
for (const [idxKey, idxLsm] of this.secondaryIndexes) {
if (!idxKey.startsWith(prefix))
continue;
toDelete.push(idxKey);
try {
await idxLsm.clear();
}
catch { /* 清理失败不阻塞 */ }
}
for (const idxKey of toDelete) {
this.secondaryIndexes.delete(idxKey);
}
}
async hasTable(tableName) {
return this.schemas.has(tableName);
}
async getTableNames() {
return Array.from(this.schemas.keys());
}
async getTableSchema(tableName) {
return this.schemas.get(tableName) ?? null;
}
// =======================================================================
// CRUD
// =======================================================================
async insert(tableName, rows) {
this.ensureOpen();
this.ensureTable(tableName);
const schema = this.schemas.get(tableName);
const pkCol = this.tablePKs.get(tableName);
const pks = [];
// v0.3.1: 批量 WAL 写入(组提交),一次 insert 合并为一次落盘
const walRecords = [];
for (const row of rows) {
const validated = this.validateRow(schema, row);
const pkValue = String(validated[pkCol]);
const key = `${tableName}:${pkValue}`;
// Check duplicate in LSM + transaction snapshot
await this.lsm.prefetchKeys([key]);
const existing = this.currentTxnId
? (this.txnSnapshot?.get(key) ?? this.lsm.get(key))
: this.lsm.get(key);
if (existing && !existing.__txn_deleted) {
throw new DatabaseError(`Duplicate primary key "${pkValue}" in table "${tableName}"`, 'DUPLICATE_KEY');
}
if (this.currentTxnId && this.txnSnapshot) {
// Within transaction: buffer to snapshot + MVCC version chain
this.txnSnapshot.set(key, validated);
this.mvcc.writeVersion(tableName, pkValue, validated, this.currentTxnId);
}
else {
// Direct write to LSM (PK index)
this.lsm.put(key, validated);
}
// 更新二级索引
this.updateSecondaryIndexes(tableName, pkValue, validated, null);
pks.push(pkValue);
walRecords.push({
type: WALRecordType.INSERT,
txnId: this.currentTxnId ?? 0,
tableName,
key: pkValue,
data: validated,
});
}
await this.wal.appendBatch(walRecords);
this.opCounter += rows.length;
this.checkMemoryBudget();
await this.checkpointManager.tick();
this.tryGC();
return pks;
}
async find(tableName, query) {
this.ensureOpen();
this.ensureTable(tableName);
let rows;
// Try index lookup
const fastPath = await this.tryIndexLookup(tableName, query);
if (fastPath !== null) {
rows = fastPath;
}
else {
rows = await this.getAllRows(tableName);
}
// v0.3.3: 事务内合并未提交快照(统一在 mergeTxnSnapshot 处理)
rows = this.mergeTxnSnapshot(tableName, rows);
// WHERE filter
if (query.where && Object.keys(query.where).length > 0) {
rows = rows.filter((row) => matchWhere(row, query.where));
}
// ORDER
if (query.orderBy && query.orderBy.length > 0) {
rows = applyOrderBy(rows, query.orderBy);
}
// LIMIT/OFFSET
const offset = query.offset ?? 0;
const limit = query.limit ?? rows.length;
rows = rows.slice(offset, offset + limit);
// Column projection
if (query.columns && query.columns.length > 0 && query.columns[0] !== '*') {
rows = rows.map((row) => projectColumns(row, query.columns));
}
// 查询完成,回收查询期间的临时缓存超限
this.trimAllCaches();
return rows;
}
async update(tableName, query, updates) {
this.ensureOpen();
this.ensureTable(tableName);
const schema = this.schemas.get(tableName);
const rows = await this.getAllRows(tableName);
let count = 0;
// v0.3.1: 批量 WAL 写入(组提交)
const walRecords = [];
// v0.4.2-fix: ON UPDATE 级联环路保护
const visited = new Set();
for (const row of rows) {
const pkCol = this.tablePKs.get(tableName);
const key = `${tableName}:${row[pkCol]}`;
if (!query.where || Object.keys(query.where).length === 0 || matchWhere(row, query.where)) {
const updated = { ...row, ...updates };
this.validateRow(schema, updated);
// v0.4.2-fix: 支持更新主键 — 删除旧键 + 落新键 + WAL 两条记录
const newPk = String(updated[pkCol]);
const pkChanged = newPk !== String(row[pkCol]);
if (pkChanged) {
// ON UPDATE 外键级联(RESTRICT 抛错 / CASCADE / SET NULL
await this.applyForeignKeyUpdateRules(tableName, String(row[pkCol]), newPk, walRecords, visited);
}
if (this.currentTxnId && this.txnSnapshot) {
if (pkChanged) {
this.txnSnapshot.set(key, { __txn_deleted: true });
this.mvcc.deleteVersion(tableName, String(row[pkCol]), this.currentTxnId);
}
this.txnSnapshot.set(`${tableName}:${newPk}`, updated);
this.mvcc.writeVersion(tableName, newPk, updated, this.currentTxnId);
}
else {
if (pkChanged)
this.lsm.delete(key);
this.lsm.put(`${tableName}:${newPk}`, updated);
}
count++;
if (pkChanged) {
walRecords.push({
type: WALRecordType.DELETE,
txnId: this.currentTxnId ?? 0,
tableName,
key: String(row[pkCol]),
});
}
walRecords.push({
type: WALRecordType.UPDATE,
txnId: this.currentTxnId ?? 0,
tableName,
key: newPk,
data: updated,
});
// 更新二级索引(主键变更时旧索引条目一并清理)
this.updateSecondaryIndexes(tableName, newPk, updated, pkChanged ? row : null);
}
}
await this.wal.appendBatch(walRecords);
this.opCounter += count;
await this.checkpointManager.tick();
this.trimAllCaches();
return count;
}
/**
* v0.4.2-fix: ON UPDATE 外键级联 — 主键 oldPk → newPk 时处理引用表。
* RESTRICT 抛错 / CASCADE 更新 FK / SET NULL 置空(含索引与 WAL 记录)。
* 两阶段:先全量 RESTRICT 检查,再执行级联。
*/
async applyForeignKeyUpdateRules(tableName, oldPk, newPk, walRecords, visited) {
const visitKey = `${tableName}:${oldPk}`;
if (visited.has(visitKey))
return;
visited.add(visitKey);
// 阶段 1: RESTRICT 检查
for (const [refTableName, refSchema] of this.schemas) {
if (refTableName === tableName)
continue;
for (const [colName, colDef] of Object.entries(refSchema.columns)) {
if (!colDef.references || !colDef.onUpdate)
continue;
const [refTable] = colDef.references.split('.');
if (refTable !== tableName)
continue;
if (colDef.onUpdate !== 'RESTRICT')
continue;
const refRows = await this.getAllRows(refTableName);
if (refRows.some((r) => String(r[colName]) === oldPk)) {
throw new DatabaseError(`Cannot update "${tableName}" key "${oldPk}": foreign key "${colName}" in "${refTableName}" has dependent rows`, 'FOREIGN_KEY_VIOLATION');
}
}
}
// 阶段 2: CASCADE / SET NULL
for (const [refTableName, refSchema] of this.schemas) {
if (refTableName === tableName)
continue;
for (const [colName, colDef] of Object.entries(refSchema.columns)) {
if (!colDef.references || !colDef.onUpdate)
continue;
const [refTable] = colDef.references.split('.');
if (refTable !== tableName)
continue;
if (colDef.onUpdate !== 'CASCADE' && colDef.onUpdate !== 'SET NULL')
continue;
const refRows = await this.getAllRows(refTableName);
for (const refRow of refRows) {
if (String(refRow[colName]) !== oldPk)
continue;
const refPkCol = this.tablePKs.get(refTableName);
const refPk = String(refRow[refPkCol]);
const updatedRef = { ...refRow, [colName]: colDef.onUpdate === 'CASCADE' ? newPk : null };
const refKey = `${refTableName}:${refPk}`;
if (this.currentTxnId && this.txnSnapshot) {
this.txnSnapshot.set(refKey, updatedRef);
this.mvcc.writeVersion(refTableName, refPk, updatedRef, this.currentTxnId);
}
else {
this.lsm.put(refKey, updatedRef);
}
this.updateSecondaryIndexes(refTableName, refPk, updatedRef, refRow);
walRecords.push({
type: WALRecordType.UPDATE,
txnId: this.currentTxnId ?? 0,
tableName: refTableName,
key: refPk,
data: updatedRef,
});
}
}
}
}
async delete(tableName, query) {
this.ensureOpen();
this.ensureTable(tableName);
const rows = await this.getAllRows(tableName);
let count = 0;
// v0.3.1: 批量 WAL 写入(组提交)
const walRecords = [];
// v0.4.1: 外键级联(环路保护)
const visited = new Set();
for (const row of rows) {
const pkCol = this.tablePKs.get(tableName);
const key = `${tableName}:${row[pkCol]}`;
if (!query.where || Object.keys(query.where).length === 0 || matchWhere(row, query.where)) {
// v0.4.1: 外键规则(RESTRICT 抛错 / CASCADE 递归删 / SET NULL 置空)
count += await this.applyForeignKeyRules(tableName, String(row[pkCol]), walRecords, visited);
if (this.currentTxnId && this.txnSnapshot) {
// Buffer delete in snapshot + MVCC tombstone
this.txnSnapshot.set(key, { __txn_deleted: true });
this.mvcc.deleteVersion(tableName, String(row[pkCol]), this.currentTxnId);
}
else {
this.lsm.delete(key);
}
count++;
walRecords.push({
type: WALRecordType.DELETE,
txnId: this.currentTxnId ?? 0,
tableName,
key: String(row[pkCol]),
});
// 移除二级索引
this.updateSecondaryIndexes(tableName, String(row[pkCol]), null, row);
}
}
await this.wal.appendBatch(walRecords);
this.opCounter += count;
await this.checkpointManager.tick();
this.trimAllCaches();
return count;
}
/**
* v0.4.1: 外键级联规则 — 对齐 MemoryEngine.cascadeDelete 行为。
* 删除 tableName 主键为 pkValue 的行前,检查引用它的所有表:
* - RESTRICT: 存在引用行 → 抛 FOREIGN_KEY_VIOLATION
* - CASCADE: 递归删除引用行(含索引/WAL)
* - SET NULL: 引用行外键列置 null(含索引/WAL)
* @returns 级联影响的行数(CASCADE 删除行数 + SET NULL 更新行数)
*/
async applyForeignKeyRules(tableName, pkValue, walRecords, visited) {
let total = 0;
const visitKey = `${tableName}:${pkValue}`;
if (visited.has(visitKey))
return 0;
visited.add(visitKey);
for (const [refTableName, refSchema] of this.schemas) {
if (refTableName === tableName)
continue;
for (const [colName, colDef] of Object.entries(refSchema.columns)) {
if (!colDef.references || !colDef.onDelete)
continue;
const [refTable] = colDef.references.split('.');
if (refTable !== tableName)
continue;
const refRows = await this.getAllRows(refTableName);
const matched = refRows.filter((r) => String(r[colName]) === pkValue);
if (colDef.onDelete === 'RESTRICT' && matched.length > 0) {
throw new DatabaseError(`Cannot delete from "${tableName}": foreign key "${colName}" in "${refTableName}" has dependent rows`, 'FOREIGN_KEY_VIOLATION');
}
if (colDef.onDelete === 'CASCADE') {
const refPkCol = this.tablePKs.get(refTableName);
for (const refRow of matched) {
const refPk = String(refRow[refPkCol]);
// 递归级联(先处理更深层引用)
total += await this.applyForeignKeyRules(refTableName, refPk, walRecords, visited);
// 删除引用行
const refKey = `${refTableName}:${refPk}`;
if (this.currentTxnId && this.txnSnapshot) {
this.txnSnapshot.set(refKey, { __txn_deleted: true });
this.mvcc.deleteVersion(refTableName, refPk, this.currentTxnId);
}
else {
this.lsm.delete(refKey);
}
this.updateSecondaryIndexes(refTableName, refPk, null, refRow);
walRecords.push({
type: WALRecordType.DELETE,
txnId: this.currentTxnId ?? 0,
tableName: refTableName,
key: refPk,
});
total++;
}
}
else if (colDef.onDelete === 'SET NULL') {
const refPkCol = this.tablePKs.get(refTableName);
for (const refRow of matched) {
const refPk = String(refRow[refPkCol]);
const updated = { ...refRow, [colName]: null };
const refKey = `${refTableName}:${refPk}`;
if (this.currentTxnId && this.txnSnapshot) {
this.txnSnapshot.set(refKey, updated);
this.mvcc.writeVersion(refTableName, refPk, updated, this.currentTxnId);
}
else {
this.lsm.put(refKey, updated);
}
this.updateSecondaryIndexes(refTableName, refPk, updated, refRow);
walRecords.push({
type: WALRecordType.UPDATE,
txnId: this.currentTxnId ?? 0,
tableName: refTableName,
key: refPk,
data: updated,
});
// 对齐 Memory 语义:SET NULL 不影响返回的删除行数
}
}
}
}
return total;
}
/**
* v0.4.0: 流式查询 — 逐行回调,不物化结果数组。
* 全表路径走 LSM rangeScanLazy 惰性扫描;索引等值/范围路径复用 tryIndexLookup。
* 事务中回退物化(快照合并需要全量行集)。
*/
async findStream(tableName, query, onRow) {
this.ensureOpen();
this.ensureTable(tableName);
const hasWhere = !!(query.where && Object.keys(query.where).length > 0);
const project = query.columns && query.columns.length > 0 && query.columns[0] !== '*'
? (row) => projectColumns(row, query.columns)
: null;
const limit = query.limit ?? Infinity;
const offset = query.offset ?? 0;
const pkCol = this.tablePKs.get(tableName);
const prefix = `${tableName}:`;
let count = 0;
let skipped = 0;
const emit = (row) => {
if (hasWhere && !matchWhere(row, query.where))
return true;
if (skipped < offset) {
skipped++;
return true;
}
onRow(project ? project(row) : row);
count++;
return count < limit;
};
if (this.currentTxnId && this.txnSnapshot) {
// 事务中:物化后逐行回调(快照合并需要全量行集)
const rows = await this.find(tableName, { ...query, orderBy: undefined, limit: undefined, offset: undefined });
for (const row of rows) {
onRow(project ? project(row) : row);
}
return rows.length;
}
// 索引路径:等值/范围查找(结果行已过滤,直接回调)
const fastPath = await this.tryIndexLookup(tableName, query);
if (fastPath !== null) {
for (const row of fastPath) {
if (!emit(row))
break;
}
return count;
}
// 全表惰性扫描(含 WHERE 过滤,不物化)
await this.lsm.prefetchRange(prefix, `${prefix}\uffff`);
this.lsm.rangeScanLazy(prefix, `${prefix}\uffff`, (key, value) => {
if (count >= limit)
return;
const row = { ...value };
row[pkCol] = key.slice(prefix.length);
emit(row);
});
return count;
}
async count(tableName, query) {
this.ensureOpen();
this.ensureTable(tableName);
const rows = await this.getAllRows(tableName);
this.trimAllCaches();
if (!query?.where || Object.keys(query.where).length === 0)
return rows.length;
return rows.filter((row) => matchWhere(row, query.where)).length;
}
async clear(tableName) {
this.ensureOpen();
this.ensureTable(tableName);
const rows = await this.getAllRows(tableName);
// v0.3.3: 事务内清空走快照(删除标记),提交时生效;并写入 WAL
const walRecords = [];
for (const row of rows) {
const pkCol = this.tablePKs.get(tableName);
const key = `${tableName}:${row[pkCol]}`;
if (this.currentTxnId && this.txnSnapshot) {
this.txnSnapshot.set(key, { __txn_deleted: true });
this.mvcc.deleteVersion(tableName, String(row[pkCol]), this.currentTxnId);
}
else {
this.lsm.delete(key);
}
walRecords.push({
type: WALRecordType.DELETE,
txnId: this.currentTxnId ?? 0,
tableName,
key: String(row[pkCol]),
});
// 移除二级索引
this.updateSecondaryIndexes(tableName, String(row[pkCol]), null, row);
}
await this.wal.appendBatch(walRecords);
this.opCounter += rows.length;
await this.checkpointManager.tick();
this.tryGC();
}
// ---- ALTER TABLEv0.4.1 ----
/**
* v0.4.1: ALTER TABLE — 结构变更真正生效于存储:
* - ADD: 持久化 schemapersistSchemas),行无需修改
* - DROP: 持久化 schema + 遍历主 LSM 重写所有行(移除该列键)+ WAL UPDATE 记录
* (通用路径 getTableSchema 返回副本,Executor 的引用修改对 Aria 无效)
*/
async alterTable(tableName, action, column) {
this.ensureOpen();
this.ensureNoDDLInTransaction('ALTER TABLE');
this.ensureTable(tableName);
const schema = this.schemas.get(tableName);
if (action === 'ADD') {
if (schema.columns[column.name]) {
throw new DatabaseError(`Column "${column.name}" already exists in table "${tableName}"`, 'COLUMN_EXISTS');
}
schema.columns[column.name] = column;
await this.persistSchemas();
return;
}
// DROP
if (!schema.columns[column.name]) {
throw new DatabaseError(`Column "${column.name}" does not exist in table "${tableName}"`, 'COLUMN_NOT_FOUND');
}
// v0.4.2-fix: 被删列是索引列 → 先清理索引 LSM(残留会导致后续同名列索引脏数据)
if (schema.columns[column.name].index || schema.columns[column.name].unique) {
const idxKey = `${tableName}:idx:${column.name}`;
const idxLsm = this.secondaryIndexes.get(idxKey);
if (idxLsm) {
try {
await idxLsm.clear();
}
catch { /* 清理失败不阻塞 */ }
this.secondaryIndexes.delete(idxKey);
}
}
delete schema.columns[column.name];
await this.persistSchemas();
// 重写主 LSM:移除所有行的该列键(find 副本无法就地删除,必须重写存储)
const prefix = `${tableName}:`;
const endKey = `${prefix}\uffff`;
await this.lsm.prefetchRange(prefix, endKey);
const entries = this.lsm.rangeScan(prefix, endKey);
const walRecords = [];
for (const [key, value] of entries) {
if (!(column.name in value))
continue;
const updated = { ...value };
delete updated[column.name];
this.lsm.put(key, updated);
// 二级索引列被删时同步清理索引
const pk = key.slice(prefix.length);
this.updateSecondaryIndexes(tableName, pk, updated, value);
walRecords.push({
type: WALRecordType.UPDATE,
txnId: this.currentTxnId ?? 0,
tableName,
key: pk,
data: updated,
});
}
await this.wal.appendBatch(walRecords);
this.opCounter += walRecords.length;
await this.checkpointManager.tick();
this.trimAllCaches();
}
async createIndex(tableName, column, unique) {
this.ensureOpen();
this.ensureNoDDLInTransaction('CREATE INDEX');
this.ensureTable(tableName);
const schema = this.schemas.get(tableName);
const colDef = schema.columns[column];
if (!colDef)
throw new DatabaseError(`Column "${column}" does not exist in table "${tableName}"`, 'COLUMN_NOT_FOUND');
const idxKey = `${tableName}:idx:${column}`;
// v0.4.2-fix: 以索引 LSM 是否已建为准(schema 标记可能因重启恢复而存在,
// 但索引 LSM 未恢复 → 此前静默 return 导致索引永久缺失)
if (this.secondaryIndexes.has(idxKey))
return;
colDef.index = true;
if (unique)
colDef.unique = true;
const idxLsm = new LSM({
memtableSizeThreshold: this.config.memtableSizeThreshold,
levelSizeMultiplier: this.config.levelSizeMultiplier,
blockSize: this.config.pageSize,
bloomBitsPerKey: this.config.bloomFilterBitsPerKey,
cacheLimitBytes: this.config.bufferPoolPages * this.config.pageSize,
sstableStore: this.createSSTableStore(`idx_${tableName}_${column}`),
});
await idxLsm.init();
this.secondaryIndexes.set(idxKey, idxLsm);
// 从主 LSM 重建索引数据
const pkCol = this.tablePKs.get(tableName);
const rows = await this.getAllRows(tableName);
for (const row of rows) {
const value = row[column];
if (value !== undefined && value !== null) {
idxLsm.put(`${String(value)}:${row[pkCol]}`, { pk: row[pkCol] });
}
}
await idxLsm.flush();
await this.persistSchemas();
}
async dropIndex(tableName, column, _indexName) {
this.ensureOpen();
this.ensureNoDDLInTransaction('DROP INDEX');
this.ensureTable(tableName);
const schema = this.schemas.get(tableName);
const colDef = schema.columns[column];
if (!colDef)
throw new DatabaseError(`Column "${column}" does not exist in table "${tableName}"`, 'COLUMN_NOT_FOUND');
// 主键索引不可删除(PK 查找依赖主 LSM)
if (colDef.primaryKey) {
throw new DatabaseError(`Cannot drop primary key index on column "${column}"`, 'NOT_SUPPORTED');
}
// v0.4.1: DROP 不存在的索引应报错(此前静默成功)
if (!colDef.index && !colDef.unique && !this.secondaryIndexes.has(`${tableName}:idx:${column}`)) {
throw new DatabaseError(`Index on column "${column}" does not exist in table "${tableName}"`, 'INDEX_NOT_FOUND');
}
colDef.index = false;
colDef.unique = false;
const idxKey = `${tableName}:idx:${column}`;
const idxLsm = this.secondaryIndexes.get(idxKey);
if (idxLsm) {
await idxLsm.clear();
this.secondaryIndexes.delete(idxKey);
}
await this.persistSchemas();
}
// =======================================================================
// 事务
// =======================================================================
async beginTransaction() {
if (this.currentTxnId)
throw new DatabaseError('Transaction already in progress', 'TX_ACTIVE');
this.currentTxnId = this.mvcc.beginTransaction();
this.txnSnapshot = new Map();
await this.wal.append({
type: WALRecordType.BEGIN,
txnId: this.currentTxnId,
tableName: '',
key: '',
});
}
async commitTransaction() {
if (!this.currentTxnId)
throw new DatabaseError('No active transaction', 'TX_NONE');
// v0.4.3-fix: 先持久化 WAL COMMIT,再合并快照到 LSM —
// 崩溃在 WAL 提交后、快照合并前:恢复时 WAL 重放数据,重启一致;
// 崩溃在 WAL 提交前:commitTransaction 尚未返回,事务视为未提交(可回滚)
await this.wal.append({
type: WALRecordType.COMMIT,
txnId: this.currentTxnId,
tableName: '',
key: '',
});
await this.wal.flush();
if (this.txnSnapshot) {
for (const [key, value] of this.txnSnapshot) {
if (value.__txn_deleted) {
this.lsm.delete(key);
}
else {
this.lsm.put(key, value);
}
}
}
this.mvcc.commitTransaction(this.currentTxnId);
this.currentTxnId = null;
this.txnSnapshot = null;
}
async rollbackTransaction() {
if (!this.currentTxnId)
throw new DatabaseError('No active transaction', 'TX_NONE');
// v0.3.3: 记录事务涉及的表(用于回滚后重建索引,消除索引残留)
const affectedTables = new Set();
if (this.txnSnapshot) {
for (const key of this.txnSnapshot.keys()) {
const idx = key.indexOf(':');
if (idx > 0)
affectedTables.add(key.slice(0, idx));
}
}
this.mvcc.rollbackTransaction(this.currentTxnId);
this.txnSnapshot = null;
await this.wal.append({
type: WALRecordType.ROLLBACK,
txnId: this.currentTxnId,
tableName: '',
key: '',
});
this.currentTxnId = null;
// v0.3.3: 事务内直接写入了二级索引 LSM,回滚后全量重建受影响表的索引
for (const tableName of affectedTables) {
if (this.schemas.has(tableName)) {
await this.reindexTable(tableName);
}
}
}
async savepoint(name) {
if (!this.currentTxnId)
throw new DatabaseError('No active transaction for savepoint', 'TX_NONE');
if (this.savepoints.has(name))
throw new DatabaseError(`Savepoint "${name}" already exists`, 'SAVEPOINT_EXISTS');
// 保存当前事务快照
this.savepoints.set(name, {
txnId: this.currentTxnId,
snapshot: this.txnSnapshot ? new Map(this.txnSnapshot) : null,
});
}
async rollbackToSavepoint(name) {
const sp = this.savepoints.get(name);
if (!sp)
throw new DatabaseError(`Savepoint "${name}" not found`, 'SAVEPOINT_NOT_FOUND');
// 恢复到 savepoint 时的快照
this.txnSnapshot = sp.snapshot ? new Map(sp.snapshot) : null;
// v0.3.3: 清理该事务在 MVCC 版本链中的全部记录(快照已含正确数据,
// 版本链仅作 undo 记录,清空后 commit 时 LSM 写入与快照保持一致)
this.mvcc.discardVersions(this.currentTxnId);
// 清除此 savepoint 之后的所有 savepoint
let found = false;
for (const [k] of this.savepoints) {
if (k === name) {
found = true;
continue;
}
if (found)
this.savepoints.delete(k);
}
}
async releaseSavepoint(name) {
if (!this.savepoints.has(name))
throw new DatabaseError(`Savepoint "${name}" not found`, 'SAVEPOINT_NOT_FOUND');
this.savepoints.delete(name);
}
// ---- 在线备份 ----
async backup() {
this.ensureOpen();
const result = {};
for (const tableName of this.schemas.keys()) {
result[tableName] = await this.getAllRows(tableName);
}
return result;
}
// =======================================================================
// 内部
// =======================================================================
async getAllRows(tableName) {
const pkCol = this.tablePKs.get(tableName);
const prefix = `${tableName}:`;
// 预加载范围内涉及的 SSTable,避免 rangeScan 时缓存未命中静默丢数据
await this.lsm.prefetchRange(prefix, `${prefix}\uffff`);
const entries = this.lsm.rangeScan(prefix, `${prefix}\uffff`);
const rows = entries.map(([key, value]) => {
const row = { ...value };
row[pkCol] = key.slice(prefix.length);
return row;
});
// v0.3.3: 事务内合并未提交快照(update/delete/count/clear 也能看到本事务的写入)
return this.mergeTxnSnapshot(tableName, rows);
}
/**
* v0.3.3: 将事务未提交快照的变更合并到行列表(新增/更新/删除标记)。
* 幂等操作:行已是最新时不重复修改。
*/
mergeTxnSnapshot(tableName, rows) {
if (!this.currentTxnId || !this.txnSnapshot)
return rows;
const pkCol = this.tablePKs.get(tableName);
const prefix = `${tableName}:`;
for (const [key, value] of this.txnSnapshot) {
if (!key.startsWith(prefix))
continue;
const pk = key.slice(prefix.length);
const del = value.__txn_deleted;
const idx = rows.findIndex((r) => r[pkCol] === pk);
if (del) {
if (idx >= 0)
rows.splice(idx, 1);
}
else {
const row = { ...value, [pkCol]: pk };
if (idx >= 0)
rows[idx] = row;
else
rows.push(row);
}
}
return rows;
}
getPK(schema) {
for (const [name, col] of Object.entries(schema.columns)) {
if (col.primaryKey)
return name;
}
return Object.keys(schema.columns)[0];
}
validateRow(schema, row) {
const validated = {};
for (const [colName, colDef] of Object.entries(schema.columns)) {
let value = row[colName];
if (value === undefined && colDef.default !== undefined)
value = colDef.default;
if (colDef.required && (value === undefined || value === null)) {
throw new DatabaseError(`Column "${colName}" is required in table "${schema.name}"`, 'VALIDATION_ERROR');
}
if (value !== undefined && value !== null) {
this.checkType(colName, colDef.type, value, colDef);
}
if (value !== undefined)
validated[colName] = value;
}
return validated;
}
checkType(colName, type, value, colDef) {
checkFieldType('', colName, type, value, colDef);
}
// =======================================================================
// Schema 持久化
// =======================================================================
async persistSchemas() {
const data = {};
for (const [name, schema] of this.schemas) {
data[name] = schema.columns;
}
const json = JSON.stringify(data);
const buf = new TextEncoder().encode(json).buffer;
await this.backend.write('__aria_schemas', buf);
}
async loadSchemas() {
const raw = await this.backend.read('__aria_schemas');
if (!raw)
return;
try {
const json = new TextDecoder().decode(raw);
const data = JSON.parse(json);
for (const [tableName, columns] of Object.entries(data)) {
const schema = { name: tableName, columns };
this.schemas.set(tableName, schema);
this.tablePKs.set(tableName, this.getPK(schema));
}
}
catch {
// 忽略损坏的 schema 数据
}
}
// =======================================================================
// SSTableStore 构建
// =======================================================================
/**
* 创建命名空间隔离的 SSTableStore。
*
* 主 LSM 与每个二级索引 LSM 各持有独立实例:
* - 文件 key 前缀隔离(sst_ / sst_idx_${table}_${col}_
* - 元数据 key 隔离(__aria_lsm_meta / __aria_lsm_meta_${ns}
* - id 序列独立(避免 v0.2.4 共享 id 空间导致的文件互相覆盖)
*/
createSSTableStore(ns) {
const filePrefix = ns === 'main' ? 'sst_' : `sst_${ns}_`;
const META_KEY = ns === 'main' ? '__aria_lsm_meta' : `__aria_lsm_meta_${ns}`;
let seq = 0;
let seqLoaded = false;
const encodeText = (text) => {
return new TextEncoder().encode(text).buffer;
};
const readMetaList = async () => {
const raw = await this.backend.read(META_KEY);
if (!raw)
return [];
try {
return JSON.parse(new TextDecoder().decode(raw));
}
catch {
return [];
}
};
return {
save: async (id, data) => {
let buf = data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength);
// 压缩(若启用)
if (this.config.compression) {
const compressed = compressLZ4(new Uint8Array(buf));
buf = compressed.buffer.slice(compressed.byteOffset, compressed.byteOffset + compressed.byteLength);
}
// 加密(若启用)
if (isCryptoEnabled()) {
const enc = await encryptPage(buf);
const header = new Uint8Array(12 + 4); // IV(12) + originalLen(4)
header.set(enc.iv, 0);
new DataView(header.buffer).setUint32(12, data.byteLength, false);
const combined = new Uint8Array(header.length + enc.data.byteLength);
combined.set(header, 0);
combined.set(new Uint8Array(enc.data), header.length);
buf = combined.buffer;
}
await this.backend.write(`${filePrefix}${id}`, buf);
},
load: async (id) => {
const raw = await this.backend.read(`${filePrefix}${id}`);
if (!raw)
return null;
let buf = new Uint8Array(raw);
// 解密(若数据带加密头)
if (isCryptoEnabled() && buf.length > 16) {
const iv = buf.slice(0, 12);
const origLen = new DataView(buf.buffer, buf.byteOffset + 12, 4).getUint32(0, false);
const ciphertext = buf.slice(16).buffer;
const decrypted = await decryptPage(iv, ciphertext);
buf = new Uint8Array(decrypted, 0, origLen);
}
// 解压(若启用)
if (this.config.compression) {
const decompressed = decompressLZ4(buf, buf.length * 2); // 估计原始大小
buf = decompressed;
}
return buf;
},
delete: async (id) => {
await this.backend.delete(`${filePrefix}${id}`);
},
allocateId: async () => {
// 从本命名空间的 meta 恢复 id 序列,保证单调递增且不与其他 LSM 冲突
if (!seqLoaded) {
const metas = await readMetaList();
seq = metas.reduce((m, x) => Math.max(m, x.id), 0);
seqLoaded = true;
}
return ++seq;
},
listMeta: readMetaList,
saveMeta: async (meta) => {
const list = await readMetaList();
// 更新或添加
const idx = list.findIndex((m) => m.id === meta.id);
if (idx >= 0)
list[idx] = meta;
else
list.push(meta);
await this.backend.write(META_KEY, encodeText(JSON.stringify(list)));
},
deleteMeta: async (id) => {
const list = await readMetaList();
const filtered = list.filter((m) => m.id !== id);
await this.backend.write(META_KEY, encodeText(JSON.stringify(filtered)));
},
};
}
// =======================================================================
// WAL 恢复
// =======================================================================
applyWALRecord(record) {
switch (record.type) {
case WALRecordType.INSERT:
case WALRecordType.UPDATE:
if (record.data) {
this.lsm.put(`${record.tableName}:${record.key}`, record.data);
}
break;
case WALRecordType.DELETE:
this.lsm.delete(`${record.tableName}:${record.key}`);
break;
case WALRecordType.CREATE_TABLE:
if (record.data?.schema) {
try {
const s = JSON.parse(record.data.schema);
if (!this.schemas.has(s.name)) {
this.schemas.set(s.name, s);
this.tablePKs.set(s.name, this.getPK(s));
}
}
catch { /* skip */ }
}
break;
case WALRecordType.COMMIT:
case WALRecordType.ROLLBACK:
case WALRecordType.BEGIN:
break;
}
}
/**
* v0.3.3: DROP_TABLE 恢复 — 删除 schema 并清除主 LSM 中该表的所有残留数据。
*
* 此前 DROP_TABLE 在恢复时被忽略,而 CREATE_TABLE 回放会重建 schema
* 导致崩溃后"已删除的表和数据复活"(实证 P0 bug)。
*/
async applyDropTableRecovery(tableName) {
if (!tableName)
return;
// v0.4.2-fix: 清理该表二级索引(崩溃恢复路径同样不留孤儿索引)
await this.cleanupTableIndexes(tableName);
this.schemas.delete(tableName);
this.tablePKs.delete(tableName);
// 清除主 LSM 中该表前缀的所有数据(含 SSTable 中的旧数据)
const prefix = `${tableName}:`;
const endKey = `${prefix}\uffff`;
await this.lsm.prefetchRange(prefix, endKey);
const entries = this.lsm.rangeScan(prefix, endKey);
for (const [key] of entries) {
this.lsm.delete(key);
}
}
// =======================================================================
// 二级索引
// =======================================================================
/** 更新行的二级索引条目 */
updateSecondaryIndexes(tableName, pkValue, newRow, oldRow) {
const schema = this.schemas.get(tableName);
if (!schema)
return;
for (const [colName, colDef] of Object.entries(schema.columns)) {
// v0.3.3: 主键列不建冗余二级索引(主 LSM 即 PK 索引)
if (!colDef.index && !colDef.unique)
continue;
const idxKey = `${tableName}:idx:${colName}`;
const idxLsm = this.secondaryIndexes.get(idxKey);
if (!idxLsm)
continue;
// 删除旧值
if (oldRow) {
const oldVal = oldRow[colName];
if (oldVal !== undefined && oldVal !== null) {
idxLsm.delete(`${String(oldVal)}:${pkValue}`);
}
}
// 插入新值
if (newRow) {
const newVal = newRow[colName];
if (newVal !== undefined && newVal !== null) {
idxLsm.put(`${String(newVal)}:${pkValue}`, { pk: pkValue });
}
}
}
}
/** 通过二级索引快速查找 */
async tryIndexLookup(tableName, query) {
if (!query.where)
return null;
const schema = this.schemas.get(tableName);
if (!schema)
return null;
const pkCol = this.tablePKs.get(tableName);
for (const [col, condition] of Object.entries(query.where)) {
// 跳过 $and/$or/$not 逻辑组合
if (col === '$and' || col === '$or' || col === '$not')
continue;
const colDef = schema.columns[col];
const hasIndex = colDef && (colDef.index || colDef.unique || colDef.primaryKey);
if (!hasIndex && col !== pkCol)
continue;
// PK 等值 → 主 LSM 精确查找
if (col === pkCol) {
if (typeof condition !== 'object' || condition === null) {
const key = `${tableName}:${condition}`;
await this.lsm.prefetchKeys([key]);
const value = this.lsm.get(key);
return value ? [{ ...value, [pkCol]: condition }] : [];
}
const cond = condition;
if ('$eq' in cond) {
const key = `${tableName}:${cond.$eq}`;
await this.lsm.prefetchKeys([key]);
const value = this.lsm.get(key);
return value ? [{ ...value, [pkCol]: cond.$eq }] : [];
}
// v0.3.3: PK $in → 主 LSM 多次精确查找(替代冗余 PK 二级索引)
if ('$in' in cond && Array.isArray(cond.$in)) {
const keys = cond.$in.map((v) => `${tableName}:${v}`);
await this.lsm.prefetchKeys(keys);
const rows = [];
const seen = new Set(); // v0.4.1: IN 子查询可能含重复值,按 pk 去重
for (const v of cond.$in) {
const pk = String(v);
if (seen.has(pk))
continue;
const value = this.lsm.get(`${tableName}:${pk}`);
if (value) {
seen.add(pk);
rows.push({ ...value, [pkCol]: pk });
}
}
return rows;
}
// v0.3.3: PK 范围查询 → 主 LSM 前缀扫描 + 条件过滤(修复字符串算术 bug)
if ('$gt' in cond || '$gte' in cond || '$lt' in cond || '$lte' in cond) {
const prefix = `${tableName}:`;
await this.lsm.prefetchRange(prefix, `${prefix}\uffff`);
const entries = this.lsm.rangeScan(prefix, `${prefix}\uffff`);
const rows = [];
for (const [key, value] of entries) {
const candidate = { ...value, [pkCol]: key.slice(prefix.length) };
if (matchWhere(candidate, { [pkCol]: condition }))
rows.push(candidate);
}
return rows;
}
}
// 二级索引查找
const idxKey = `${tableName}:idx:${col}`;
const idxLsm = this.secondaryIndexes.get(idxKey);
if (!idxLsm)
continue;
// $eq → 精确查找
if (typeof condition !== 'object' || condition === null) {
return this.indexScanToRows(tableName, pkCol, idxLsm, String(condition), String(condition));
}
const c = condition;
if ('$eq' in c) {
const v = String(c.$eq);
return this.indexScanToRows(tableName, pkCol, idxLsm, v, v);
}
// $in → 多次精确查找
if ('$in' in c && Array.isArray(c.$in)) {
const results = [];
const seenPks = new Set(); // v0.4.1: IN 值可能重复,按 pk 去重
for (const val of c.$in) {
const rows = await this.indexScanToRows(tableName, pkCol, idxLsm, String(val), String(val));
for (const row of rows) {
const pk = String(row[pkCol]);
if (!seenPks.has(pk)) {
seenPks.add(pk);
results.push(row);
}
}
}
return results;
}
// $gt / $gte / $lt / $lte → 范围扫描
if ('$gt' in c || '$gte' in c || '$lt' in c || '$lte' in c) {
let startKey = '';
let endKey = '\uffff';
if (c.$gt !== undefined)
startKey = `${String(Number(c.$gt) + 1)}:`;
else if (c.$gte !== undefined)
startKey = `${String(c.$gte)}:`;
if (c.$lt !== undefined)
endKey = `${String(Number(c.$lt) - 1)}:\uffff`;
else if (c.$lte !== undefined)
endKey = `${String(c.$lte)}:\uffff`;
return this.indexScanToRows(tableName, pkCol, idxLsm, startKey, endKey);
}
}
return null;
}
/** 从索引扫描结果恢复完整行 */
async indexScanToRows(tableName, pkCol, idxLsm, startKey, endKey) {
// 使用前缀扫描:endKey 需要包含 \uffff 以匹配所有带后缀的 key
const actualEndKey = endKey.includes('\uffff') ? endKey : `${endKey}\uffff`;
// 预加载索引 LSM 与主 LSM 涉及的 SSTable
await idxLsm.prefetchRange(startKey, actualEndKey);
const entries = idxLsm.rangeScan(startKey, actualEndKey);
const pks = [];
for (const [, idxEntry] of entries) {
const pk = idxEntry.pk;
if (pk)
pks.push(pk);
}
await this.lsm.prefetchKeys(pks.map((pk) => `${tableName}:${pk}`));
const rows = [];
for (const pk of pks) {
const row = this.lsm.get(`${tableName}:${pk}`);
if (row)
rows.push({ ...row, [pkCol]: pk });
}
return rows;
}
// =======================================================================
// 辅助
// =======================================================================
/** 每 10 次 gc 计数器触发一次 MVCC 垃圾回收 */
tryGC() {
this.gcCounter++;
if (this.gcCounter >= 10) {
this.mvcc.gc(100);
this.gcCounter = 0;
}
}
/** 回收主 LSM 与所有二级索引 LSM 的临时缓存超限 */
trimAllCaches() {
this.lsm.trimCache();
for (const idxLsm of this.secondaryIndexes.values()) {
idxLsm.trimCache();
}
}
/** 检查内存预算,超出时强制 flush + GC */
checkMemoryBudget() {
const maxBytes = this.config.maxMemoryMB * 1024 * 1024;
const used = this.lsm.getEstimatedMemory();
if (used > maxBytes) {
this.lsm.flush().catch(() => { });
this.mvcc.gc(50);
}
}
/** 估算 WAL 大小(字节) */
getWALEstimatedSize() {
return this.wal.getBufferedCount() * 200; // 粗略估算每条 ~200B
}
/**
* ANALYZE: 收集表统计信息
* 返回行数、平均行大小、索引深度等
*/
async analyzeTable(tableName) {
this.ensureOpen();
this.ensureTable(tableName);
const rows = await this.getAllRows(tableName);
const stats = {
table: tableName,
rowCount: rows.length,
avgRowSize: rows.length > 0
? Math.round(rows.reduce((s, r) => s + JSON.stringify(r).length, 0) / rows.length)
: 0,
indexDepth: this.lsm.getStats().levelCounts.filter((c) => c > 0).length,
sstableCount: this.lsm.getStats().sstableCount,
memtableSize: this.lsm.getStats().memtableSize,
estimatedMemory: this.lsm.getEstimatedMemory(),
};
// 列基数统计
const schema = this.schemas.get(tableName);
if (schema && rows.length > 0) {
const columnStats = {};
for (const colName of Object.keys(schema.columns)) {
const values = new Set(rows.map((r) => String(r[colName])));
columnStats[colName] = { distinctValues: values.size };
}
stats.columnStats = columnStats;
}
return stats;
}
/**
* REINDEX: 重建指定表的所有二级索引
*/
async reindexTable(tableName) {
this.ensureOpen();
this.ensureTable(tableName);
return this.reindexTableInternal(tableName);
}
/** v0.4.2-fix: 重建索引内部实现(不校验 opened,供 open 恢复流程调用) */
async reindexTableInternal(tableName) {
const schema = this.schemas.get(tableName);
if (!schema)
return 0;
let rebuiltCount = 0;
for (const [colName, colDef] of Object.entries(schema.columns)) {
// v0.3.3: 主键列不建冗余二级索引(主 LSM 即 PK 索引)
if (!colDef.index && !colDef.unique)
continue;
const idxKey = `${tableName}:idx:${colName}`;
const idxLsm = this.secondaryIndexes.get(idxKey);
if (!idxLsm)
continue;
// 清空旧索引
await idxLsm.clear();
rebuiltCount++;
// 从主 LSM 重建索引
const rows = await this.getAllRows(tableName);
for (const row of rows) {
const val = row[colName];
if (val !== undefined && val !== null) {
idxLsm.put(`${String(val)}:${row[this.tablePKs.get(tableName)]}`, { pk: row[this.tablePKs.get(tableName)] });
}
}
}
return rebuiltCount;
}
/**
* VACUUM: 压缩 LSM + 清理碎片
*/
async vacuum() {
this.ensureOpen();
// 强制 flush memtable
await this.lsm.flush();
// 压缩各层级
for (let level = 0; level < 6; level++) {
if (this.lsm.getStats().levelCounts[level] >= 2) {
await this.lsm.compactLevel(level);
}
}
// GC MVCC 版本(保留最新 10 个)
const beforeGC = this.mvcc.getGlobalLSN();
this.mvcc.gc(10);
return { compactedLevels: 6, gcVersions: beforeGC };
}
/**
* 查询优化器:估算各索引成本,选择最优方案
*/
estimateQueryCost(tableName, query) {
const schema = this.schemas.get(tableName);
if (!schema || !query.where)
return { strategy: 'full_scan', estimatedRows: 0 };
const pkCol = this.tablePKs.get(tableName);
for (const [col, condition] of Object.entries(query.where)) {
if (col === '$and' || col === '$or' || col === '$not')
continue;
// PK 等值 → 最快,估计 1 行
if (col === pkCol && (typeof condition !== 'object' || condition.$eq)) {
return { strategy: 'pk_lookup', estimatedRows: 1 };
}
// 索引列等值 → 快
const colDef = schema.columns[col];
if (colDef?.index || colDef?.unique) {
if (typeof condition !== 'object' || condition.$eq) {
return { strategy: `index_eq:${col}`, estimatedRows: 1 };
}
if (condition.$in && Array.isArray(condition.$in)) {
return { strategy: `index_in:${col}`, estimatedRows: condition.$in.length };
}
if (condition.$gt || condition.$lt || condition.$gte || condition.$lte) {
return { strategy: `index_range:${col}`, estimatedRows: 100 };
}
}
}
return { strategy: 'full_scan', estimatedRows: 1000 };
}
ensureOpen() {
if (!this.opened)
throw new DatabaseError('AriaEngine not opened', 'DB_NOT_OPEN');
}
/** v0.4.2-fix: Aria 事务中 DDL 显式拒绝(结构变更无法通过行快照回滚) */
ensureNoDDLInTransaction(op) {
if (this.currentTxnId) {
throw new DatabaseError(`${op} is not supported inside a transaction (AriaEngine DDL is not transactional)`, 'NOT_SUPPORTED');
}
}
ensureTable(tableName) {
if (!this.schemas.has(tableName)) {
throw new DatabaseError(`Table "${tableName}" does not exist`, 'TABLE_NOT_FOUND');
}
}
/** Get the number of WAL records stored */
async getWALCount() {
const raw = await this.backend.read('__wal_count');
if (!raw)
return 0;
try {
const dec = new TextDecoder();
return parseInt(dec.decode(raw), 10) || 0;
}
catch {
return 0;
}
}
/** Set the number of WAL records stored */
async setWALCount(count) {
const enc = new TextEncoder();
const buf = enc.encode(String(count)).buffer;
await this.backend.write('__wal_count', buf);
}
}
/**
* metona-sqlark Hybrid Engine — 内存 + 磁盘混合存储引擎
* @module hybrid/index
*
* 采用 write-through 策略:
* - 所有写操作同时写入内存和磁盘
* - 所有读操作直接从内存返回
* - 数据库打开时从磁盘加载数据到内存
*/
// ---------------------------------------------------------------------------
// HybridEngine
// ---------------------------------------------------------------------------
class HybridEngine {
constructor(diskEngine = 'indexeddb') {
this.name = 'hybrid';
this.dbName = '';
this.version = 1;
this.memoryEngine = new MemoryEngine();
this.diskEngineType = diskEngine;
this.diskEngine = diskEngine === 'opfs' ? new OPFSEngine() : new IndexedDBEngine();
}
// ---- 生命周期 ----
async open(dbName, version) {
this.dbName = dbName;
this.version = version;
// 先打开磁盘引擎
await this.diskEngine.open(dbName, version);
// 再打开内存引擎
await this.memoryEngine.open(dbName, version);
// 从磁盘加载现存表
await this.reloadMemoryFromDisk();
}
/**
* 从磁盘重载内存缓存(v0.3.2:多标签页同步)。
* 其他标签页写入磁盘后调用,使本标签页读到最新数据。
*/
async reloadMemoryFromDisk() {
await this.memoryEngine.close();
await this.memoryEngine.open(this.dbName, this.version);
const tableNames = await this.diskEngine.getTableNames();
for (const tableName of tableNames) {
const schema = await this.diskEngine.getTableSchema(tableName);
if (!schema)
continue;
// 在内存中创建表
await this.memoryEngine.createTable(schema);
// 从磁盘加载数据到内存
const rows = await this.diskEngine.find(tableName, { table: tableName });
if (rows.length > 0) {
try {
await this.memoryEngine.insert(tableName, rows);
}
catch (e) {
// eslint-disable-next-line no-console
console.warn(`[metona-sqlark] Failed to load table "${tableName}" data from disk:`, e);
}
}
}
}
async close() {
await this.memoryEngine.close();
await this.diskEngine.close();
}
isOpen() {
return this.memoryEngine.isOpen() && this.diskEngine.isOpen();
}
// ---- v0.4.2-fix: 自愈 / 重置 / 元数据(委托双引擎) ----
/** 自愈:修复磁盘引擎后重载内存缓存 */
async repair() {
if (typeof this.diskEngine.repair === 'function') {
await this.diskEngine.repair();
}
await this.reloadMemoryFromDisk();
}
/** 清空全部数据与表结构 */
async clearAll() {
if (typeof this.diskEngine.clearAll === 'function') {
await this.diskEngine.clearAll();
}
else {
const names = await this.diskEngine.getTableNames();
for (const name of names) {
await this.diskEngine.dropTable(name);
}
}
if (typeof this.memoryEngine.clearAll === 'function') {
await this.memoryEngine.clearAll();
}
else {
const names = await this.memoryEngine.getTableNames();
for (const name of names) {
await this.memoryEngine.dropTable(name);
}
}
}
async getMeta(key) {
if (typeof this.diskEngine.getMeta === 'function') {
return this.diskEngine.getMeta(key);
}
return null;
}
async setMeta(key, value) {
if (typeof this.diskEngine.setMeta === 'function') {
await this.diskEngine.setMeta(key, value);
}
}
// ---- 表管理 ----
async createTable(schema) {
await this.memoryEngine.createTable(schema);
await this.diskEngine.createTable(schema);
}
async dropTable(tableName) {
await this.memoryEngine.dropTable(tableName);
await this.diskEngine.dropTable(tableName);
}
async hasTable(tableName) {
return this.memoryEngine.hasTable(tableName);
}
async getTableNames() {
return this.memoryEngine.getTableNames();
}
async getTableSchema(tableName) {
return this.memoryEngine.getTableSchema(tableName);
}
/** v0.4.2-fix: 引擎级 ALTER TABLE — 双引擎同步(磁盘持久化 + 内存引用) */
async alterTable(tableName, action, column) {
await this.memoryEngine.alterTable(tableName, action, column);
if (typeof this.diskEngine.alterTable === 'function') {
await this.diskEngine.alterTable(tableName, action, column);
}
else {
// 磁盘引擎无引擎级实现 → 从磁盘重建内存 schema(disk 引擎 schema 以自身为准)
const schema = await this.diskEngine.getTableSchema(tableName);
if (schema && action === 'DROP')
delete schema.columns[column.name];
}
}
// ---- CRUDwrite-through 策略) ----
async insert(tableName, rows) {
const pks = await this.memoryEngine.insert(tableName, rows);
// write-through: 同步写入磁盘
await this.diskEngine.insert(tableName, rows);
return pks;
}
async find(tableName, query) {
// 直接从内存读取
return this.memoryEngine.find(tableName, query);
}
/** v0.4.0: 流式查询(内存引擎逐行回调) */
async findStream(tableName, query, onRow) {
return this.memoryEngine.findStream(tableName, query, onRow);
}
async update(tableName, query, updates) {
const count = await this.memoryEngine.update(tableName, query, updates);
// write-through: 同步更新磁盘
await this.diskEngine.update(tableName, query, updates);
return count;
}
async delete(tableName, query) {
const count = await this.memoryEngine.delete(tableName, query);
// write-through: 同步删除磁盘
await this.diskEngine.delete(tableName, query);
return count;
}
async count(tableName, query) {
return this.memoryEngine.count(tableName, query);
}
async clear(tableName) {
await this.memoryEngine.clear(tableName);
await this.diskEngine.clear(tableName);
}
// ---- 动态索引(v0.3.0 ----
async createIndex(tableName, column, unique) {
await this.memoryEngine.createIndex(tableName, column, unique);
if (typeof this.diskEngine.createIndex === 'function') {
await this.diskEngine.createIndex(tableName, column, unique);
}
}
async dropIndex(tableName, column, indexName) {
await this.memoryEngine.dropIndex(tableName, column, indexName);
if (typeof this.diskEngine.dropIndex === 'function') {
await this.diskEngine.dropIndex(tableName, column, indexName);
}
}
// ---- 事务 ----
async beginTransaction() {
await this.memoryEngine.beginTransaction();
await this.diskEngine.beginTransaction();
}
async commitTransaction() {
// 先写磁盘,保证持久化优先;磁盘失败则回滚内存
await this.diskEngine.commitTransaction();
try {
await this.memoryEngine.commitTransaction();
}
catch (error) {
// v0.4.2-fix: 磁盘已提交无法回滚(此前调 diskEngine.rollbackTransaction()
// 会抛 TX_NONE 掩盖原错误)。如实上报内存提交失败,磁盘数据保持已提交状态。
throw new DatabaseError('Hybrid commit failed: memory engine error after disk commit (disk data is committed)', 'TX_COMMIT_ERROR', error);
}
}
async rollbackTransaction() {
await this.memoryEngine.rollbackTransaction();
await this.diskEngine.rollbackTransaction();
}
// ---- 引擎信息 ----
/** 获取磁盘引擎类型 */
getDiskEngineType() {
return this.diskEngineType;
}
/** 获取内存引擎(供内部使用) */
getMemoryEngine() {
return this.memoryEngine;
}
}
/**
* metona-sqlark Query Builder — 链式查询构建器
* @module query/builder
*
* 链式调用 → 构建 AST → 执行引擎操作。
* 支持 JOIN(需要 Executor)。
*/
// ---------------------------------------------------------------------------
// SelectQueryBuilder
// ---------------------------------------------------------------------------
class SelectQueryBuilder {
constructor(engine, tableName, _columns = ['*'], executor) {
this.engine = engine;
this.tableName = tableName;
this._columns = _columns;
this._where = {};
this._orderBy = [];
this._joins = [];
this._executor = executor;
}
/** 主表别名 */
as(alias) {
this._alias = alias;
return this;
}
/** INNER JOIN */
innerJoin(table, on, alias) {
return this._addJoin('INNER', table, on, alias);
}
/** LEFT JOIN */
leftJoin(table, on, alias) {
return this._addJoin('LEFT', table, on, alias);
}
/** RIGHT JOIN */
rightJoin(table, on, alias) {
return this._addJoin('RIGHT', table, on, alias);
}
/** CROSS JOIN */
crossJoin(table, alias) {
return this._addJoin('CROSS', table, {}, alias);
}
/** 通用 JOIN */
join(table, on, alias) {
return this._addJoin('INNER', table, on, alias);
}
_addJoin(type, table, on, alias) {
this._joins.push({ type, table, on, alias });
return this;
}
/** 添加过滤条件 */
where(condition) {
this._where = { ...this._where, ...condition };
return this;
}
/** 排序 */
orderBy(column, direction = 'asc') {
this._orderBy.push({ column, direction });
return this;
}
/** 限制返回条数 */
limit(n) {
this._limit = n;
return this;
}
/** 偏移量 */
offset(n) {
this._offset = n;
return this;
}
/** 执行查询 */
async execute() {
// 有 JOIN → 通过 Executor 执行
if (this._joins.length > 0 && this._executor) {
const ast = this.toAST();
return this._executor.execute(ast);
}
// 无 JOIN → 直接调用引擎
return this.engine.find(this.tableName, {
table: this.tableName,
columns: this._columns,
where: this._where,
orderBy: this._orderBy.length > 0 ? this._orderBy : undefined,
limit: this._limit,
offset: this._offset,
});
}
/** 获取 AST */
toAST() {
return {
type: 'SELECT',
columns: this._columns,
from: this.tableName,
alias: this._alias,
joins: this._joins.length > 0 ? [...this._joins] : undefined,
where: this._where,
orderBy: this._orderBy.length > 0 ? this._orderBy : undefined,
limit: this._limit,
offset: this._offset,
};
}
}
// ---------------------------------------------------------------------------
// UpdateQueryBuilder
// ---------------------------------------------------------------------------
class UpdateQueryBuilder {
constructor(engine, tableName, _updates, onWrite) {
this.engine = engine;
this.tableName = tableName;
this._updates = _updates;
this._where = {};
this.onWrite = onWrite;
}
where(condition) {
this._where = { ...this._where, ...condition };
return this;
}
async execute() {
const count = await this.engine.update(this.tableName, { table: this.tableName, where: this._where }, this._updates);
this.onWrite?.(this.tableName);
return count;
}
toAST() {
return { type: 'UPDATE', table: this.tableName, sets: this._updates, where: this._where };
}
}
// ---------------------------------------------------------------------------
// DeleteQueryBuilder
// ---------------------------------------------------------------------------
class DeleteQueryBuilder {
constructor(engine, tableName, onWrite) {
this.engine = engine;
this.tableName = tableName;
this._where = {};
this.onWrite = onWrite;
}
where(condition) {
this._where = { ...this._where, ...condition };
return this;
}
async execute() {
const count = await this.engine.delete(this.tableName, { table: this.tableName, where: this._where });
this.onWrite?.(this.tableName);
return count;
}
toAST() {
return { type: 'DELETE', from: this.tableName, where: this._where };
}
}
/**
* metona-sqlark Table — 表操作 API
* @module table/table
*/
// ---------------------------------------------------------------------------
// Table
// ---------------------------------------------------------------------------
class Table {
constructor(engine, tableName, executor, onWrite) {
this.schema = null;
this.engine = engine;
this.name = tableName;
this.executor = executor;
this.onWrite = onWrite;
}
// ---- Schema ----
async getSchema() {
if (!this.schema) {
const s = await this.engine.getTableSchema(this.name);
if (!s)
throw new DatabaseError(`Table "${this.name}" does not exist`, 'TABLE_NOT_FOUND');
this.schema = s;
}
return this.schema;
}
// ---- 插入 ----
async insert(row) {
const pks = await this.engine.insert(this.name, [row]);
this.onWrite?.(this.name);
return pks[0];
}
async insertMany(rows) {
const pks = await this.engine.insert(this.name, rows);
this.onWrite?.(this.name);
return pks;
}
// ---- 查询 ----
select(columns = ['*']) {
return new SelectQueryBuilder(this.engine, this.name, columns, this.executor);
}
/** v0.4.0: 流式查询 — 逐行回调,不物化全部结果 */
async stream(onRow, query = {}) {
if (typeof this.engine.findStream !== 'function') {
const rows = await this.engine.find(this.name, {
table: this.name,
where: query.where,
limit: query.limit,
offset: query.offset,
columns: query.columns,
});
for (const row of rows)
onRow(row);
return rows.length;
}
return this.engine.findStream(this.name, {
table: this.name,
where: query.where,
limit: query.limit,
offset: query.offset,
columns: query.columns,
}, onRow);
}
// ---- 更新 ----
update(updates) {
return new UpdateQueryBuilder(this.engine, this.name, updates, this.onWrite);
}
// ---- 删除 ----
delete() {
return new DeleteQueryBuilder(this.engine, this.name, this.onWrite);
}
// ---- 聚合 ----
async count(where) {
return this.engine.count(this.name, where ? { table: this.name, where } : undefined);
}
// ---- 管理 ----
async clear() {
await this.engine.clear(this.name);
this.onWrite?.(this.name);
}
async drop() {
await this.engine.dropTable(this.name);
this.onWrite?.(this.name);
}
}
/**
* metona-sqlark Query Compiler — AST → 查询计划
* @module query/compiler
*
* 将 AST 语句编译为引擎可执行的 QueryPlan。
* v0.0.1: 简单直接映射,未来可加入索引选择、过滤下推等优化。
*/
// ---------------------------------------------------------------------------
// 编译 AST → QueryPlan
// ---------------------------------------------------------------------------
/**
* 编译 SELECT / DELETE / UPDATE 语句为 QueryPlan。
* INSERT 和 DDL 语句不需要 QueryPlan。
*/
function compileStatement(stmt) {
switch (stmt.type) {
case 'SELECT':
return compileSelect(stmt);
case 'DELETE':
return compileDelete(stmt);
case 'UPDATE':
return compileUpdate(stmt);
default:
throw new DatabaseError(`Cannot compile statement type "${stmt.type}" to QueryPlan`, 'COMPILE_ERROR');
}
}
function compileSelect(stmt) {
return {
table: stmt.from,
columns: stmt.columns,
where: stmt.where,
orderBy: stmt.orderBy?.length ? stmt.orderBy : undefined,
limit: stmt.limit,
offset: stmt.offset,
};
}
function compileDelete(stmt) {
return {
table: stmt.from,
where: stmt.where,
};
}
function compileUpdate(stmt) {
return {
table: stmt.table,
where: stmt.where,
};
}
/**
* metona-sqlark SQL Token Types — 词法单元定义
* @module sql/tokens
*/
// ---------------------------------------------------------------------------
// Token 类型枚举
// ---------------------------------------------------------------------------
var TokenType;
(function (TokenType) {
// 关键字
TokenType["SELECT"] = "SELECT";
TokenType["FROM"] = "FROM";
TokenType["WHERE"] = "WHERE";
TokenType["INSERT"] = "INSERT";
TokenType["INTO"] = "INTO";
TokenType["VALUES"] = "VALUES";
TokenType["UPDATE"] = "UPDATE";
TokenType["SET"] = "SET";
TokenType["DELETE"] = "DELETE";
TokenType["CREATE"] = "CREATE";
TokenType["TABLE"] = "TABLE";
TokenType["DROP"] = "DROP";
TokenType["ORDER"] = "ORDER";
TokenType["BY"] = "BY";
TokenType["ASC"] = "ASC";
TokenType["DESC"] = "DESC";
TokenType["LIMIT"] = "LIMIT";
TokenType["OFFSET"] = "OFFSET";
TokenType["AND"] = "AND";
TokenType["OR"] = "OR";
TokenType["NOT"] = "NOT";
TokenType["LIKE"] = "LIKE";
TokenType["IN"] = "IN";
TokenType["PRIMARY"] = "PRIMARY";
TokenType["KEY"] = "KEY";
TokenType["UNIQUE"] = "UNIQUE";
TokenType["DEFAULT"] = "DEFAULT";
TokenType["NULL"] = "NULL";
TokenType["TRUE"] = "TRUE";
TokenType["REFERENCES"] = "REFERENCES";
TokenType["CASCADE"] = "CASCADE";
TokenType["BETWEEN"] = "BETWEEN";
TokenType["IF"] = "IF";
TokenType["EXISTS"] = "EXISTS";
TokenType["FALSE"] = "FALSE";
TokenType["ALTER"] = "ALTER";
TokenType["ADD"] = "ADD";
TokenType["TRUNCATE"] = "TRUNCATE";
// JOIN 相关
TokenType["INNER"] = "INNER";
TokenType["LEFT"] = "LEFT";
TokenType["RIGHT"] = "RIGHT";
TokenType["CROSS"] = "CROSS";
TokenType["JOIN"] = "JOIN";
TokenType["ON"] = "ON";
TokenType["AS"] = "AS";
TokenType["OUTER"] = "OUTER";
// 聚合
TokenType["GROUP"] = "GROUP";
TokenType["HAVING"] = "HAVING";
TokenType["COUNT"] = "COUNT";
TokenType["SUM"] = "SUM";
TokenType["AVG"] = "AVG";
TokenType["MIN"] = "MIN";
TokenType["MAX"] = "MAX";
TokenType["DISTINCT"] = "DISTINCT";
// v0.3.0: 事务 / UNION / EXISTS / 动态索引
TokenType["BEGIN"] = "BEGIN";
TokenType["COMMIT"] = "COMMIT";
TokenType["ROLLBACK"] = "ROLLBACK";
TokenType["UNION"] = "UNION";
TokenType["ALL"] = "ALL";
TokenType["INDEX"] = "INDEX";
// v0.3.1: CASE WHEN 表达式
TokenType["CASE"] = "CASE";
TokenType["WHEN"] = "WHEN";
TokenType["THEN"] = "THEN";
TokenType["ELSE"] = "ELSE";
TokenType["END"] = "END";
// 标识符 & 字面量
TokenType["IDENTIFIER"] = "IDENTIFIER";
TokenType["STRING"] = "STRING";
TokenType["NUMBER"] = "NUMBER";
// 运算符 & 分隔符
TokenType["COMMA"] = "COMMA";
TokenType["LPAREN"] = "LPAREN";
TokenType["RPAREN"] = "RPAREN";
TokenType["SEMICOLON"] = "SEMICOLON";
TokenType["EQ"] = "EQ";
TokenType["NEQ"] = "NEQ";
TokenType["GT"] = "GT";
TokenType["GTE"] = "GTE";
TokenType["LT"] = "LT";
TokenType["LTE"] = "LTE";
TokenType["STAR"] = "STAR";
TokenType["DOT"] = "DOT";
// 特殊
TokenType["EOF"] = "EOF";
TokenType["ILLEGAL"] = "ILLEGAL";
})(TokenType || (TokenType = {}));
// ---------------------------------------------------------------------------
// 关键字映射
// ---------------------------------------------------------------------------
const KEYWORDS = {
'SELECT': TokenType.SELECT,
'FROM': TokenType.FROM,
'WHERE': TokenType.WHERE,
'INSERT': TokenType.INSERT,
'INTO': TokenType.INTO,
'VALUES': TokenType.VALUES,
'UPDATE': TokenType.UPDATE,
'SET': TokenType.SET,
'DELETE': TokenType.DELETE,
'CREATE': TokenType.CREATE,
'TABLE': TokenType.TABLE,
'DROP': TokenType.DROP,
'ORDER': TokenType.ORDER,
'BY': TokenType.BY,
'ASC': TokenType.ASC,
'DESC': TokenType.DESC,
'LIMIT': TokenType.LIMIT,
'OFFSET': TokenType.OFFSET,
'AND': TokenType.AND,
'OR': TokenType.OR,
'NOT': TokenType.NOT,
'LIKE': TokenType.LIKE,
'IN': TokenType.IN,
'PRIMARY': TokenType.PRIMARY,
'KEY': TokenType.KEY,
'UNIQUE': TokenType.UNIQUE,
'DEFAULT': TokenType.DEFAULT,
'NULL': TokenType.NULL,
'TRUE': TokenType.TRUE,
'FALSE': TokenType.FALSE,
'REFERENCES': TokenType.REFERENCES,
'CASCADE': TokenType.CASCADE,
'BETWEEN': TokenType.BETWEEN,
'IF': TokenType.IF,
'EXISTS': TokenType.EXISTS,
'ALTER': TokenType.ALTER,
'ADD': TokenType.ADD,
'TRUNCATE': TokenType.TRUNCATE,
// JOIN
'INNER': TokenType.INNER,
'LEFT': TokenType.LEFT,
'RIGHT': TokenType.RIGHT,
'CROSS': TokenType.CROSS,
'JOIN': TokenType.JOIN,
'ON': TokenType.ON,
'AS': TokenType.AS,
'OUTER': TokenType.OUTER,
// 聚合
'GROUP': TokenType.GROUP,
'HAVING': TokenType.HAVING,
'COUNT': TokenType.COUNT,
'SUM': TokenType.SUM,
'AVG': TokenType.AVG,
'MIN': TokenType.MIN,
'MAX': TokenType.MAX,
'DISTINCT': TokenType.DISTINCT,
// v0.3.0
'BEGIN': TokenType.BEGIN,
'COMMIT': TokenType.COMMIT,
'ROLLBACK': TokenType.ROLLBACK,
'UNION': TokenType.UNION,
'ALL': TokenType.ALL,
'INDEX': TokenType.INDEX,
// v0.3.1
'CASE': TokenType.CASE,
'WHEN': TokenType.WHEN,
'THEN': TokenType.THEN,
'ELSE': TokenType.ELSE,
'END': TokenType.END,
};
/**
* metona-sqlark SQL Lexer — 词法分析器
* @module sql/lexer
*
* 将 SQL 字符串切分为 Token 流。
*/
// ---------------------------------------------------------------------------
// Lexer
// ---------------------------------------------------------------------------
class Lexer {
constructor(input) {
this.position = 0;
this.readPosition = 0;
this.ch = '';
this.input = input;
this.readChar();
}
/** 读取下一个 Token */
nextToken() {
this.skipWhitespace();
let tok;
switch (this.ch) {
case ',':
tok = this.makeToken(TokenType.COMMA, ',');
break;
case '(':
tok = this.makeToken(TokenType.LPAREN, '(');
break;
case ')':
tok = this.makeToken(TokenType.RPAREN, ')');
break;
case ';':
tok = this.makeToken(TokenType.SEMICOLON, ';');
break;
case '*':
tok = this.makeToken(TokenType.STAR, '*');
break;
case '.':
tok = this.makeToken(TokenType.DOT, '.');
break;
case '=':
tok = this.makeToken(TokenType.EQ, '=');
break;
case '!':
if (this.peekChar() === '=') {
this.readChar();
tok = this.makeToken(TokenType.NEQ, '!=');
}
else {
tok = this.makeToken(TokenType.ILLEGAL, '!');
}
break;
case '>':
if (this.peekChar() === '=') {
this.readChar();
tok = this.makeToken(TokenType.GTE, '>=');
}
else {
tok = this.makeToken(TokenType.GT, '>');
}
break;
case '<':
if (this.peekChar() === '=') {
this.readChar();
tok = this.makeToken(TokenType.LTE, '<=');
}
else if (this.peekChar() === '>') {
this.readChar();
tok = this.makeToken(TokenType.NEQ, '<>');
}
else {
tok = this.makeToken(TokenType.LT, '<');
}
break;
case "'":
case '"':
tok = this.readString(this.ch);
break;
case '':
tok = { type: TokenType.EOF, value: '', position: this.position };
break;
default:
// SQL 注释: -- 行注释
if (this.ch === '-' && this.peekChar() === '-') {
this.skipLineComment();
return this.nextToken();
}
// SQL 注释: /* 块注释 */
if (this.ch === '/' && this.peekChar() === '*') {
this.skipBlockComment();
return this.nextToken();
}
if (this.isLetter(this.ch)) {
const ident = this.readIdentifier();
const keyword = KEYWORDS[ident.toUpperCase()];
tok = {
type: keyword ?? TokenType.IDENTIFIER,
value: ident,
position: this.position - ident.length,
};
return tok; // 已读取完毕,不需要再 readChar
}
else if (this.isDigit(this.ch) || (this.ch === '-' && this.isDigit(this.peekChar()))) {
const num = this.readNumber();
tok = {
type: TokenType.NUMBER,
value: num,
position: this.position - num.length,
};
return tok;
}
else {
tok = this.makeToken(TokenType.ILLEGAL, this.ch);
}
break;
}
this.readChar();
return tok;
}
// ---- 内部 ----
readChar() {
if (this.readPosition >= this.input.length) {
this.ch = '';
}
else {
this.ch = this.input[this.readPosition];
}
this.position = this.readPosition;
this.readPosition++;
}
peekChar() {
if (this.readPosition >= this.input.length)
return '';
return this.input[this.readPosition];
}
skipWhitespace() {
while (this.ch === ' ' || this.ch === '\t' || this.ch === '\n' || this.ch === '\r') {
this.readChar();
}
}
/** 跳过 -- 行注释到行尾 */
skipLineComment() {
while (this.ch !== '\n' && this.ch !== '\r' && this.ch !== '') {
this.readChar();
}
}
/** 跳过块注释 slash-star ... star-slash */
skipBlockComment() {
this.readChar(); // skip *
this.readChar(); // move past *
while (this.ch !== '' && !(this.ch === '*' && this.peekChar() === '/')) {
this.readChar();
}
if (this.ch !== '') {
this.readChar(); // skip *
this.readChar(); // skip /
}
}
readIdentifier() {
const start = this.position;
while (this.isLetter(this.ch) || this.isDigit(this.ch) || this.ch === '_') {
this.readChar();
}
return this.input.slice(start, this.position);
}
readNumber() {
const start = this.position;
// 负号
if (this.ch === '-')
this.readChar();
while (this.isDigit(this.ch)) {
this.readChar();
}
// 小数点
if (this.ch === '.' && this.isDigit(this.peekChar())) {
this.readChar();
while (this.isDigit(this.ch)) {
this.readChar();
}
}
return this.input.slice(start, this.position);
}
readString(quote) {
const start = this.position + 1; // 跳过一个引号
this.readChar(); // 跳过开始引号
let value = '';
while (this.ch !== '') {
if (this.ch === quote) {
// v0.3.3: 支持 SQL 标准 '' 转义(两个连续引号 = 一个引号)
if (this.peekChar() === quote) {
value += quote;
this.readChar(); // 跳过第二个引号
this.readChar();
continue;
}
break; // 结束引号(由 nextToken 的 readChar 跳过)
}
// 反斜杠转义(兼容旧语法)
if (this.ch === '\\' && this.peekChar() === quote) {
this.readChar();
value += quote;
this.readChar();
continue;
}
value += this.ch;
this.readChar();
}
return {
type: TokenType.STRING,
value,
position: start,
};
}
isLetter(ch) {
return /[a-zA-Z_]/.test(ch);
}
isDigit(ch) {
return /[0-9]/.test(ch);
}
makeToken(type, value) {
return { type, value, position: this.position };
}
}
// ---------------------------------------------------------------------------
// 便捷方法:一次性词法分析
// ---------------------------------------------------------------------------
/** 将 SQL 字符串解析为 Token 列表 */
function tokenize(sql) {
const lexer = new Lexer(sql);
const tokens = [];
let tok = lexer.nextToken();
while (tok.type !== TokenType.EOF) {
tokens.push(tok);
tok = lexer.nextToken();
}
tokens.push(tok); // EOF
return tokens;
}
/**
* metona-sqlark SQL Parser — 递归下降语法分析器
* @module sql/parser
*
* Token 流 → AST Statement。
* 支持的语法是标准 SQL 的子集。
*/
// ---------------------------------------------------------------------------
// Parser
// ---------------------------------------------------------------------------
class Parser {
constructor(sql) {
this.sql = sql;
this.lexer = new Lexer(sql);
// 预读两个 token
this.nextToken();
this.nextToken();
}
/** 解析完整 SQL 语句 */
parseStatement() {
switch (this.curToken.type) {
case TokenType.SELECT:
return this.parseSelect();
case TokenType.INSERT:
return this.parseInsert();
case TokenType.UPDATE:
return this.parseUpdate();
case TokenType.DELETE:
return this.parseDelete();
case TokenType.CREATE:
return this.parseCreateStatement();
case TokenType.DROP:
return this.parseDropStatement();
case TokenType.ALTER:
return this.parseAlterTable();
case TokenType.TRUNCATE:
return this.parseTruncateTable();
case TokenType.BEGIN:
return this.parseBegin();
case TokenType.COMMIT:
return this.parseCommit();
case TokenType.ROLLBACK:
return this.parseRollback();
default:
throw this.error(`Unexpected token "${this.curToken.value}"`);
}
}
/** 解析所有语句(分号分隔的多语句支持) */
parseAllStatements() {
const statements = [];
while (!this.curTokenIs(TokenType.EOF)) {
// 跳过多余的分号
while (this.curTokenIs(TokenType.SEMICOLON))
this.nextToken();
if (this.curTokenIs(TokenType.EOF))
break;
statements.push(this.parseStatement());
// 语句后应紧跟分号或 EOF
if (this.curTokenIs(TokenType.SEMICOLON)) {
this.nextToken();
}
else if (!this.curTokenIs(TokenType.EOF)) {
throw this.error(`Expected ';' after statement, got "${this.curToken.value}"`);
}
}
return statements;
}
// ---- 事务语句 ----
parseBegin() {
this.expect(TokenType.BEGIN);
// 可选 TRANSACTION 关键字
if (this._isKeywordAsIdent() && this.curToken.value.toUpperCase() === 'TRANSACTION') {
this.nextToken();
}
return { type: 'BEGIN' };
}
parseCommit() {
this.expect(TokenType.COMMIT);
if (this._isKeywordAsIdent() && this.curToken.value.toUpperCase() === 'TRANSACTION') {
this.nextToken();
}
return { type: 'COMMIT' };
}
parseRollback() {
this.expect(TokenType.ROLLBACK);
if (this._isKeywordAsIdent() && this.curToken.value.toUpperCase() === 'TRANSACTION') {
this.nextToken();
}
return { type: 'ROLLBACK' };
}
// ---- CREATE TABLE / CREATE INDEX ----
parseCreateStatement() {
this.expect(TokenType.CREATE);
if (this.curTokenIs(TokenType.TABLE)) {
return this.parseCreateTable();
}
if (this.curTokenIs(TokenType.INDEX) ||
(this._isKeywordAsIdent() && this.curToken.value.toUpperCase() === 'INDEX')) {
return this.parseCreateIndex();
}
if (this.curTokenIs(TokenType.UNIQUE)) {
// CREATE UNIQUE INDEX
this.nextToken();
if (this.curTokenIs(TokenType.INDEX) ||
(this._isKeywordAsIdent() && this.curToken.value.toUpperCase() === 'INDEX')) {
const stmt = this.parseCreateIndex();
stmt.unique = true;
return stmt;
}
}
throw this.error(`Expected TABLE or INDEX after CREATE, got "${this.curToken.value}"`);
}
parseCreateIndex() {
this.expect(TokenType.INDEX);
const name = this.expectIdentifier('index name');
this.expect(TokenType.ON);
const table = this.expectIdentifier('table name');
this.expect(TokenType.LPAREN);
const column = this.expectIdentifier('column name');
this.expect(TokenType.RPAREN);
return { type: 'CREATE_INDEX', name, table, column };
}
// ---- DROP TABLE / DROP INDEX ----
parseDropStatement() {
this.expect(TokenType.DROP);
if (this.curTokenIs(TokenType.TABLE)) {
return this.parseDropTable();
}
if (this.curTokenIs(TokenType.INDEX) ||
(this._isKeywordAsIdent() && this.curToken.value.toUpperCase() === 'INDEX')) {
return this.parseDropIndex();
}
throw this.error(`Expected TABLE or INDEX after DROP, got "${this.curToken.value}"`);
}
parseDropIndex() {
this.expect(TokenType.INDEX);
const name = this.expectIdentifier('index name');
// SQLite 风格:DROP INDEX idx_name [ON table]
let table = '';
let column = '';
if (this.curTokenIs(TokenType.ON)) {
this.nextToken();
table = this.expectIdentifier('table name');
if (this.curTokenIs(TokenType.LPAREN)) {
this.nextToken();
column = this.expectIdentifier('column name');
this.expect(TokenType.RPAREN);
}
}
return { type: 'DROP_INDEX', name, table, column };
}
// ===================================================================
// SELECT
// ===================================================================
parseSelect() {
this.expect(TokenType.SELECT);
// DISTINCT(可选)
let distinct = false;
if (this.curTokenIs(TokenType.DISTINCT)) {
distinct = true;
this.nextToken();
}
// 列
const columns = [];
if (this.curTokenIs(TokenType.STAR)) {
columns.push('*');
this.nextToken();
}
else {
columns.push(...this.parseColumnList());
}
// FROMv0.4.0 可选:SELECT 1 / SELECT 'lit' 无表查询)
let fromSubquery;
let tableName = '';
let alias;
if (this.curTokenIs(TokenType.FROM)) {
this.nextToken();
// v0.4.0: FROM (SELECT ...) AS alias 派生表
if (this.curTokenIs(TokenType.LPAREN)) {
this.nextToken();
fromSubquery = this.parseSelect();
this.expect(TokenType.RPAREN);
if (this.curTokenIs(TokenType.AS)) {
this.nextToken();
alias = this.expectIdentifier('alias');
}
else if (this.curToken.type === TokenType.IDENTIFIER && !this._isReservedAfterFrom()) {
alias = this.curToken.value;
this.nextToken();
}
}
else {
tableName = this.expectIdentifier('table name');
// 表别名(可选)
if (this.curTokenIs(TokenType.AS)) {
this.nextToken();
alias = this.expectIdentifier('alias');
}
else if (this.curToken.type === TokenType.IDENTIFIER && !this._isReservedAfterFrom()) {
alias = this.curToken.value;
this.nextToken();
}
}
}
const stmt = {
type: 'SELECT',
columns,
distinct: distinct || undefined,
from: tableName,
alias,
where: {},
};
if (fromSubquery) {
stmt.fromSubquery = fromSubquery;
}
// JOIN 子句(可选,支持多个)
const joins = this.parseJoinClauses();
if (joins.length > 0) {
stmt.joins = joins;
}
// WHERE(可选)
if (this.curTokenIs(TokenType.WHERE)) {
this.nextToken();
stmt.where = this.parseCondition();
}
// GROUP BY(可选)
if (this.curTokenIs(TokenType.GROUP)) {
this.nextToken();
this.expect(TokenType.BY);
stmt.groupBy = this.parseIdentifierList();
}
// HAVING(可选)
if (this.curTokenIs(TokenType.HAVING)) {
this.nextToken();
stmt.having = this.parseCondition();
}
// ORDER BY(可选)
if (this.curTokenIs(TokenType.ORDER)) {
this.nextToken();
this.expect(TokenType.BY);
stmt.orderBy = this.parseOrderByList();
}
// LIMIT(可选)
if (this.curTokenIs(TokenType.LIMIT)) {
this.nextToken();
stmt.limit = this.expectNumber('LIMIT value');
}
// OFFSET(可选)
if (this.curTokenIs(TokenType.OFFSET)) {
this.nextToken();
stmt.offset = this.expectNumber('OFFSET value');
}
// UNION / UNION ALL(可选,v0.3.0
if (this.curTokenIs(TokenType.UNION)) {
return this.parseUnion(stmt);
}
return stmt;
}
/** 解析 UNION / UNION ALL 组合(支持链式) */
parseUnion(left) {
this.expect(TokenType.UNION);
let all = false;
if (this.curTokenIs(TokenType.ALL)) {
all = true;
this.nextToken();
}
const right = this.parseSelect();
const unionStmt = { type: 'SELECT_UNION', left, right, all: all || undefined };
// 链式 UNION
if (this.curTokenIs(TokenType.UNION)) {
return this.parseUnionChain(unionStmt);
}
return unionStmt;
}
/** 链式 UNION:左侧是已组合的 UNION 语句 */
parseUnionChain(left) {
this.expect(TokenType.UNION);
let all = false;
if (this.curTokenIs(TokenType.ALL)) {
all = true;
this.nextToken();
}
const right = this.parseSelect();
const unionStmt = { type: 'SELECT_UNION', left, right, all: all || undefined };
if (this.curTokenIs(TokenType.UNION)) {
return this.parseUnionChain(unionStmt);
}
return unionStmt;
}
/** 解析 JOIN 子句列表 */
parseJoinClauses() {
const joins = [];
while (this._isJoinKeyword()) {
joins.push(this.parseJoinClause());
}
return joins;
}
_isJoinKeyword() {
return (this.curTokenIs(TokenType.INNER) ||
this.curTokenIs(TokenType.LEFT) ||
this.curTokenIs(TokenType.RIGHT) ||
this.curTokenIs(TokenType.CROSS) ||
this.curTokenIs(TokenType.JOIN));
}
/** 解析单个 JOIN 子句 */
parseJoinClause() {
let type = 'INNER';
if (this.curTokenIs(TokenType.INNER)) {
type = 'INNER';
this.nextToken();
}
else if (this.curTokenIs(TokenType.LEFT)) {
type = 'LEFT';
this.nextToken();
if (this.curTokenIs(TokenType.OUTER))
this.nextToken(); // 可选 OUTER
}
else if (this.curTokenIs(TokenType.RIGHT)) {
type = 'RIGHT';
this.nextToken();
if (this.curTokenIs(TokenType.OUTER))
this.nextToken();
}
else if (this.curTokenIs(TokenType.CROSS)) {
type = 'CROSS';
this.nextToken();
}
this.expect(TokenType.JOIN);
const tableName = this.expectIdentifier('table name');
// JOIN 表别名(可选)
let alias;
if (this.curTokenIs(TokenType.AS)) {
this.nextToken();
alias = this.expectIdentifier('alias');
}
else if (this.curToken.type === TokenType.IDENTIFIER && !this._isJoinReserved()) {
alias = this.curToken.value;
this.nextToken();
}
// ON 条件(CROSS JOIN 不需要 ON
let on = {};
if (type !== 'CROSS' && this.curTokenIs(TokenType.ON)) {
this.nextToken();
on = this.parseCondition();
}
return { type, table: tableName, alias, on };
}
/** 判断当前 token 是否为 FROM 之后的保留字 */
_isReservedAfterFrom() {
return (this.curTokenIs(TokenType.WHERE) ||
this.curTokenIs(TokenType.ORDER) ||
this.curTokenIs(TokenType.LIMIT) ||
this.curTokenIs(TokenType.OFFSET) ||
this.curTokenIs(TokenType.GROUP) ||
this._isJoinKeyword());
}
_isJoinReserved() {
return (this.curTokenIs(TokenType.ON) ||
this.curTokenIs(TokenType.WHERE) ||
this.curTokenIs(TokenType.ORDER) ||
this.curTokenIs(TokenType.LIMIT) ||
this._isJoinKeyword());
}
// ===================================================================
// INSERT
// ===================================================================
parseInsert() {
this.expect(TokenType.INSERT);
this.expect(TokenType.INTO);
const tableName = this.expectIdentifier('table name');
// 列名(可选)
let columns;
if (this.curTokenIs(TokenType.LPAREN)) {
this.nextToken();
columns = this.parseIdentifierList();
this.expect(TokenType.RPAREN);
}
// INSERT INTO ... SELECT ...v0.3.0
if (this.curTokenIs(TokenType.SELECT)) {
return {
type: 'INSERT',
into: tableName,
columns,
select: this.parseSelect(),
};
}
// VALUES
this.expect(TokenType.VALUES);
// 值列表
const values = [];
do {
if (this.curTokenIs(TokenType.COMMA)) {
this.nextToken();
}
this.expect(TokenType.LPAREN);
const rowValues = this.parseValueList();
this.expect(TokenType.RPAREN);
values.push(rowValues);
} while (this.curTokenIs(TokenType.COMMA));
return {
type: 'INSERT',
into: tableName,
columns,
values,
};
}
// ===================================================================
// UPDATE
// ===================================================================
parseUpdate() {
this.expect(TokenType.UPDATE);
const tableName = this.expectIdentifier('table name');
this.expect(TokenType.SET);
// SET col=val, ...
const sets = {};
do {
if (this.curTokenIs(TokenType.COMMA))
this.nextToken();
const col = this.expectIdentifier('column name');
this.expect(TokenType.EQ);
sets[col] = this.parseValue();
} while (this.curTokenIs(TokenType.COMMA));
let where = {};
if (this.curTokenIs(TokenType.WHERE)) {
this.nextToken();
where = this.parseCondition();
}
return { type: 'UPDATE', table: tableName, sets, where };
}
// ===================================================================
// DELETE
// ===================================================================
parseDelete() {
this.expect(TokenType.DELETE);
this.expect(TokenType.FROM);
const tableName = this.expectIdentifier('table name');
let where = {};
if (this.curTokenIs(TokenType.WHERE)) {
this.nextToken();
where = this.parseCondition();
}
return { type: 'DELETE', from: tableName, where };
}
// ===================================================================
// CREATE TABLE
// ===================================================================
parseCreateTable() {
this.expect(TokenType.TABLE);
// IF NOT EXISTS(可选)
let ifNotExists = false;
if (this.curTokenIs(TokenType.IF)) {
this.nextToken();
this.expect(TokenType.NOT);
this.expect(TokenType.EXISTS);
ifNotExists = true;
}
const tableName = this.expectIdentifier('table name');
this.expect(TokenType.LPAREN);
const columns = [];
do {
if (this.curTokenIs(TokenType.COMMA))
this.nextToken();
columns.push(this.parseColumnDef());
} while (this.curTokenIs(TokenType.COMMA));
this.expect(TokenType.RPAREN);
return { type: 'CREATE_TABLE', name: tableName, columns, ifNotExists: ifNotExists || undefined };
}
parseColumnDef() {
const name = this.expectIdentifier('column name');
const type = this.expectIdentifier('column type').toLowerCase();
const col = { name, type };
// 修饰符
while (this.curTokenIs(TokenType.PRIMARY) ||
this.curTokenIs(TokenType.UNIQUE) ||
this.curTokenIs(TokenType.NOT) ||
this.curTokenIs(TokenType.DEFAULT) ||
this.curTokenIs(TokenType.REFERENCES)) {
if (this.curTokenIs(TokenType.PRIMARY)) {
this.nextToken();
this.expect(TokenType.KEY);
col.primaryKey = true;
}
else if (this.curTokenIs(TokenType.UNIQUE)) {
this.nextToken();
col.unique = true;
}
else if (this.curTokenIs(TokenType.NOT)) {
this.nextToken();
this.expect(TokenType.NULL);
col.required = true;
}
else if (this.curTokenIs(TokenType.DEFAULT)) {
this.nextToken();
col.default = this.parseValue();
}
else if (this.curTokenIs(TokenType.REFERENCES)) {
this.nextToken();
const refTable = this.expectIdentifier('referenced table');
this.expect(TokenType.LPAREN);
const refCol = this.expectIdentifier('referenced column');
this.expect(TokenType.RPAREN);
col.references = `${refTable}.${refCol}`;
// ON DELETE / ON UPDATE
while (this.curTokenIs(TokenType.ON)) {
this.nextToken();
if (this.curTokenIs(TokenType.DELETE)) {
this.nextToken();
col.onDelete = this.parseCascadeAction();
}
else if (this.curTokenIs(TokenType.UPDATE)) {
this.nextToken();
col.onUpdate = this.parseCascadeAction();
}
else {
break;
}
}
}
else {
break;
}
}
return col;
}
/** 解析 CASCADE | SET NULL | RESTRICT */
parseCascadeAction() {
if (this.curTokenIs(TokenType.CASCADE)) {
this.nextToken();
return 'CASCADE';
}
if (this.curTokenIs(TokenType.SET)) {
this.nextToken();
this.expect(TokenType.NULL);
return 'SET NULL';
}
// RESTRICT 或默认
if (this.curToken.type === TokenType.IDENTIFIER && this.curToken.value.toUpperCase() === 'RESTRICT') {
this.nextToken();
return 'RESTRICT';
}
return 'RESTRICT';
}
// ===================================================================
// ALTER TABLE
// ===================================================================
parseAlterTable() {
this.expect(TokenType.ALTER);
this.expect(TokenType.TABLE);
const tableName = this.expectIdentifier('table name');
// ADD COLUMN / DROP COLUMN
let action;
if (this.curTokenIs(TokenType.ADD)) {
action = 'ADD';
this.nextToken();
// Optional COLUMN keyword
if (this.curToken.type === TokenType.IDENTIFIER && this.curToken.value.toUpperCase() === 'COLUMN') {
this.nextToken();
}
const col = this.parseColumnDef();
return { type: 'ALTER_TABLE', name: tableName, action, column: col };
}
else if (this.curTokenIs(TokenType.DROP) ||
(this._isKeywordAsIdent() && this.curToken.value.toUpperCase() === 'DROP')) {
action = 'DROP';
this.nextToken();
// Optional COLUMN keyword
if (this.curToken.type === TokenType.IDENTIFIER && this.curToken.value.toUpperCase() === 'COLUMN') {
this.nextToken();
}
const colName = this.expectIdentifier('column name');
return { type: 'ALTER_TABLE', name: tableName, action, column: { name: colName, type: 'string' } };
}
else {
throw this.error('Expected ADD or DROP in ALTER TABLE');
}
}
// ===================================================================
// TRUNCATE TABLE
// ===================================================================
parseTruncateTable() {
this.expect(TokenType.TRUNCATE);
this.expect(TokenType.TABLE);
const tableName = this.expectIdentifier('table name');
return { type: 'TRUNCATE_TABLE', name: tableName };
}
// ===================================================================
// DROP TABLE
// ===================================================================
parseDropTable() {
this.expect(TokenType.TABLE);
// IF EXISTS(可选)
let ifExists = false;
if (this.curTokenIs(TokenType.IF)) {
this.nextToken();
this.expect(TokenType.EXISTS);
ifExists = true;
}
const tableName = this.expectIdentifier('table name');
return { type: 'DROP_TABLE', name: tableName, ifExists: ifExists || undefined };
}
// ===================================================================
// 条件表达式
// ===================================================================
/** condition → simple_cond ((AND|OR) simple_cond)* */
parseCondition() {
let left = this.parseSimpleCondition();
while (this.curTokenIs(TokenType.AND) || this.curTokenIs(TokenType.OR)) {
const isAnd = this.curTokenIs(TokenType.AND);
this.nextToken();
const right = this.parseSimpleCondition();
if (isAnd) {
// 合并到 $and
left = { $and: [left, right] };
}
else {
left = { $or: [left, right] };
}
}
return left;
}
/** 公共 WHERE 条件入口(供 CASE WHEN 求值等外部场景,v0.3.1 */
parseWhere() {
return this.parseCondition();
}
/** simple_cond → column op value | column IS [NOT] NULL | column [NOT] LIKE pattern
* | column [NOT] IN (values) | NOT condition | (condition)
* | [NOT] EXISTS (SELECT ...) ← v0.3.0 */
parseSimpleCondition() {
// [NOT] EXISTS (SELECT ...)
if (this.curTokenIs(TokenType.EXISTS) ||
(this._isKeywordAsIdent() && this.curToken.value.toUpperCase() === 'EXISTS')) {
this.nextToken();
return this.parseExistsCondition(false);
}
if (this.curTokenIs(TokenType.NOT) && this._peekIsExists()) {
this.nextToken(); // 跳过 NOT
this.nextToken(); // 跳过 EXISTS
return this.parseExistsCondition(true);
}
// NOT expr(注意 NOT IN / NOT LIKE 不作为通用 NOT
if (this.curTokenIs(TokenType.NOT) && !this._isNotInOrLike()) {
this.nextToken();
const inner = this.parseSimpleCondition();
return { $not: inner };
}
// (condition)
if (this.curTokenIs(TokenType.LPAREN)) {
this.nextToken();
const inner = this.parseCondition();
this.expect(TokenType.RPAREN);
return inner;
}
// column
const column = this.parseColumnRef();
// IS NULL / IS NOT NULL
if (this.curTokenIs(TokenType.IDENTIFIER) && this.curToken.value.toUpperCase() === 'IS') {
this.nextToken();
const isNot = this.curTokenIs(TokenType.NOT);
if (isNot)
this.nextToken();
this.expect(TokenType.NULL);
const result = {};
result[column] = isNot ? { $ne: null } : { $eq: null };
return result;
}
// BETWEEN val1 AND val2
if (this.curTokenIs(TokenType.BETWEEN)) {
this.nextToken();
const low = this.parseValue();
this.expect(TokenType.AND);
const high = this.parseValue();
const result = {};
result[column] = { $gte: low, $lte: high };
return result;
}
// NOT BETWEEN val1 AND val2
if (this.curTokenIs(TokenType.NOT) && this.peekTokenIs(TokenType.BETWEEN)) {
this.nextToken(); // skip NOT
this.nextToken(); // skip BETWEEN
const low = this.parseValue();
this.expect(TokenType.AND);
const high = this.parseValue();
const result = {};
result[column] = { $not: { $gte: low, $lte: high } };
return result;
}
// NOT LIKE / NOT INNOT 后紧跟 LIKE 或 IN
if (this.curTokenIs(TokenType.NOT)) {
if (this.peekTokenIs(TokenType.IN)) {
// NOT IN
this.nextToken(); // skip NOT
this.nextToken(); // skip IN
this.expect(TokenType.LPAREN);
if (this.curTokenIs(TokenType.SELECT)) {
const subquery = this.parseSelect();
this.expect(TokenType.RPAREN);
const result = {};
result[column] = { $nin: { $subquery: subquery } };
return result;
}
const values = this.parseValueList();
this.expect(TokenType.RPAREN);
const result = {};
result[column] = { $nin: values };
return result;
}
else if (this.peekTokenIs(TokenType.LIKE)) {
// NOT LIKE
this.nextToken(); // skip NOT
this.nextToken(); // skip LIKE
const pattern = this.parseValue();
const result = {};
result[column] = { $not: { $like: pattern } };
return result;
}
}
// LIKE
if (this.curTokenIs(TokenType.LIKE)) {
this.nextToken();
const pattern = this.parseValue();
const result = {};
result[column] = { $like: pattern };
return result;
}
// IN
if (this.curTokenIs(TokenType.IN)) {
this.nextToken();
this.expect(TokenType.LPAREN);
// 子查询: IN (SELECT ...)
if (this.curTokenIs(TokenType.SELECT)) {
const subquery = this.parseSelect();
this.expect(TokenType.RPAREN);
const result = {};
result[column] = { $in: { $subquery: subquery } };
return result;
}
const values = this.parseValueList();
this.expect(TokenType.RPAREN);
const result = {};
result[column] = { $in: values };
return result;
}
// v0.4.1: 裸布尔列条件(WHERE done / CASE WHEN done THEN)— 列后直接是终止符时视为真值判断
if (this.curTokenIs(TokenType.AND) || this.curTokenIs(TokenType.OR) ||
this.curTokenIs(TokenType.RPAREN) || this.curTokenIs(TokenType.EOF) ||
(this.curToken.type === TokenType.IDENTIFIER && ['THEN', 'END', 'ELSE', 'NULLS', 'LIMIT', 'OFFSET', 'ORDER', 'GROUP', 'HAVING', 'UNION', 'WHERE'].includes(this.curToken.value.toUpperCase()))) {
const result = {};
result[column] = { $eq: true };
return result;
}
// 比较运算符
const op = this.parseComparisonOp();
// 子查询: op (SELECT ...)
if (this.curTokenIs(TokenType.LPAREN) && this.peekTokenIs(TokenType.SELECT)) {
this.nextToken(); // skip (
const subquery = this.parseSelect();
this.expect(TokenType.RPAREN);
const result = {};
result[column] = { [op]: { $subquery: subquery } };
return result;
}
// 尝试解析列引用(identifier DOT identifier 格式)
let value;
if ((this.curToken.type === TokenType.IDENTIFIER || this._isKeywordAsIdent()) &&
this.peekTokenIs(TokenType.DOT)) {
const colRef = this.parseColumnRef();
value = { $col: colRef };
}
else {
value = this.parseValue();
}
const result = {};
result[column] = { [op]: value };
return result;
}
/** 解析 EXISTS (SELECT ...) / NOT EXISTS (SELECT ...) */
parseExistsCondition(negate) {
this.expect(TokenType.LPAREN);
const subquery = this.parseSelect();
this.expect(TokenType.RPAREN);
// $exists 键由 Executor.resolveSubqueries 解析为 booleanwhere-matcher 消费
return { $exists: { $subquery: subquery, $negate: negate || undefined } };
}
/** 判断当前 NOT 后是否紧跟 EXISTS */
_peekIsExists() {
return this.peekToken.type === TokenType.EXISTS ||
(this.peekToken.type === TokenType.IDENTIFIER && this.peekToken.value.toUpperCase() === 'EXISTS');
}
/** 判断当前 NOT 是否为 NOT IN / NOT LIKE 的一部分(不应作为通用 NOT 处理) */
_isNotInOrLike() {
return this.peekTokenIs(TokenType.IN) || this.peekTokenIs(TokenType.LIKE);
}
peekTokenIs(type) {
return this.peekToken.type === type;
}
parseComparisonOp() {
switch (this.curToken.type) {
case TokenType.EQ:
this.nextToken();
return '$eq';
case TokenType.NEQ:
this.nextToken();
return '$ne';
case TokenType.GT:
this.nextToken();
return '$gt';
case TokenType.GTE:
this.nextToken();
return '$gte';
case TokenType.LT:
this.nextToken();
return '$lt';
case TokenType.LTE:
this.nextToken();
return '$lte';
default:
throw this.error(`Expected comparison operator, got "${this.curToken.value}"`);
}
}
// ===================================================================
// 辅助解析
// ===================================================================
parseColumnList() {
const cols = [];
cols.push(this.parseColumnWithAlias());
while (this.curTokenIs(TokenType.COMMA)) {
this.nextToken();
cols.push(this.parseColumnWithAlias());
}
return cols;
}
/** v0.3.3: 解析列(支持 `col AS alias` 显式别名与 `col alias` 隐式别名) */
parseColumnWithAlias() {
let col = this.parseColumnRef();
if (this.curTokenIs(TokenType.AS)) {
this.nextToken();
const alias = this.expectIdentifier('alias');
col = `${col} AS ${alias}`;
}
else if (this.curToken.type === TokenType.IDENTIFIER && !this._isReservedAfterFrom() && !this._isJoinKeyword()) {
const alias = this.curToken.value;
this.nextToken();
col = `${col} AS ${alias}`;
}
return col;
}
/** 解析列引用:支持 'col'、'table.col'、'COUNT(*)'/'SUM(col)'、数字常量列(SELECT 1)、字符串常量列(SELECT 'x'v0.4.0)和 CASE WHEN 表达式(v0.3.1 */
parseColumnRef() {
// CASE WHEN 表达式(v0.3.1
if (this.curTokenIs(TokenType.CASE)) {
return this.parseCaseExpressionText();
}
// 数字常量列:SELECT 1 FROM t(常见于 EXISTS 子查询)
if (this.curTokenIs(TokenType.NUMBER)) {
const value = this.curToken.value;
this.nextToken();
return value;
}
// v0.4.0: 字符串常量列:SELECT 'value' FROM t
if (this.curTokenIs(TokenType.STRING)) {
const value = this.curToken.value;
this.nextToken();
return `'${value}'`;
}
// 聚合函数?
if (this.curTokenIs(TokenType.COUNT) ||
this.curTokenIs(TokenType.SUM) ||
this.curTokenIs(TokenType.AVG) ||
this.curTokenIs(TokenType.MIN) ||
this.curTokenIs(TokenType.MAX)) {
return this.parseAggregateCall();
}
const first = this.expectIdentifier('column name');
if (this.curTokenIs(TokenType.DOT)) {
this.nextToken();
const second = this.expectIdentifier('column name');
return `${first}.${second}`;
}
return first;
}
/**
* 解析 CASE WHEN 表达式,返回原文(含可选 AS 别名)。
* 例:CASE WHEN age > 30 THEN 'senior' ELSE 'junior' END AS status
*/
parseCaseExpressionText() {
const start = this.curToken.position;
this.nextToken(); // 跳过 CASE
let depth = 1;
let end = start + 'CASE'.length;
while (!this.curTokenIs(TokenType.EOF) && depth > 0) {
if (this.curTokenIs(TokenType.CASE))
depth++;
if (this.curTokenIs(TokenType.END)) {
depth--;
end = this.curToken.position + 'END'.length;
this.nextToken();
if (depth === 0)
break;
}
end = this.curToken.position + this.curToken.value.length;
this.nextToken();
}
let text = this.sql.slice(start, end);
// 可选 AS 别名
if (this.curTokenIs(TokenType.AS)) {
this.nextToken();
text += ` AS ${this.expectIdentifier('alias')}`;
}
else if (this.curToken.type === TokenType.IDENTIFIER && !this.curTokenIs(TokenType.COMMA) && !this._isReservedAfterFrom()) {
text += ` AS ${this.curToken.value}`;
this.nextToken();
}
return text;
}
/** 解析聚合函数调用: COUNT(*), SUM(col), AVG(col), MIN(col), MAX(col)v0.4.0 支持 COUNT(DISTINCT col) */
parseAggregateCall() {
const func = this.curToken.value.toUpperCase();
this.nextToken();
this.expect(TokenType.LPAREN);
// v0.4.0: COUNT(DISTINCT col) 等去重聚合
let distinct = false;
if (this.curTokenIs(TokenType.DISTINCT)) {
distinct = true;
this.nextToken();
}
let arg;
if (this.curTokenIs(TokenType.STAR)) {
arg = '*';
this.nextToken();
}
else {
arg = this.parseColumnRef();
}
this.expect(TokenType.RPAREN);
// 可选别名: AS alias
let alias = '';
if (this.curTokenIs(TokenType.AS)) {
this.nextToken();
alias = this.expectIdentifier('alias');
}
else if (this.curToken.type === TokenType.IDENTIFIER && this._isAggregateAlias()) {
alias = this.curToken.value;
this.nextToken();
}
const inner = distinct ? `DISTINCT ${arg}` : arg;
if (alias) {
return `${func}(${inner}) AS ${alias}`;
}
return `${func}(${inner})`;
}
_isAggregateAlias() {
return !this._isReservedAfterFrom() && !this._isJoinKeyword();
}
parseIdentifierList() {
const ids = [];
ids.push(this.parseIdentifierWithDot());
while (this.curTokenIs(TokenType.COMMA)) {
this.nextToken();
ids.push(this.parseIdentifierWithDot());
}
return ids;
}
parseValueList() {
const vals = [];
vals.push(this.parseValue());
while (this.curTokenIs(TokenType.COMMA)) {
this.nextToken();
vals.push(this.parseValue());
}
return vals;
}
parseOrderByList() {
const list = [];
list.push(this.parseOrderBy());
while (this.curTokenIs(TokenType.COMMA)) {
this.nextToken();
list.push(this.parseOrderBy());
}
return list;
}
parseOrderBy() {
const column = this.parseIdentifierWithDot();
let direction = 'asc';
if (this.curTokenIs(TokenType.ASC)) {
this.nextToken();
}
else if (this.curTokenIs(TokenType.DESC)) {
direction = 'desc';
this.nextToken();
}
// v0.4.0: NULLS FIRST / NULLS LAST
let nulls;
if (this.curTokenIs(TokenType.IDENTIFIER) && this.curToken.value.toUpperCase() === 'NULLS') {
this.nextToken();
if (this.curTokenIs(TokenType.IDENTIFIER) && this.curToken.value.toUpperCase() === 'FIRST') {
nulls = 'first';
this.nextToken();
}
else if (this.curTokenIs(TokenType.IDENTIFIER) && this.curToken.value.toUpperCase() === 'LAST') {
nulls = 'last';
this.nextToken();
}
}
return { column, direction, ...(nulls ? { nulls } : {}) };
}
/** v0.4.0: 标识符(支持 'table.column' 带表前缀引用,用于 ORDER BY / GROUP BY */
parseIdentifierWithDot() {
const first = this.expectIdentifier('identifier');
if (this.curTokenIs(TokenType.DOT)) {
this.nextToken();
return `${first}.${this.expectIdentifier('identifier')}`;
}
return first;
}
/** 解析字面量值 */
parseValue() {
switch (this.curToken.type) {
case TokenType.STRING: {
const val = this.curToken.value;
this.nextToken();
return val;
}
case TokenType.NUMBER: {
const val = Number(this.curToken.value);
this.nextToken();
return val;
}
case TokenType.TRUE:
this.nextToken();
return true;
case TokenType.FALSE:
this.nextToken();
return false;
case TokenType.NULL:
this.nextToken();
return null;
default:
throw this.error(`Expected value, got "${this.curToken.value}"`);
}
}
// ===================================================================
// Token 操作
// ===================================================================
nextToken() {
this.curToken = this.peekToken;
this.peekToken = this.lexer.nextToken();
}
curTokenIs(type) {
return this.curToken.type === type;
}
expect(type) {
if (this.curTokenIs(type)) {
this.nextToken();
return;
}
throw this.error(`Expected ${type}, got "${this.curToken.value}"`);
}
expectIdentifier(context) {
if (this.curToken.type === TokenType.IDENTIFIER || this._isKeywordAsIdent()) {
const val = this.curToken.value;
this.nextToken();
return val;
}
throw this.error(`Expected ${context}, got "${this.curToken.value}"`);
}
/** 关键字可以作为标识符(如列名等于关键字) */
_isKeywordAsIdent() {
return (this.curToken.type !== TokenType.EOF &&
this.curToken.type !== TokenType.ILLEGAL &&
this.curToken.type !== TokenType.STRING &&
this.curToken.type !== TokenType.NUMBER &&
this.curToken.type !== TokenType.COMMA &&
this.curToken.type !== TokenType.LPAREN &&
this.curToken.type !== TokenType.RPAREN &&
this.curToken.type !== TokenType.SEMICOLON &&
this.curToken.type !== TokenType.EQ &&
this.curToken.type !== TokenType.NEQ &&
this.curToken.type !== TokenType.GT &&
this.curToken.type !== TokenType.GTE &&
this.curToken.type !== TokenType.LT &&
this.curToken.type !== TokenType.LTE &&
this.curToken.type !== TokenType.DOT &&
this.curToken.type !== TokenType.STAR);
}
expectNumber(context) {
if (this.curToken.type === TokenType.NUMBER) {
const val = Number(this.curToken.value);
this.nextToken();
return val;
}
throw this.error(`Expected ${context}, got "${this.curToken.value}"`);
}
error(msg) {
return new DatabaseError(`Parse error at position ${this.curToken.position}: ${msg}`, 'PARSE_ERROR');
}
}
// ---------------------------------------------------------------------------
// 便捷方法
// ---------------------------------------------------------------------------
/** 解析 SQL 字符串为 AST Statement */
function parse(sql) {
const parser = new Parser(sql);
const stmt = parser.parseStatement();
return stmt;
}
/** 解析 SQL 字符串为 AST Statement 数组(分号分隔的多语句支持,v0.3.0) */
function parseAll(sql) {
const parser = new Parser(sql);
return parser.parseAllStatements();
}
/** 解析独立 WHERE 条件表达式(CASE WHEN 求值等场景,v0.3.1 */
function parseWhereCondition(sql) {
const parser = new Parser(sql);
return parser.parseWhere();
}
/**
* metona-sqlark Query Executor — AST 执行器
* @module query/executor
*
* JOIN / GROUP BY / DISTINCT 逻辑在此层处理。
*/
/** 解析 "CASE WHEN c1 THEN v1 WHEN c2 THEN v2 ELSE v3 END [AS alias]" */
function parseCaseExpression(expr) {
const m = expr.match(/^\s*CASE\s+([\s\S]*?)\s+END\s*(?:AS\s+(\w+))?\s*$/i);
if (!m)
return null;
const body = m[1];
const alias = m[2] ?? null;
const whens = [];
const re = /WHEN\s+([\s\S]*?)\s+THEN\s+([\s\S]*?)(?=\s+WHEN\s+|\s+ELSE\s+|\s*$)/gi;
let match;
while ((match = re.exec(body)) !== null) {
let cond = null;
try {
cond = parseWhereCondition(match[1].trim());
}
catch {
// 条件解析失败视为不匹配
}
whens.push({ cond, value: match[2].trim() });
}
let elseValue = null;
const elseMatch = body.match(/\sELSE\s+([\s\S]*)$/i);
if (elseMatch)
elseValue = elseMatch[1].trim();
return { whens, elseValue, alias };
}
/** 解析 CASE 值:字面量(null/true/false/数字/字符串)优先,其次列引用 → 行值 */
function resolveCaseValue(text, row) {
const v = text.trim();
if (v === 'null')
return null;
if (v === 'true')
return true;
if (v === 'false')
return false;
const num = Number(v);
if (v !== '' && !isNaN(num))
return num;
const str = v.match(/^'(.*)'$/s) || v.match(/^"(.*)"$/s);
if (str)
return str[1];
if (/^[a-zA-Z_][a-zA-Z0-9_.]*$/.test(v)) {
return row[v] ?? null; // 列引用(含 table.col
}
return v;
}
/** 对行求值 CASE WHEN 表达式 */
function evaluateCase(expr, row) {
for (const { cond, value } of expr.whens) {
if (cond && matchWhere(row, cond)) {
return resolveCaseValue(value, row);
}
}
return expr.elseValue !== null ? resolveCaseValue(expr.elseValue, row) : null;
}
// ---------------------------------------------------------------------------
// Executor
// ---------------------------------------------------------------------------
class QueryExecutor {
constructor(engine, maxRowsPerQuery = 0) {
this.engine = engine;
this.maxRowsPerQuery = maxRowsPerQuery;
}
/** 设置查询结果行数上限 */
setMaxRowsPerQuery(max) {
this.maxRowsPerQuery = max;
}
async execute(stmt) {
switch (stmt.type) {
case 'SELECT': return this.executeSelect(stmt);
case 'SELECT_UNION': return this.executeSelectUnion(stmt);
case 'EXPLAIN': return this.executeExplain(stmt);
case 'INSERT': return this.executeInsert(stmt);
case 'UPDATE': return this.executeUpdate(stmt);
case 'DELETE': return this.executeDelete(stmt);
case 'CREATE_TABLE': return this.executeCreateTable(stmt);
case 'DROP_TABLE': return this.executeDropTable(stmt);
case 'ALTER_TABLE': return this.executeAlterTable(stmt);
case 'TRUNCATE_TABLE': return this.executeTruncateTable(stmt);
case 'CREATE_INDEX': return this.executeCreateIndex(stmt);
case 'DROP_INDEX': return this.executeDropIndex(stmt);
case 'BEGIN': return this.executeBegin();
case 'COMMIT': return this.executeCommit();
case 'ROLLBACK': return this.executeRollback();
default: throw new DatabaseError('Unknown statement type', 'UNKNOWN_STATEMENT');
}
}
// ===================================================================
// UNIONv0.3.0
// ===================================================================
/** 递归执行 UNION / UNION ALL,返回合并结果 */
async executeSelectUnion(stmt) {
const leftRows = await this.executeSelectPart(stmt.left);
const rightRows = await this.executeSelectPart(stmt.right);
// 列名以左侧为准,右侧只取值
const leftCols = leftRows.length > 0 ? Object.keys(leftRows[0]) : [];
const normalized = leftRows.map((row) => row);
if (stmt.all) {
for (const row of rightRows)
normalized.push(this.projectUnionRow(row, leftCols));
return normalized;
}
// UNION 去重(与 DISTINCT 相同的列值拼接键)
const seen = new Set();
const result = [];
for (const row of normalized) {
const key = Object.values(row).map((v) => String(v ?? '\0')).join('\x1f');
if (!seen.has(key)) {
seen.add(key);
result.push(row);
}
}
for (const row of rightRows) {
const projected = this.projectUnionRow(row, leftCols);
const key = Object.values(projected).map((v) => String(v ?? '\0')).join('\x1f');
if (!seen.has(key)) {
seen.add(key);
result.push(projected);
}
}
return result;
}
async executeSelectPart(part) {
return part.type === 'SELECT_UNION' ? this.executeSelectUnion(part) : this.executeSelect(part);
}
/** 将 UNION 右侧行投影为左侧列结构(按位置取值) */
projectUnionRow(row, leftCols) {
if (leftCols.length === 0)
return row;
const values = Object.values(row);
const projected = {};
for (let i = 0; i < leftCols.length; i++) {
projected[leftCols[i]] = i < values.length ? values[i] : null;
}
return projected;
}
/** EXPLAIN: 输出查询计划 */
async executeExplain(stmt) {
const plan = compileStatement(stmt.query.type === 'SELECT' ? stmt.query : stmt.query);
const startTime = Date.now();
let result = null;
try {
result = await this.execute(stmt.query);
}
catch { /* explain 即使执行失败也返回计划 */ }
const elapsed = Date.now() - startTime;
const rows = Array.isArray(result) ? result.length : 0;
return {
type: stmt.query.type,
table: plan.table,
columns: plan.columns,
where: plan.where || {},
orderBy: plan.orderBy || [],
limit: plan.limit,
offset: plan.offset,
usingIndex: plan.table ? 'auto' : 'none',
estimatedRows: rows,
actualTimeMs: elapsed,
};
}
// ===================================================================
// SELECT
// ===================================================================
async executeSelect(stmt) {
const hasGroupBy = !!(stmt.groupBy && stmt.groupBy.length > 0);
const hasAggregate = !hasGroupBy && this._hasAggregateColumn(stmt.columns);
let rows;
const isJoinQuery = !!(stmt.joins && stmt.joins.length > 0);
// CASE WHEN 表达式需要原始列求值(SELECT 列或 WHERE 条件中的 CASE):
// 引擎层取全行,投影统一在 executor 端完成
const needsRawRows = this.hasCaseColumn(stmt.columns) ||
(!!stmt.where && this.whereHasCase(stmt.where));
// v0.3.3: ORDER BY 引用 SELECT 别名 → 引擎层不排序/不截断,投影后再排序
const orderByAlias = this.orderByUsesSelectAlias(stmt);
// v0.3.3: SELECT 列含 `col AS alias` → 引擎层投影会丢失源列,统一取原始行由 executor 投影
const hasSelectAlias = stmt.columns.some((c) => /\s+AS\s+\w+$/i.test(c));
if (stmt.fromSubquery) {
// v0.4.0: FROM (SELECT ...) 派生表 — 子查询结果作为行源
const subRows = await this.executeSelectPart(stmt.fromSubquery);
rows = isJoinQuery
? await this.executeJoinSelect(stmt, subRows.map((row) => this.prefixRow(row, stmt.alias ?? '')))
: subRows;
if (!isJoinQuery && stmt.where && Object.keys(stmt.where).length > 0) {
// 非 JOINWHERE 在 executor 端过滤(子查询结果不经引擎)
stmt.where = await this.resolveSubqueries(stmt.where);
rows = rows.filter((row) => matchWhere(row, stmt.where));
}
}
else if (!stmt.from && !isJoinQuery) {
// v0.4.0: 无表查询(SELECT 1 / SELECT 'lit')— 单行空上下文,常量列投影
rows = [{}];
}
else if (isJoinQuery) {
// JOIN 路径:行带表别名前缀(如 'd.id'),WHERE 保持原名不剥离
rows = await this.executeJoinSelect(stmt);
}
else {
// 非 JOIN 路径:规范化 WHERE 字段名(剥离主表别名前缀,修复 WHERE u.age > 20
if (stmt.where && Object.keys(stmt.where).length > 0) {
stmt.where = this.normalizeWhereColumns(stmt.where, [stmt.alias ?? stmt.from]);
}
// v0.4.0: ORDER BY / GROUP BY 带表前缀同样剥离(如 ORDER BY u.age
const mainAliases = [stmt.alias ?? stmt.from].filter(Boolean);
if (stmt.orderBy && stmt.orderBy.length > 0) {
stmt.orderBy = stmt.orderBy.map((o) => ({ ...o, column: this.stripAlias(o.column, mainAliases) }));
}
if (stmt.groupBy && stmt.groupBy.length > 0) {
stmt.groupBy = stmt.groupBy.map((c) => this.stripAlias(c, mainAliases));
}
// v0.4.0: SELECT 列带表前缀剥离(SELECT u.name → name,行键无前缀)
stmt.columns = stmt.columns.map((c) => {
if (c === '*' || /^(COUNT|SUM|AVG|MIN|MAX)\(/i.test(c) || /^\s*CASE\b/i.test(c) || /^'/.test(c))
return c;
const m = c.match(/^(.+?)\s+AS\s+(\w+)$/i);
if (m) {
const stripped = this.stripAlias(m[1].trim(), mainAliases);
return stripped === m[1].trim() ? c : `${stripped} AS ${m[2]}`;
}
return this.stripAlias(c, mainAliases);
});
// WHERE 含关联子查询($col 引用外层行)→ 逐行绑定上下文求值
if (stmt.where && this.hasCorrelatedRefs(stmt.where)) {
const plan = compileStatement(hasGroupBy || hasAggregate ? { ...stmt, columns: ['*'] } : stmt);
// v0.4.0 修复: 关联子查询需要完整外层行(SELECT 列可能不含被 $col 引用的列,如 EXISTS 绑定的主键)
plan.columns = ['*'];
if (orderByAlias) {
plan.orderBy = undefined;
plan.limit = undefined;
plan.offset = undefined;
}
rows = await this.engine.find(plan.table, { ...plan, where: this.stripCorrelatedExists(stmt.where) });
rows = await this.filterCorrelated(rows, stmt.where);
}
else {
// 先解析子查询
if (stmt.where && Object.keys(stmt.where).length > 0) {
stmt.where = await this.resolveSubqueries(stmt.where);
}
const plan = compileStatement(hasGroupBy || hasAggregate ? { ...stmt, columns: ['*'] } : stmt);
if (needsRawRows || hasSelectAlias)
plan.columns = ['*'];
if (orderByAlias) {
plan.orderBy = undefined;
plan.limit = undefined;
plan.offset = undefined;
}
rows = await this.engine.find(plan.table, plan);
}
}
// 无 GROUP BY 但有聚合 → 计算单行聚合结果
if (hasAggregate) {
rows = [this.computeSingleAggregate(rows, stmt)];
}
if (hasGroupBy)
rows = this.executeGroupBy(rows, stmt);
if (stmt.distinct)
rows = this.executeDistinct(rows);
if (stmt.having && Object.keys(stmt.having).length > 0) {
// v0.4.0 修复: HAVING 中的标量子查询(HAVING SUM(o.amount) > (SELECT AVG(...)))需先解析
stmt.having = await this.resolveSubqueries(stmt.having);
// v0.4.0: HAVING 引用聚合表达式键(如 SUM(o.amount))时归一为别名键(如 spent)
const aliasMap = stmt._aggAliasMap;
if (aliasMap && aliasMap.size > 0) {
const normalized = {};
for (const [k, v] of Object.entries(stmt.having)) {
normalized[aliasMap.get(k) ?? k] = v;
}
stmt.having = normalized;
}
rows = rows.filter((row) => matchWhere(row, stmt.having));
}
if (stmt.orderBy && stmt.orderBy.length > 0)
rows = applyOrderBy(rows, stmt.orderBy);
if (!hasGroupBy && !hasAggregate && stmt.columns.length > 0 && stmt.columns[0] !== '*') {
rows = rows.map((row) => this.projectRow(row, stmt.columns));
}
// v0.3.3: ORDER BY 别名 → 投影后才存在,需在投影后重新排序
if (orderByAlias && stmt.orderBy && stmt.orderBy.length > 0) {
rows = applyOrderBy(rows, stmt.orderBy);
}
const offset = stmt.offset ?? 0;
const limit = stmt.limit ?? rows.length;
rows = rows.slice(offset, offset + limit);
// 全局行数上限保护
if (this.maxRowsPerQuery > 0 && rows.length > this.maxRowsPerQuery) {
rows = rows.slice(0, this.maxRowsPerQuery);
}
return rows;
}
// ---- JOIN ----
async executeJoinSelect(stmt, preloadedMain) {
const mainAlias = stmt.alias ?? stmt.from;
// v0.4.0: 派生表行源已预加载(行带别名前缀)
let mainRows;
if (preloadedMain) {
mainRows = preloadedMain;
}
else {
// v0.4.1: WHERE 中主表前缀等值条件下推到引擎(走二级索引,如 WHERE o.user_id = '1'
const { pushable } = this.extractPushableWhere(stmt.where ?? {}, mainAlias);
mainRows = (await this.engine.find(stmt.from, {
table: stmt.from,
where: Object.keys(pushable).length > 0 ? pushable : undefined,
})).map((row) => this.prefixRow(row, mainAlias));
}
let resultRows = mainRows;
for (const join of stmt.joins) {
const joinAlias = join.alias ?? join.table;
// v0.3.2: 等值 ON + 右列主键 → 哈希连接(一次 $in 查询替代嵌套循环)
const hashJoined = await this.tryHashJoin(resultRows, join, joinAlias, mainAlias);
if (hashJoined) {
resultRows = hashJoined;
continue;
}
const joinRows = (await this.engine.find(join.table, { table: join.table }))
.map((row) => this.prefixRow(row, joinAlias));
resultRows = this.joinRows(resultRows, joinRows, join);
}
if (stmt.where && Object.keys(stmt.where).length > 0) {
// v0.3.1: 关联子查询($col/EXISTS 引用外层行)→ 逐行绑定求值
if (this.hasCorrelatedRefs(stmt.where)) {
resultRows = await this.filterCorrelated(resultRows, stmt.where);
}
else {
// 非关联子查询(IN (SELECT ...) 等),字段名保持别名前缀
stmt.where = await this.resolveSubqueries(stmt.where);
resultRows = resultRows.filter((row) => matchWhere(row, stmt.where));
}
}
return resultRows;
}
prefixRow(row, alias) {
const prefixed = {};
for (const [key, value] of Object.entries(row))
prefixed[`${alias}.${key}`] = value;
return prefixed;
}
/**
* v0.4.1: 提取可下推的 WHERE 条件 — 主表别名前缀的普通条件(如 o.user_id = '1')。
* 下推到引擎可走二级索引;$col/$subquery/$and/$or/$not 等复杂条件保守不下推。
*/
extractPushableWhere(where, mainAlias) {
const pushable = {};
if (!mainAlias)
return { pushable };
const prefix = `${mainAlias}.`;
for (const [key, value] of Object.entries(where)) {
if (!key.startsWith(prefix))
continue;
const v = value;
if (typeof v === 'object' && v !== null &&
('$col' in v || '$subquery' in v || '$and' in v || '$or' in v || '$not' in v)) {
continue;
}
pushable[key.slice(prefix.length)] = value;
}
return { pushable };
}
/**
* 哈希连接(v0.3.2 单等值 / v0.4.0 多列等值):
* ON 为等值条件(单列或多列)且右表任一列为索引/主键时,
* 收集左表连接值 → 一次 $in 查询右表 → 哈希映射匹配。
* 替代嵌套循环,大表 INNER/LEFT JOIN 复杂度 O(N + M)。
* 不适用时返回 null(回退嵌套循环)。
*/
async tryHashJoin(leftRows, join, joinAlias, mainAlias) {
if (join.type === 'CROSS' || join.type === 'RIGHT')
return null;
// 解析 ON 为 (leftCol, rightCol) 等值对列表(v0.4.0 支持多列,含顶层 $and 展开)
const pairs = [];
const collectPairs = (on) => {
for (const [keyCol, cond] of Object.entries(on)) {
if (keyCol === '$and') {
if (!cond.every(collectPairs))
return false;
continue;
}
if (keyCol === '$or' || keyCol === '$not')
return false; // 非等值逻辑不适用
let refCol = null;
if (typeof cond === 'object' && cond !== null) {
const c = cond;
if ('$eq' in c && typeof c.$eq === 'object' && c.$eq !== null && '$col' in c.$eq) {
refCol = String(c.$eq.$col);
}
else if ('$col' in c && Object.keys(c).length === 1) {
refCol = String(c.$col);
}
}
if (!refCol)
return false; // 非等值条件不适用哈希连接
const keyIsLeft = mainAlias ? keyCol.startsWith(`${mainAlias}.`) : false;
pairs.push({
leftCol: keyIsLeft ? keyCol : refCol,
rightCol: keyIsLeft ? refCol : keyCol,
});
}
return true;
};
if (!collectPairs(join.on))
return null;
if (pairs.length === 0)
return null;
// 右表列必须是主键/索引列(确保 $in 走索引)——任一列即可
const schema = await this.engine.getTableSchema(join.table);
if (!schema)
return null;
const probePair = pairs.find((p) => {
const bare = p.rightCol.split('.').pop();
const colDef = schema.columns[bare];
return colDef && (colDef.primaryKey || colDef.index || colDef.unique);
});
if (!probePair)
return null;
// 收集左表连接值(去重)——用探测列的值缩小候选集
const probeRightBare = probePair.rightCol.split('.').pop();
const values = Array.from(new Set(leftRows.map((r) => r[probePair.leftCol]).filter((v) => v !== undefined && v !== null)));
if (values.length === 0)
return null;
// 一次 $in 查询右表(缩小候选集)
const rightRows = await this.engine.find(join.table, {
table: join.table,
where: { [probeRightBare]: { $in: values } },
});
// 构建复合键哈希映射:右表多列值 → 行列表
const hash = new Map();
for (const rr of rightRows) {
const key = pairs.map((p) => String(rr[p.rightCol.split('.').pop()] ?? '\0')).join('\x1f');
if (!hash.has(key))
hash.set(key, []);
hash.get(key).push(rr);
}
const nullRight = {};
for (const key of Object.keys(schema.columns))
nullRight[key] = null;
const result = [];
for (const l of leftRows) {
const key = pairs.map((p) => String(l[p.leftCol] ?? '\0')).join('\x1f');
const matches = hash.get(key);
if (matches && matches.length > 0) {
for (const r of matches) {
result.push({ ...l, ...this.prefixRow(r, joinAlias) });
}
}
else if (join.type === 'LEFT') {
// LEFT JOIN 无匹配 → 右表列置 null
result.push({ ...l, ...this.prefixRow(nullRight, joinAlias) });
}
// INNER JOIN 无匹配 → 跳过
}
return result;
}
/** 嵌套循环连接(优化:避免 ON 时对象扩散) */
joinRows(leftRows, rightRows, join) {
if (join.type === 'CROSS') {
const result = [];
for (const l of leftRows)
for (const r of rightRows)
result.push({ ...l, ...r });
return result;
}
const result = [];
for (const l of leftRows) {
let matched = false;
for (const r of rightRows) {
// 合并后匹配 ON(避免创建临时对象再丢弃)
const merged = { ...l, ...r };
if (matchWhere(merged, join.on, { $col: true })) {
result.push(merged);
matched = true;
}
}
if (!matched && join.type === 'LEFT') {
const nullRight = {};
for (const key of Object.keys(rightRows[0] ?? {}))
nullRight[key] = null;
result.push({ ...l, ...nullRight });
}
}
if (join.type === 'RIGHT') {
for (const r of rightRows) {
const isMatched = leftRows.some((l) => {
const merged = { ...l, ...r };
return matchWhere(merged, join.on, { $col: true });
});
if (!isMatched) {
const nullLeft = {};
for (const key of Object.keys(leftRows[0] ?? {}))
nullLeft[key] = null;
result.push({ ...nullLeft, ...r });
}
}
}
return result;
}
// ---- GROUP BY ----
executeGroupBy(rows, stmt) {
const groups = new Map();
for (const row of rows) {
const key = stmt.groupBy.map((col) => String(row[col] ?? 'null')).join('|');
if (!groups.has(key))
groups.set(key, []);
groups.get(key).push(row);
}
const result = [];
// v0.4.0: 聚合表达式键 → 输出键 映射(HAVING SUM(...) 引用表达式时归一为别名键)
const aliasMap = new Map();
for (const groupRows of groups.values()) {
const aggregated = {};
for (const col of stmt.groupBy)
aggregated[col] = groupRows[0][col];
for (const colExpr of stmt.columns) {
if (colExpr === '*')
continue;
const m = colExpr.match(/^(COUNT|SUM|AVG|MIN|MAX)\((.+?)\)(?:\s+AS\s+(\w+))?$/i);
if (m) {
const [, func, arg, alias] = m;
const value = this.computeAggregate(func.toUpperCase(), groupRows, arg.trim());
const exprKey = `${func.toUpperCase()}(${arg.trim()})`;
const outputKey = alias || colExpr;
if (outputKey !== exprKey)
aliasMap.set(exprKey, outputKey);
aggregated[outputKey] = value;
}
else if (/^\s*CASE\b/i.test(colExpr)) {
// v0.3.2: 非聚合的 CASE WHEN 列取组内第一行求值
const expr = parseCaseExpression(colExpr);
aggregated[expr?.alias ?? colExpr] = expr ? evaluateCase(expr, groupRows[0]) : null;
}
else if (!stmt.groupBy.includes(colExpr)) {
aggregated[colExpr] = groupRows[0][colExpr];
}
}
result.push(aggregated);
}
stmt._aggAliasMap = aliasMap;
return result;
}
computeAggregate(func, rows, col) {
// v0.3.2: 聚合参数支持 CASE WHEN 表达式(如 SUM(CASE WHEN age > 18 THEN 1 ELSE 0 END)
const caseExpr = /^\s*CASE\b/i.test(col) ? parseCaseExpression(col) : null;
// v0.4.0: COUNT(DISTINCT col) 等去重聚合
const distinctArg = !caseExpr && /^\s*DISTINCT\s+/i.test(col);
const argCol = distinctArg ? col.replace(/^\s*DISTINCT\s+/i, '').trim() : col;
const rawValues = rows
.map((r) => (caseExpr ? evaluateCase(caseExpr, r) : r[argCol]))
.filter((v) => v !== null && v !== undefined);
// v0.4.0: COUNT 对原始值去重(任意类型);数值聚合在类型转换后去重
if (func === 'COUNT') {
if (argCol === '*')
return rows.length;
if (distinctArg) {
return new Set(rawValues.map((v) => (typeof v === 'object' ? JSON.stringify(v) : String(v)))).size;
}
return rawValues.length;
}
const nums = rawValues.map(Number);
const distinctNums = distinctArg ? Array.from(new Set(nums)) : nums;
switch (func) {
case 'SUM': return distinctNums.reduce((a, b) => a + b, 0);
case 'AVG': return distinctNums.length === 0 ? 0 : distinctNums.reduce((a, b) => a + b, 0) / distinctNums.length;
case 'MIN': return distinctNums.length === 0 ? 0 : Math.min(...distinctNums);
case 'MAX': return distinctNums.length === 0 ? 0 : Math.max(...distinctNums);
default: return 0;
}
}
// ---- DISTINCT(优化:列值拼接代替 JSON.stringify ----
executeDistinct(rows) {
const seen = new Set();
return rows.filter((row) => {
const key = Object.values(row).map((v) => String(v ?? '\0')).join('\x1f');
if (seen.has(key))
return false;
seen.add(key);
return true;
});
}
// ===================================================================
// 其他语句
// ===================================================================
async executeInsert(stmt) {
const schema = await this.engine.getTableSchema(stmt.into);
if (!schema)
throw new DatabaseError(`Table "${stmt.into}" does not exist`, 'TABLE_NOT_FOUND');
const colNames = stmt.columns ?? Object.keys(schema.columns);
// INSERT INTO ... SELECT ...v0.3.0
if (stmt.select) {
const selectRows = await this.executeSelectPart(stmt.select);
// v0.4.0 修复:源列顺序不能依赖行键(validateRow 会跳过 undefined 导致行键缺失/乱序)。
// 以 SELECT 列列表 / 源表 schema 列顺序为准,按位置对齐目标列,缺列不填。
let srcCols = [];
const sel = stmt.select;
if (sel.type === 'SELECT') {
if (sel.columns && sel.columns.length > 0 && sel.columns[0] !== '*') {
srcCols = sel.columns.map((c) => c.split('.').pop());
}
else if (sel.from) {
const srcSchema = await this.engine.getTableSchema(sel.from);
srcCols = srcSchema ? Object.keys(srcSchema.columns) : [];
}
}
if (srcCols.length === 0 && selectRows.length > 0) {
srcCols = Object.keys(selectRows[0]);
}
const rows = selectRows.map((row) => {
const mapped = {};
for (let i = 0; i < colNames.length; i++) {
const src = i < srcCols.length ? srcCols[i] : null;
if (src && src in row)
mapped[colNames[i]] = row[src];
}
return mapped;
});
return this.engine.insert(stmt.into, rows);
}
const rows = (stmt.values ?? []).map((vals) => {
const row = {};
for (let i = 0; i < colNames.length; i++) {
if (i < vals.length)
row[colNames[i]] = vals[i];
}
return row;
});
return this.engine.insert(stmt.into, rows);
}
async executeUpdate(stmt) {
const plan = compileStatement(stmt);
return this.engine.update(plan.table, plan, stmt.sets);
}
async executeDelete(stmt) {
const plan = compileStatement(stmt);
return this.engine.delete(plan.table, plan);
}
async executeCreateTable(stmt) {
// IF NOT EXISTS: 表已存在时静默返回
if (stmt.ifNotExists) {
const exists = await this.engine.hasTable(stmt.name);
if (exists)
return;
}
const columns = {};
for (const col of stmt.columns)
columns[col.name] = astColumnToColumnDef(col);
return this.engine.createTable(createSchema(stmt.name, columns));
}
async executeDropTable(stmt) {
if (stmt.ifExists) {
const exists = await this.engine.hasTable(stmt.name);
if (!exists)
return; // IF EXISTS: 表不存在时静默返回
}
return this.engine.dropTable(stmt.name);
}
async executeAlterTable(stmt) {
const exists = await this.engine.hasTable(stmt.name);
if (!exists)
throw new DatabaseError(`Table "${stmt.name}" does not exist`, 'TABLE_NOT_FOUND');
const schema = await this.engine.getTableSchema(stmt.name);
if (!schema)
return;
// v0.4.1: 引擎级 alterTableAria 需重写存储行 + 持久化 schema;其余引擎走通用引用路径)
if (typeof this.engine.alterTable === 'function') {
return this.engine.alterTable(stmt.name, stmt.action, { ...astColumnToColumnDef(stmt.column), name: stmt.column.name });
}
if (stmt.action === 'ADD') {
if (schema.columns[stmt.column.name]) {
throw new DatabaseError(`Column "${stmt.column.name}" already exists in table "${stmt.name}"`, 'COLUMN_EXISTS');
}
// 直接在 schema 引用上添加列(已有行的该列值为 undefined/default
schema.columns[stmt.column.name] = astColumnToColumnDef(stmt.column);
}
else if (stmt.action === 'DROP') {
if (!schema.columns[stmt.column.name]) {
throw new DatabaseError(`Column "${stmt.column.name}" does not exist in table "${stmt.name}"`, 'COLUMN_NOT_FOUND');
}
// 从 schema 引用上删除列定义(保留所有行数据)
delete schema.columns[stmt.column.name];
// 清除已有行中该列的值(MemoryEngine 的 find 返回引用,delete 直接生效)
const rows = await this.engine.find(stmt.name, { table: stmt.name });
const colName = stmt.column.name;
for (const row of rows) {
if (colName in row)
delete row[colName];
}
}
}
async executeTruncateTable(stmt) {
const exists = await this.engine.hasTable(stmt.name);
if (!exists)
throw new DatabaseError(`Table "${stmt.name}" does not exist`, 'TABLE_NOT_FOUND');
return this.engine.clear(stmt.name);
}
// ===================================================================
// CREATE INDEX / DROP INDEXv0.3.0
// ===================================================================
async executeCreateIndex(stmt) {
const exists = await this.engine.hasTable(stmt.table);
if (!exists)
throw new DatabaseError(`Table "${stmt.table}" does not exist`, 'TABLE_NOT_FOUND');
const schema = await this.engine.getTableSchema(stmt.table);
if (schema && !schema.columns[stmt.column]) {
throw new DatabaseError(`Column "${stmt.column}" does not exist in table "${stmt.table}"`, 'COLUMN_NOT_FOUND');
}
if (typeof this.engine.createIndex !== 'function') {
throw new DatabaseError(`Engine "${this.engine.name}" does not support CREATE INDEX`, 'NOT_SUPPORTED');
}
return this.engine.createIndex(stmt.table, stmt.column, stmt.unique);
}
async executeDropIndex(stmt) {
if (typeof this.engine.dropIndex !== 'function') {
throw new DatabaseError(`Engine "${this.engine.name}" does not support DROP INDEX`, 'NOT_SUPPORTED');
}
return this.engine.dropIndex(stmt.table, stmt.column, stmt.name);
}
// ===================================================================
// 事务语句(v0.3.0
// ===================================================================
async executeBegin() {
return this.engine.beginTransaction();
}
async executeCommit() {
return this.engine.commitTransaction();
}
async executeRollback() {
return this.engine.rollbackTransaction();
}
/** 列列表是否包含 CASE WHEN 表达式 */
hasCaseColumn(columns) {
return columns.some((col) => /^\s*CASE\b/i.test(col));
}
/**
* v0.3.3: ORDER BY 是否引用 SELECT 别名(如 `SELECT name AS n ... ORDER BY n`)。
* 别名列在引擎层投影前不存在,需投影后重新排序。
*/
orderByUsesSelectAlias(stmt) {
if (!stmt.orderBy || stmt.orderBy.length === 0)
return false;
const aliases = new Set();
for (const col of stmt.columns) {
const m = col.match(/\s+AS\s+(\w+)$/i);
if (m)
aliases.add(m[1]);
else if (/^\s*CASE\b/i.test(col)) {
const expr = parseCaseExpression(col);
if (expr?.alias)
aliases.add(expr.alias);
}
}
if (aliases.size === 0)
return false;
return stmt.orderBy.some((o) => aliases.has(o.column));
}
/** WHERE 是否包含 CASE WHEN 表达式键 */
whereHasCase(where) {
for (const [key, value] of Object.entries(where)) {
if (key === '$and' || key === '$or') {
if (value.some((sub) => this.whereHasCase(sub)))
return true;
continue;
}
if (key === '$not') {
if (this.whereHasCase(value))
return true;
continue;
}
if (/^\s*CASE\b/i.test(key))
return true;
}
return false;
}
getEngine() { return this.engine; }
/**
* 列投影(v0.3.1):普通列走 projectColumnsCASE WHEN 表达式逐行求值;
* v0.3.3: 支持 `col AS alias` 列别名
*/
projectRow(row, columns) {
const plain = [];
const aliasCols = [];
const caseCols = [];
const constCols = [];
for (const col of columns) {
if (col === '*')
continue;
const expr = parseCaseExpression(col);
if (expr) {
caseCols.push({ alias: expr.alias ?? col, expr });
continue;
}
const m = col.match(/^(.+?)\s+AS\s+(\w+)$/i);
if (m) {
aliasCols.push({ alias: m[2], source: m[1].trim() });
continue;
}
// v0.4.0: 字符串常量列 SELECT 'lit' → 常量输出
const lit = col.match(/^'(.*)'$/s);
if (lit) {
const value = lit[1].replace(/\\'/g, "'");
constCols.push({ key: col, value });
continue;
}
plain.push(col);
}
const projected = plain.length > 0 ? projectColumns(row, plain) : {};
for (const { alias, source } of aliasCols) {
if (source === '*') {
Object.assign(projected, row);
}
else {
const lit = source.match(/^'(.*)'$/s);
projected[alias] = lit ? lit[1].replace(/\\'/g, "'") : row[source];
}
}
for (const { key, value } of constCols) {
projected[key] = value;
}
for (const { alias, expr } of caseCols) {
projected[alias] = evaluateCase(expr, row);
}
return projected;
}
// ===================================================================
// 无 GROUP BY 时的聚合计算
// ===================================================================
/** 检查 SELECT 列列表中是否包含聚合函数 */
_hasAggregateColumn(columns) {
return columns.some((col) => /^(COUNT|SUM|AVG|MIN|MAX)\(/i.test(col));
}
/** 计算单行聚合结果(无 GROUP BY) */
computeSingleAggregate(rows, stmt) {
const result = {};
for (const colExpr of stmt.columns) {
if (colExpr === '*')
continue;
const m = colExpr.match(/^(COUNT|SUM|AVG|MIN|MAX)\((.+?)\)(?:\s+AS\s+(\w+))?$/i);
if (m) {
const [, func, arg, alias] = m;
result[alias || colExpr] = this.computeAggregate(func.toUpperCase(), rows, arg.trim());
}
else {
// 非聚合列取第一行的值
result[colExpr] = rows.length > 0 ? rows[0][colExpr] : null;
}
}
return result;
}
// ===================================================================
// 关联子查询 / 别名规范化(v0.3.0)
// ===================================================================
/** 剥离主表别名前缀:'u.id' → 'id'(键与 $col 值均处理,支持多层别名) */
normalizeWhereColumns(where, aliases) {
const normalized = {};
for (const [key, value] of Object.entries(where)) {
if (key === '$and' || key === '$or') {
normalized[key] = value.map((sub) => this.normalizeWhereColumns(sub, aliases));
continue;
}
if (key === '$not') {
normalized.$not = this.normalizeWhereColumns(value, aliases);
continue;
}
if (key === '$exists') {
normalized[key] = this.normalizeExistsValue(value, aliases);
continue;
}
const newKey = this.stripAlias(key, aliases);
normalized[newKey] = this.normalizeFieldValue(value, aliases);
}
return normalized;
}
normalizeExistsValue(value, aliases) {
if (typeof value !== 'object' || value === null)
return value;
const v = value;
if (v.$subquery) {
const sub = v.$subquery;
// 子查询 where 需同时识别:子查询自身别名 + 外层别名(关联引用)
const subAliases = [sub.alias ?? sub.from, ...aliases].filter(Boolean);
return { ...v, $subquery: { ...sub, where: this.normalizeWhereColumns(sub.where, subAliases) } };
}
return value;
}
normalizeFieldValue(value, aliases) {
if (typeof value !== 'object' || value === null || Array.isArray(value))
return value;
const ops = {};
for (const [op, operand] of Object.entries(value)) {
if (op === '$and' || op === '$or') {
ops[op] = operand.map((sub) => this.normalizeWhereColumns(sub, aliases));
}
else if (op === '$not' && typeof operand === 'object' && operand !== null) {
ops[op] = this.normalizeFieldValue(operand, aliases);
}
else if (op === '$col') {
ops[op] = this.stripAlias(String(operand), aliases);
}
else if (typeof operand === 'object' && operand !== null && !Array.isArray(operand) && '$col' in operand) {
// 操作符值中嵌套的列引用:{ $eq: { $col: 'u.id' } }
ops[op] = { $col: this.stripAlias(String(operand.$col), aliases) };
}
else {
ops[op] = operand;
}
}
return ops;
}
stripAlias(col, aliases) {
for (const alias of aliases) {
if (!alias)
continue;
const prefix = `${alias}.`;
if (col.startsWith(prefix))
return col.slice(prefix.length);
}
return col;
}
/** WHERE 是否含关联引用($col 或关联 EXISTS)或 CASE WHEN 表达式键 */
hasCorrelatedRefs(where) {
for (const [key, value] of Object.entries(where)) {
if (key === '$and' || key === '$or') {
if (value.some((sub) => this.hasCorrelatedRefs(sub)))
return true;
continue;
}
if (key === '$not') {
if (this.hasCorrelatedRefs(value))
return true;
continue;
}
if (key === '$exists') {
// 关联 EXISTS:子查询 where 含 $col 或主 where 含 $negate 未解析标记
if (typeof value === 'object' && value !== null && '$subquery' in value) {
return true; // 关联 EXISTS 统一走逐行求值
}
continue;
}
// v0.3.2: CASE WHEN 表达式键(逐行求值)
if (/^\s*CASE\b/i.test(key))
return true;
if (this.fieldHasColRef(value))
return true;
}
return false;
}
fieldHasColRef(value) {
if (typeof value !== 'object' || value === null || Array.isArray(value))
return false;
const ops = value;
if ('$col' in ops)
return true;
if ('$and' in ops || '$or' in ops) {
const subs = (ops.$and ?? ops.$or);
return subs.some((sub) => this.hasCorrelatedRefs(sub));
}
if ('$not' in ops && typeof ops.$not === 'object' && ops.$not !== null) {
return this.fieldHasColRef(ops.$not);
}
// 操作符值中嵌套的列引用:{ $eq: { $col: 'id' } }
for (const [, operand] of Object.entries(ops)) {
if (typeof operand === 'object' && operand !== null && !Array.isArray(operand)) {
if ('$col' in operand)
return true;
if (this.fieldHasColRef(operand))
return true;
}
}
return false;
}
/** 移除关联 EXISTS 标记(引擎层先执行无 EXISTS 条件的查询) */
stripCorrelatedExists(where) {
const cleaned = {};
for (const [key, value] of Object.entries(where)) {
if (key === '$and' || key === '$or') {
cleaned[key] = value.map((sub) => this.stripCorrelatedExists(sub));
continue;
}
if (key === '$not') {
const inner = this.stripCorrelatedExists(value);
// 剥离后为空 → 条件恒真,删掉该键(避免引擎层执行 NOT(true) 过滤掉所有行)
if (Object.keys(inner).length > 0)
cleaned.$not = inner;
continue;
}
if (key === '$exists')
continue; // 逐行求值时单独处理
if (/^\s*CASE\b/i.test(key))
continue; // v0.3.2: CASE 键逐行求值
cleaned[key] = value;
}
return cleaned;
}
/** 逐行绑定外层行上下文,求值关联 EXISTS、$col 引用与 CASE WHEN 键 */
async filterCorrelated(rows, where) {
const result = [];
for (const row of rows) {
// 1. CASE WHEN 键 → 布尔条件(同步)
let rowWhere = this.resolveCaseKeys(where, row);
// 2. $col 绑定 + 关联 EXISTS 求值(异步)
rowWhere = await this.resolveSubqueries(rowWhere, row);
if (matchWhere(row, rowWhere)) {
result.push(row);
}
}
return result;
}
/** 将 WHERE 中的 CASE WHEN 表达式键求值为布尔条件($caseResult */
resolveCaseKeys(where, row) {
const resolved = {};
for (const [key, value] of Object.entries(where)) {
if (key === '$and' || key === '$or') {
resolved[key] = value.map((sub) => this.resolveCaseKeys(sub, row));
continue;
}
if (key === '$not') {
resolved.$not = this.resolveCaseKeys(value, row);
continue;
}
if (/^\s*CASE\b/i.test(key)) {
const expr = parseCaseExpression(key);
if (!expr)
continue; // 解析失败视为不满足
const val = evaluateCase(expr, row);
if (this.caseConditionMatches(val, value)) {
resolved.$caseResult = true;
}
else {
return { $caseResult: false };
}
continue;
}
resolved[key] = value;
}
return resolved;
}
/** CASE 求值结果与操作符条件比较 */
caseConditionMatches(val, condition) {
if (typeof condition !== 'object' || condition === null || Array.isArray(condition)) {
return val === condition;
}
const ops = condition;
for (const [op, operand] of Object.entries(ops)) {
switch (op) {
case '$eq':
if (val !== operand)
return false;
break;
case '$ne':
if (val === operand)
return false;
break;
case '$gt':
if (!(val > operand))
return false;
break;
case '$gte':
if (!(val >= operand))
return false;
break;
case '$lt':
if (!(val < operand))
return false;
break;
case '$lte':
if (!(val <= operand))
return false;
break;
case '$in':
if (!(Array.isArray(operand) && operand.includes(val)))
return false;
break;
case '$nin':
if (Array.isArray(operand) && operand.includes(val))
return false;
break;
}
}
return true;
}
/** 将 where 中的 $col 引用替换为上下文行值 */
bindColumnRefs(value, contextRow) {
if (typeof value !== 'object' || value === null || Array.isArray(value))
return value;
const ops = {};
for (const [op, operand] of Object.entries(value)) {
if (op === '$col') {
ops[op] = contextRow[String(operand)] ?? null;
}
else if (op === '$and' || op === '$or') {
ops[op] = operand.map((sub) => this.bindWhereRefs(sub, contextRow));
}
else if (op === '$not' && typeof operand === 'object' && operand !== null) {
ops[op] = this.bindColumnRefs(operand, contextRow);
}
else if (typeof operand === 'object' && operand !== null && !Array.isArray(operand) && '$col' in operand) {
// 操作符值中嵌套的列引用:{ $eq: { $col: 'id' } } → { $eq: row['id'] }
ops[op] = contextRow[String(operand.$col)] ?? null;
}
else {
ops[op] = operand;
}
}
return ops;
}
bindWhereRefs(where, contextRow) {
const bound = {};
for (const [key, value] of Object.entries(where)) {
if (key === '$and' || key === '$or') {
bound[key] = value.map((sub) => this.bindWhereRefs(sub, contextRow));
}
else if (key === '$not') {
bound.$not = this.bindWhereRefs(value, contextRow);
}
else if (key === '$exists') {
bound[key] = value;
}
else {
bound[key] = this.bindColumnRefs(value, contextRow);
}
}
return bound;
}
// ===================================================================
// 子查询解析
// ===================================================================
/**
* 递归扫描 WHERE 条件,找到 $subquery 标记并执行子查询,
* 将结果替换为具体值。
* @param contextRow 关联子查询的外层行上下文(用于绑定 $col 引用)
*/
async resolveSubqueries(where, contextRow) {
// 关联上下文:先把字段级的 $col 引用绑定为外层行值
if (contextRow) {
where = this.bindWhereRefs(where, contextRow);
}
const resolved = {};
for (const [key, value] of Object.entries(where)) {
// 顶层 $existsv0.3.0):执行子查询并解析为 boolean,由 where-matcher 消费
if (key === '$exists' && typeof value === 'object' && value !== null) {
const v = value;
const sub = v.$subquery;
const negate = !!v.$negate;
let subWhere = sub.where;
// 子查询内的关联引用(如 o.user_id = u.id 中的 u.id)绑定外层行
if (this.hasCorrelatedRefs(subWhere)) {
subWhere = this.bindWhereRefs(subWhere, contextRow ?? {});
}
const rows = await this.executeSelectPart({ ...sub, where: subWhere });
resolved.$exists = rows.length > 0 !== negate;
continue;
}
// 逻辑组合操作符
if (key === '$and' && Array.isArray(value)) {
resolved.$and = await Promise.all(value.map((sub) => this.resolveSubqueries(sub, contextRow)));
continue;
}
if (key === '$or' && Array.isArray(value)) {
resolved.$or = await Promise.all(value.map((sub) => this.resolveSubqueries(sub, contextRow)));
continue;
}
if (key === '$not' && typeof value === 'object' && value !== null) {
resolved.$not = await this.resolveSubqueries(value, contextRow);
continue;
}
// 字段条件
if (typeof value === 'object' && value !== null) {
resolved[key] = await this.resolveOperatorSubqueries(value);
}
else {
resolved[key] = value;
}
}
return resolved;
}
/**
* 解析操作符值中嵌套的子查询
*/
async resolveOperatorSubqueries(ops) {
const resolved = {};
for (const [op, operand] of Object.entries(ops)) {
// 处理嵌套 $and/$or(在字段级条件中)
if (op === '$and' && Array.isArray(operand)) {
resolved.$and = await Promise.all(operand.map((sub) => this.resolveSubqueries(sub)));
continue;
}
if (op === '$or' && Array.isArray(operand)) {
resolved.$or = await Promise.all(operand.map((sub) => this.resolveSubqueries(sub)));
continue;
}
if (op === '$not') {
resolved.$not = typeof operand === 'object' && operand !== null
? await this.resolveOperatorSubqueries(operand)
: operand;
continue;
}
// 子查询检测
if (typeof operand === 'object' && operand !== null && '$subquery' in operand) {
const subStmt = operand.$subquery;
const subResult = await this.executeSelect(subStmt);
if (op === '$in' || op === '$nin') {
// IN 子查询 → 提取第一列的值列表
const colName = Object.keys(subResult[0] || {})[0];
const values = subResult.map((row) => row[colName]);
resolved[op] = values;
}
else {
// 标量子查询 → 取第一行第一列
if (subResult.length === 0) {
resolved[op] = null;
}
else {
const colName = Object.keys(subResult[0])[0];
resolved[op] = subResult[0][colName];
}
}
}
else {
resolved[op] = operand;
}
}
return resolved;
}
}
/**
* metona-sqlark Transaction — 事务管理
* @module transaction
*
* v0.1.13: 支持真正的回滚 — 利用引擎层 begin/commit/rollback 实现原子性。
*/
// ---------------------------------------------------------------------------
// Transaction
// ---------------------------------------------------------------------------
class Transaction {
constructor(engine) {
this.tables = new Map();
this.completed = false;
this.engine = engine;
}
/** 获取表操作对象 */
table(tableName) {
let t = this.tables.get(tableName);
if (!t) {
t = new Table(this.engine, tableName);
this.tables.set(tableName, t);
}
return t;
}
/** 标记事务完成(由 TransactionManager 调用) */
_markCompleted() {
this.completed = true;
}
/** 是否已完成 */
isCompleted() {
return this.completed;
}
}
// ---------------------------------------------------------------------------
// TransactionManager
// ---------------------------------------------------------------------------
class TransactionManager {
constructor(engine) {
this.engine = engine;
}
/** 执行事务 — 支持自动回滚 */
async execute(fn) {
const trx = new Transaction(this.engine);
// 开始引擎层事务
await this.engine.beginTransaction();
try {
const result = await fn(trx);
// 成功 → 提交
await this.engine.commitTransaction();
trx._markCompleted();
return result;
}
catch (error) {
// 失败 → 回滚
await this.engine.rollbackTransaction();
if (error instanceof DatabaseError)
throw error;
throw new DatabaseError(`Transaction failed: ${error.message}`, 'TRANSACTION_ERROR', error);
}
}
}
/**
* metona-sqlark Plugin — 插件系统
* @module plugin
*
* 管理插件的注册、生命周期和钩子调度。
*/
// ---------------------------------------------------------------------------
// PluginManager
// ---------------------------------------------------------------------------
class PluginManager {
constructor() {
this.plugins = [];
this.hooks = new Map();
}
/** 注册插件 */
register(plugin, db) {
// 按优先级插入
const priority = plugin.priority ?? 0;
const insertIndex = this.plugins.findIndex((p) => (p.priority ?? 0) < priority);
if (insertIndex === -1) {
this.plugins.push(plugin);
}
else {
this.plugins.splice(insertIndex, 0, plugin);
}
// 安装(传入 db 实例)
plugin.install(db);
}
/** 卸载插件 */
unregister(pluginName) {
const idx = this.plugins.findIndex((p) => p.name === pluginName);
if (idx !== -1) {
this.plugins[idx].destroy();
this.plugins.splice(idx, 1);
}
}
/** 获取所有已注册插件 */
getPlugins() {
return [...this.plugins];
}
/** 添加钩子回调 */
on(hook, callback) {
const callbacks = this.hooks.get(hook) ?? [];
callbacks.push(callback);
this.hooks.set(hook, callbacks);
}
/** 移除钩子回调 */
off(hook, callback) {
const callbacks = this.hooks.get(hook);
if (callbacks) {
const idx = callbacks.indexOf(callback);
if (idx !== -1)
callbacks.splice(idx, 1);
}
}
/** 触发钩子 */
async trigger(hook, ...args) {
const callbacks = this.hooks.get(hook);
if (callbacks) {
for (const cb of callbacks) {
await cb(...args);
}
}
}
/** 销毁所有插件 */
destroy() {
for (const plugin of this.plugins) {
try {
plugin.destroy();
}
catch (e) {
// eslint-disable-next-line no-console
console.warn(`[metona-sqlark] Plugin "${plugin.name}" destroy error:`, e);
}
}
this.plugins = [];
this.hooks.clear();
}
}
/**
* metona-sqlark Core — 数据库主类
* @module core
*
* 管理数据库生命周期、引擎调度、表操作、SQL 查询、事务和插件。
*/
// ---------------------------------------------------------------------------
// MetonaSqlark
// ---------------------------------------------------------------------------
class MetonaSqlark {
/** 获取版本号 */
get version() { return this._version; }
/** 查询结果行数上限 */
get maxRowsPerQuery() { return this.config.maxRowsPerQuery ?? 0; }
/** 调试模式 */
get debug() { return this.config.debug ?? false; }
constructor(config) {
this.ready = false;
this.tableCache = new Map();
/** 多标签页同步通道(v0.3.2 */
this.channel = null;
// ---- 发布订阅 ----
this.listeners = new Map();
// ---- 迁移 ----
this.migrations = new Map();
this.config = config;
this.name = config.name ?? DB_DEFAULTS.name;
this.mode = config.mode ?? DB_DEFAULTS.mode;
this._version = config.version ?? DB_DEFAULTS.version;
this.pluginManager = new PluginManager();
// v0.3.2: 多标签页同步 — BroadcastChannel 广播表变更
if (config.multiTabSync && typeof BroadcastChannel !== 'undefined') {
this.channel = new BroadcastChannel(`metona-sqlark:${this.name}`);
this.channel.onmessage = (event) => {
const msg = event.data;
if (!msg || msg.type !== 'change')
return;
this.emit(msg.table ?? '', { type: 'external', table: msg.table ?? '' });
// Hybrid 引擎:从磁盘重载内存,保证读到其他标签页的最新数据
if (this.engine instanceof HybridEngine) {
this.engine.reloadMemoryFromDisk().catch(() => {
// 重载失败不影响主流程(下次读可能短暂过期)
});
}
};
}
}
// ---- 初始化 ----
/** 初始化数据库(创建引擎、打开连接) */
async init() {
// 创建引擎
this.engine = this.createEngine();
// 打开连接
await this.engine.open(this.name, this.version);
// v0.4.2-fix (P2-7): 从库内加载持久化的迁移版本,
// 重启后 migrateTo 从持久化版本继续执行,不再每次从 config.version 重置
if (typeof this.engine.getMeta === 'function') {
try {
const persistedVersion = await this.engine.getMeta('__metona_version');
if (persistedVersion != null && Number(persistedVersion) >= 1) {
this._version = Math.max(this._version, Math.floor(Number(persistedVersion)));
}
}
catch { /* 读取失败回退 config.version */ }
}
// 初始化执行器和事务管理器
this.executor = new QueryExecutor(this.engine, this.maxRowsPerQuery);
this.transactionManager = new TransactionManager(this.engine);
// 注册插件
if (this.config.plugins) {
for (const plugin of this.config.plugins) {
this.pluginManager.register(plugin, this);
}
}
this.ready = true;
// 回调
if (this.config.onReady) {
this.config.onReady(this);
}
}
/** 检查是否就绪 */
isReady() {
return this.ready;
}
// ---- 表管理 ----
/** 创建表 */
async defineTable(name, columns) {
this.ensureReady();
const schema = createSchema(name, columns);
try {
await this.pluginManager.trigger('beforeCreateTable', schema);
await this.engine.createTable(schema);
await this.pluginManager.trigger('afterCreateTable', schema);
}
catch (error) {
this._onError(error);
throw error;
}
// 清除缓存
this.tableCache.delete(name);
}
/** 获取表操作对象 */
table(name) {
this.ensureReady();
let t = this.tableCache.get(name);
if (!t) {
// v0.3.2: 表操作写入后广播变更(多标签页同步)
t = new Table(this.engine, name, this.executor, (tableName) => this.broadcastChange(tableName));
this.tableCache.set(name, t);
}
return t;
}
/** 删除表 */
async dropTable(name) {
this.ensureReady();
try {
await this.pluginManager.trigger('beforeDropTable', name);
await this.engine.dropTable(name);
await this.pluginManager.trigger('afterDropTable', name);
}
catch (error) {
this._onError(error);
throw error;
}
this.tableCache.delete(name);
}
/** 获取所有表名 */
async getTableNames() {
this.ensureReady();
return this.engine.getTableNames();
}
// ---- SQL 查询 ----
/** 执行 SQL 字符串查询 */
async query(sql) {
this.ensureReady();
const startTime = this.debug ? Date.now() : 0;
await this.pluginManager.trigger('beforeQuery', sql);
let result;
try {
// v0.3.0: 支持分号分隔的多语句,逐条顺序执行,返回最后一条的结果
const statements = parseAll(sql);
for (const stmt of statements) {
result = await this.executor.execute(stmt);
// v0.3.2: 写语句广播表变更(多标签页同步)
const table = this.writeStatementTable(stmt);
if (table)
this.broadcastChange(table);
}
}
catch (error) {
this._onError(error);
throw error;
}
await this.pluginManager.trigger('afterQuery', sql, result);
if (this.debug) {
const elapsed = Date.now() - startTime;
const rows = Array.isArray(result) ? result.length : 0;
this._debug(`query [${elapsed}ms] ${rows} rows: ${sql.slice(0, 100)}`);
}
return result;
}
// ---- 流式查询(v0.4.0 ----
/**
* 流式查询:逐行回调,不一次性物化全部结果(大表友好)。
* 支持简单 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);
* });
* ```
*/
async queryStream(sql, onRow) {
this.ensureReady();
const stmt = parseAll(sql)[0];
if (!stmt || stmt.type !== 'SELECT') {
throw new DatabaseError('queryStream only supports SELECT statements', 'NOT_SUPPORTED');
}
const select = stmt;
// 不可流式场景:JOIN / GROUP BY / HAVING / DISTINCT / 聚合 / UNION / 关联子查询 / ORDER BY
const aggregate = select.columns.some((c) => /^(COUNT|SUM|AVG|MIN|MAX)\(/i.test(c));
const streamable = !select.joins && !select.groupBy && !select.having && !select.distinct
&& !aggregate && !(select.orderBy && select.orderBy.length > 0)
&& !(select.where && select.where['$exists'] !== undefined);
if (streamable && typeof this.engine.findStream === 'function') {
// 用户回调为 async(返回 Promise)时引擎同步扫描无法 await → 回退物化
const isAsync = onRow.constructor?.name === 'AsyncFunction';
if (!isAsync) {
const where = this.normalizeWhereForStream(select);
const plainCols = select.columns.filter((c) => !/\s+AS\s+\w+$/i.test(c));
return this.engine.findStream(select.from, {
table: select.from,
columns: plainCols.length > 0 && plainCols[0] !== '*' ? plainCols : ['*'],
where: where && Object.keys(where).length > 0 ? where : undefined,
limit: select.limit,
offset: select.offset,
}, onRow);
}
}
// 回退:物化后逐行回调
const result = await this.query(sql);
if (Array.isArray(result)) {
for (const row of result) {
await onRow(row);
}
return result.length;
}
return 0;
}
/** 流式查询用:剥离主表别名前缀(复用 query 路径的规范化逻辑) */
normalizeWhereForStream(select) {
const aliases = [select.alias ?? select.from].filter(Boolean);
const strip = (col) => {
for (const a of aliases) {
if (col.startsWith(`${a}.`))
return col.slice(a.length + 1);
}
return col;
};
const walk = (w) => {
const out = {};
for (const [k, v] of Object.entries(w)) {
if (k === '$and' || k === '$or') {
out[k] = v.map(walk);
}
else if (k === '$not' && typeof v === 'object' && v !== null) {
out.$not = walk(v);
}
else {
out[strip(k)] = v;
}
}
return out;
};
return walk(select.where ?? {});
}
// ---- 事务 ----
/** 执行事务 */
async transaction(fn) {
this.ensureReady();
await this.pluginManager.trigger('beforeTransaction');
try {
const result = await this.transactionManager.execute(fn);
await this.pluginManager.trigger('afterTransaction');
return result;
}
catch (error) {
this._onError(error);
throw error;
}
}
// ---- 导入导出 ----
/** 导出表数据为 JSON */
async exportTable(tableName) {
this.ensureReady();
return this.engine.find(tableName, { table: tableName });
}
/** 导入 JSON 数据到表 */
async importTable(tableName, data) {
this.ensureReady();
try {
return await this.engine.insert(tableName, data);
}
catch (error) {
this._onError(error);
throw error;
}
}
/** 导出整个数据库为 JSON */
async exportAll() {
this.ensureReady();
const result = {};
const names = await this.engine.getTableNames();
for (const name of names) {
result[name] = await this.engine.find(name, { table: name });
}
return result;
}
/** 订阅表变更 */
subscribe(tableName, callback) {
const key = `change:${tableName}`;
if (!this.listeners.has(key))
this.listeners.set(key, new Set());
this.listeners.get(key).add(callback);
return () => this.listeners.get(key)?.delete(callback);
}
/** 触发变更事件 */
emit(tableName, event) {
const key = `change:${tableName}`;
this.listeners.get(key)?.forEach((cb) => cb(event));
}
// ---- 多标签页同步(v0.3.2 ----
/** 广播表变更到其他标签页(多标签页同步) */
broadcastChange(tableName) {
if (!this.channel)
return;
try {
this.channel.postMessage({ type: 'change', table: tableName });
}
catch {
// 广播失败不影响主流程
}
}
/** 写语句对应的表名(多标签页广播用) */
writeStatementTable(stmt) {
switch (stmt.type) {
case 'INSERT': return stmt.into;
case 'UPDATE': return stmt.table;
case 'DELETE': return stmt.from;
case 'CREATE_TABLE':
case 'DROP_TABLE':
case 'TRUNCATE_TABLE':
return stmt.name;
case 'ALTER_TABLE': return stmt.name;
case 'CREATE_INDEX':
case 'DROP_INDEX':
return stmt.table;
default:
return null;
}
}
/** 注册迁移 */
addMigration(version, up) {
this.migrations.set(version, up);
}
/** 执行迁移到指定版本 */
async migrateTo(targetVersion) {
this.ensureReady();
for (const [version, up] of [...this.migrations.entries()].sort((a, b) => a[0] - b[0])) {
if (version <= targetVersion && version > this._version) {
await up(this);
this._version = version;
}
}
// v0.4.2-fix (P2-7): 迁移版本持久化到库内,重启后从持久化版本继续,
// 避免"version 重置导致已执行迁移重跑(不幂等就炸)"或"版本门槛跳过迁移"
if (typeof this.engine.setMeta === 'function') {
try {
await this.engine.setMeta('__metona_version', String(this._version));
}
catch { /* 持久化失败不阻塞迁移流程 */ }
}
}
// ---- 自愈 / 重置(v0.4.2-fix, P2-9 ----
/**
* 崩溃恢复自愈 — 校验并清理损坏数据、恢复一致性。
* 检测到异常后调用,无需删库重建。
*/
async repair() {
this.ensureReady();
if (typeof this.engine.repair === 'function') {
await this.engine.repair();
this.tableCache.clear();
return;
}
// 兜底:重建表缓存
this.tableCache.clear();
}
/**
* 清空全部数据与表结构(保留库本身)。
* 支持后续继续使用本实例重建表。
*/
async clearAll() {
this.ensureReady();
if (typeof this.engine.clearAll === 'function') {
await this.engine.clearAll();
}
else {
const names = await this.engine.getTableNames();
for (const name of names) {
await this.engine.dropTable(name);
}
}
this.tableCache.clear();
}
// ---- 插件 ----
/** 获取插件管理器 */
getPluginManager() {
return this.pluginManager;
}
/** 注册钩子 */
on(hook, callback) {
this.pluginManager.on(hook, callback);
}
// ---- 生命周期 ----
/** 关闭数据库 */
async close() {
if (this.channel) {
this.channel.close();
this.channel = null;
}
this.pluginManager.destroy();
await this.engine.close();
this.tableCache.clear();
this.ready = false;
}
/** 获取底层引擎 */
getEngine() {
return this.engine;
}
// ---- 内部 ----
createEngine() {
const mode = this.mode;
const diskEngine = this.config.diskEngine ?? 'indexeddb';
switch (mode) {
case 'memory':
return new MemoryEngine();
case 'disk':
return diskEngine === 'opfs' ? new OPFSEngine() : new IndexedDBEngine();
case 'aria':
return new AriaEngine({ storageBackend: diskEngine === 'opfs' ? 'opfs' : 'indexeddb' });
case 'hybrid':
return new HybridEngine(diskEngine);
default:
throw new DatabaseError(`Unknown storage mode: ${mode}`, 'CONFIG_ERROR');
}
}
ensureReady() {
if (!this.ready) {
throw new DatabaseError('Database not initialized. Call await db.init() first.', 'DB_NOT_READY');
}
}
/** 错误回调分发 */
_onError(error) {
if (this.config.onError) {
try {
this.config.onError(error);
}
catch { /* 避免回调自身异常影响主流程 */ }
}
}
/** 调试日志 */
_debug(msg, ...args) {
if (this.debug) {
// eslint-disable-next-line no-console
console.debug(`[MetonaSqlark:${this.name}] ${msg}`, ...args);
}
}
}
/**
* metona-sqlark Connection Manager — 数据库实例连接池
* @module connection-manager
*
* v0.1.13: 避免重复创建同名数据库实例,通过 connect() 复用已有连接。
* 管理实例生命周期,防止重复 open IndexedDB。
*/
// ---------------------------------------------------------------------------
// ConnectionManager
// ---------------------------------------------------------------------------
class ConnectionManager {
constructor() {
/** 活跃连接:dbName → MetonaSqlark */
this.connections = new Map();
/** 连接引用计数:dbName → count */
this.refCount = new Map();
}
/**
* 获取或创建数据库实例
*
* 如果同名数据库已打开,复用已有实例并增加引用计数。
* 否则创建新实例。
*
* @example
* ```ts
* const db = await MetonaSqlark.connect({ name: 'my-app', mode: 'hybrid' });
* // ... use db
* await db.disconnect(); // 引用计数 -1,归零时自动关闭
* ```
*/
async connect(config) {
const name = config.name;
// 已有连接 → 复用
const existing = this.connections.get(name);
if (existing && existing.isReady()) {
const count = (this.refCount.get(name) ?? 0) + 1;
this.refCount.set(name, count);
return existing;
}
// 创建新连接
const db = new MetonaSqlark(config);
await db.init();
this.connections.set(name, db);
this.refCount.set(name, 1);
// 注入 disconnect 方法
db.disconnect = async () => {
await this.release(name);
};
return db;
}
/**
* 释放连接引用。引用计数归零时自动关闭数据库。
*/
async release(dbName) {
const count = (this.refCount.get(dbName) ?? 1) - 1;
if (count <= 0) {
const db = this.connections.get(dbName);
if (db) {
await db.close();
this.connections.delete(dbName);
}
this.refCount.delete(dbName);
}
else {
this.refCount.set(dbName, count);
}
}
/**
* 强制关闭指定数据库(忽略引用计数)
*/
async forceClose(dbName) {
const db = this.connections.get(dbName);
if (db) {
await db.close();
this.connections.delete(dbName);
}
this.refCount.delete(dbName);
}
/**
* 强制关闭所有连接
*/
async closeAll() {
for (const [, db] of this.connections) {
try {
await db.close();
}
catch { /* ignore */ }
}
this.connections.clear();
this.refCount.clear();
}
/**
* 获取所有活跃连接名
*/
getActiveConnections() {
return Array.from(this.connections.keys());
}
/**
* 获取连接的引用计数
*/
getRefCount(dbName) {
return this.refCount.get(dbName) ?? 0;
}
}
// ---------------------------------------------------------------------------
// 全局单例
// ---------------------------------------------------------------------------
const manager = new ConnectionManager();
// 挂载到 MetonaSqlark 静态方法(通过 any 绕过 TS 类型检查)
const M = MetonaSqlark;
M.connect = (config) => manager.connect(config);
M.disconnect = (dbName) => manager.release(dbName);
M.disconnectAll = () => manager.closeAll();
M.getActiveConnections = () => manager.getActiveConnections();
/**
* 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');
* ```
*/
async function create(config) {
const db = new MetonaSqlark(config);
await db.init();
return db;
}
// ---------------------------------------------------------------------------
// 全局 API
// ---------------------------------------------------------------------------
const api = {
VERSION,
version: VERSION,
create,
MetonaSqlark,
MeSqlark: MetonaSqlark,
};
if (typeof window !== 'undefined') {
window.MetonaSqlark = api;
window.MeSqlark = api;
}
// 别名
const MeSqlark = MetonaSqlark;
exports.AriaEngine = AriaEngine;
exports.HybridEngine = HybridEngine;
exports.IndexedDBEngine = IndexedDBEngine;
exports.MeSqlark = MeSqlark;
exports.MemoryEngine = MemoryEngine;
exports.MetonaSqlark = MetonaSqlark;
exports.OPFSBackend = OPFSBackend;
exports.OPFSEngine = OPFSEngine;
exports.Table = Table;
exports.VERSION = VERSION;
exports.api = api;
exports.create = create;
exports.default = api;
exports.parse = parse;
exports.parseAll = parseAll;
exports.tokenize = tokenize;
//# sourceMappingURL=metona-sqlark.cjs.map