- Parser 支持 CREATE TABLE IF NOT EXISTS 语法 - AST CreateTableStatement 新增 ifNotExists 字段 - Executor 表已存在时静默跳过 - 修复 demo.html Aria 预设报错
5761 lines
203 KiB
JavaScript
5761 lines
203 KiB
JavaScript
/**
|
||
* 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,
|
||
});
|
||
// ---------------------------------------------------------------------------
|
||
// 错误类型
|
||
// ---------------------------------------------------------------------------
|
||
/** 数据库错误 */
|
||
class DatabaseError extends Error {
|
||
constructor(message, code, details) {
|
||
super(message);
|
||
this.code = code;
|
||
this.details = details;
|
||
this.name = 'DatabaseError';
|
||
}
|
||
}
|
||
// ---------------------------------------------------------------------------
|
||
// 版本
|
||
// ---------------------------------------------------------------------------
|
||
const VERSION = '0.2.0';
|
||
|
||
/**
|
||
* 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)) {
|
||
// 顶层 $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;
|
||
}
|
||
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 } of orderBy) {
|
||
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;
|
||
// ---- 事务快照 ----
|
||
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.opened = false;
|
||
}
|
||
isOpen() { return this.opened; }
|
||
// ---- 表管理 ----
|
||
async createTable(schema) {
|
||
if (this.schemas.has(schema.name))
|
||
throw new DatabaseError(`Table "${schema.name}" already exists`, 'TABLE_EXISTS');
|
||
this.schemas.set(schema.name, schema);
|
||
this.tables.set(schema.name, new Map());
|
||
const tableIndexes = new Map();
|
||
for (const [colName, colDef] of Object.entries(schema.columns)) {
|
||
if (colDef.index || colDef.unique)
|
||
tableIndexes.set(colName, new Map());
|
||
}
|
||
this.indexes.set(schema.name, tableIndexes);
|
||
}
|
||
async dropTable(tableName) {
|
||
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; }
|
||
// ---- 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;
|
||
}
|
||
async update(tableName, query, updates) {
|
||
this.ensureTable(tableName);
|
||
const schema = this.schemas.get(tableName);
|
||
const table = this.tables.get(tableName);
|
||
let count = 0;
|
||
for (const [pk, row] of table) {
|
||
if (!query.where || Object.keys(query.where).length === 0 || matchWhere(row, query.where)) {
|
||
const updated = { ...row, ...updates };
|
||
this.validateRow(schema, updated);
|
||
table.set(pk, updated);
|
||
count++;
|
||
}
|
||
}
|
||
return count;
|
||
}
|
||
async delete(tableName, 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)) {
|
||
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();
|
||
}
|
||
// ---- 事务 ----
|
||
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)) {
|
||
if (typeof condition !== 'object' || condition === null) {
|
||
const colIndex = tableIndexes.get(col);
|
||
if (colIndex) {
|
||
const pks = colIndex.get(condition);
|
||
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);
|
||
}
|
||
}
|
||
}
|
||
// ---- 外键级联 ----
|
||
/**
|
||
* 级联删除:查找引用 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 || colDef.onDelete === 'RESTRICT')
|
||
continue;
|
||
const [refTable, refCol] = 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);
|
||
}
|
||
}
|
||
if (colDef.onDelete === 'CASCADE') {
|
||
// 递归级联
|
||
for (const refPk of toDelete) {
|
||
const refRow = refTableData.get(refPk);
|
||
if (refRow) {
|
||
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) {
|
||
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) {
|
||
this.dbName = dbName;
|
||
this.version = version;
|
||
await this.memoryCache.open(dbName, version);
|
||
return new Promise((resolve, reject) => {
|
||
const request = indexedDB.open(dbName, version);
|
||
request.onsuccess = () => {
|
||
this.db = request.result;
|
||
// 多标签页冲突处理:其他标签页升级版本时自动关闭当前连接
|
||
this.db.onversionchange = () => {
|
||
if (this.db) {
|
||
this.db.close();
|
||
this.db = null;
|
||
// eslint-disable-next-line no-console
|
||
console.warn(`[metona-sqlark] Database "${dbName}" was upgraded in another tab. Connection closed. Please re-open.`);
|
||
}
|
||
};
|
||
resolve();
|
||
};
|
||
request.onerror = () => reject(new DatabaseError(`Failed to open IndexedDB "${dbName}"`, 'IDB_OPEN_ERROR', request.error));
|
||
request.onblocked = () => reject(new DatabaseError(`IndexedDB "${dbName}" is blocked`, 'IDB_BLOCKED'));
|
||
});
|
||
}
|
||
async close() {
|
||
if (this.db) {
|
||
this.db.onversionchange = null; // 清理监听器
|
||
this.db.close();
|
||
this.db = null;
|
||
}
|
||
await this.memoryCache.close();
|
||
}
|
||
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); }
|
||
// ---- 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);
|
||
}
|
||
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);
|
||
}
|
||
// ---- 事务 ----
|
||
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');
|
||
// 确认内存层的变更
|
||
await this.memoryCache.commitTransaction();
|
||
// 批量将内存数据刷到 IndexedDB
|
||
await this.flushToIDB();
|
||
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 });
|
||
}
|
||
}
|
||
};
|
||
request.onsuccess = () => { this.db = request.result; resolve(); };
|
||
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; 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();
|
||
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));
|
||
});
|
||
}
|
||
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));
|
||
});
|
||
}
|
||
/** 将内存缓存中的所有表数据原子性刷新到 IndexedDB */
|
||
async flushToIDB() {
|
||
const tableNames = await this.memoryCache.getTableNames();
|
||
const 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;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 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();
|
||
}
|
||
// ---- 生命周期 ----
|
||
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 });
|
||
}
|
||
async close() {
|
||
this.root = null;
|
||
this.tablesDir = null;
|
||
await this.memoryCache.close();
|
||
}
|
||
isOpen() {
|
||
return this.tablesDir !== null;
|
||
}
|
||
// ---- 表管理 ----
|
||
async createTable(schema) {
|
||
await this.memoryCache.createTable(schema);
|
||
// OPFS 中表以空 JSON 数组文件形式存在
|
||
await this.writeTableData(schema.name, []);
|
||
}
|
||
async dropTable(tableName) {
|
||
await this.memoryCache.dropTable(tableName);
|
||
if (this.tablesDir) {
|
||
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);
|
||
}
|
||
// ---- CRUD ----
|
||
async insert(tableName, rows) {
|
||
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);
|
||
}
|
||
async update(tableName, query, updates) {
|
||
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) {
|
||
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) {
|
||
await this.memoryCache.clear(tableName);
|
||
await this.writeTableData(tableName, []);
|
||
}
|
||
// ---- 事务 ----
|
||
async beginTransaction() {
|
||
await this.memoryCache.beginTransaction();
|
||
}
|
||
async commitTransaction() {
|
||
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) {
|
||
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 加载表数据到内存缓存 */
|
||
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]);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* AriaEngine Types — 内部类型定义
|
||
* @module engine/aria/types
|
||
*
|
||
* 页面式存储引擎的所有内部枚举、接口和常量。
|
||
*/
|
||
// =============================================================================
|
||
// 页面常量
|
||
// =============================================================================
|
||
/** 页面大小:4KB */
|
||
const PAGE_SIZE = 4096;
|
||
// =============================================================================
|
||
// 页面类型
|
||
// =============================================================================
|
||
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: 'batch',
|
||
checkpointInterval: 1000,
|
||
compression: false,
|
||
storageBackend: 'indexeddb',
|
||
};
|
||
|
||
/**
|
||
* 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) {
|
||
// 简化:在实际生产环境中需要完整的删除修复
|
||
// 这里使用简化版,仅处理常见情况
|
||
}
|
||
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 格式。
|
||
*
|
||
* SSTable 文件布局:
|
||
* ┌──────────────────────────────────────────────┐
|
||
* │ 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, 0x53535442 ="SSTB")│
|
||
* │ - checksum (u32) │
|
||
* └──────────────────────────────────────────────┘
|
||
*/
|
||
const SSTABLE_MAGIC$1 = 0x53535442; // "SSTB"
|
||
const SSTABLE_FOOTER_SIZE = 32;
|
||
// ---------------------------------------------------------------------------
|
||
// SSTableBuilder
|
||
// ---------------------------------------------------------------------------
|
||
class SSTableBuilder {
|
||
constructor(blockSizeLimit = 4096) {
|
||
this.entries = [];
|
||
this.currentBlock = [];
|
||
this.currentBlockStartKey = '';
|
||
this.blockSizeLimit = blockSizeLimit;
|
||
}
|
||
/** 添加一个 key-value 条目(必须按键排序添加) */
|
||
add(key, value) {
|
||
if (this.currentBlock.length === 0) {
|
||
this.currentBlockStartKey = key;
|
||
}
|
||
this.currentBlock.push([key, value]);
|
||
this.entries.push([key, value]);
|
||
// 如果当前 Block 达到大小限制,切割
|
||
const estimated = this.estimateBlockSize();
|
||
if (estimated >= this.blockSizeLimit && this.currentBlock.length > 1) ;
|
||
}
|
||
/**
|
||
* 构建 SSTable 文件的二进制数据。
|
||
* 返回 { data: Uint8Array, indexEntries: IndexEntry[], bloomFilter: BloomFilter }
|
||
*/
|
||
build() {
|
||
const blocks = this.splitIntoBlocks();
|
||
const bloomFilter = new BloomFilter(this.entries.length);
|
||
// 预计算总大小
|
||
let totalSize = 0;
|
||
const blockOffsets = [];
|
||
for (const block of blocks) {
|
||
blockOffsets.push(totalSize);
|
||
const blockSize = this.computeBlockSize(block);
|
||
totalSize += blockSize;
|
||
}
|
||
// 索引块
|
||
const indexEntries = [];
|
||
for (let i = 0; i < blocks.length; i++) {
|
||
const block = blocks[i];
|
||
const lastKey = block[block.length - 1][0];
|
||
const blockSize = this.computeBlockSize(block);
|
||
indexEntries.push({
|
||
key: lastKey,
|
||
blockOffset: blockOffsets[i],
|
||
blockSize,
|
||
});
|
||
}
|
||
const indexBlockSize = this.estimateIndexBlockSize(indexEntries);
|
||
// 写入到 buffer
|
||
const finalSize = totalSize + indexBlockSize + 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);
|
||
// ---- Footer ----
|
||
const footerOffset = offset;
|
||
view.setUint32(footerOffset, indexOffset, false); // index_offset
|
||
view.setUint32(footerOffset + 4, indexBlockSize, false); // index_size
|
||
view.setUint32(footerOffset + 8, 0, false); // bloom_offset (embedded in footer)
|
||
view.setUint32(footerOffset + 12, 0, false); // bloom_size
|
||
view.setUint32(footerOffset + 16, bloomFilter.getHashCount(), false);
|
||
view.setUint32(footerOffset + 20, this.entries.length, false);
|
||
view.setUint32(footerOffset + 24, SSTABLE_MAGIC$1, false);
|
||
view.setUint32(footerOffset + 28, 0, false); // checksum (simplified: 0)
|
||
return {
|
||
sstableData: new Uint8Array(buf),
|
||
indexEntries,
|
||
};
|
||
}
|
||
/** 获取条目数 */
|
||
getEntryCount() {
|
||
return this.entries.length;
|
||
}
|
||
// -----------------------------------------------------------------------
|
||
// 内部
|
||
// -----------------------------------------------------------------------
|
||
splitIntoBlocks() {
|
||
const blocks = [];
|
||
let current = [];
|
||
for (const entry of this.entries) {
|
||
current.push(entry);
|
||
if (this.estimateBlockSizeFromEntries(current) >= this.blockSizeLimit && current.length > 1) {
|
||
blocks.push(current.slice(0, -1));
|
||
current = [entry];
|
||
}
|
||
}
|
||
if (current.length > 0)
|
||
blocks.push(current);
|
||
return blocks;
|
||
}
|
||
estimateBlockSize() {
|
||
return this.estimateBlockSizeFromEntries(this.currentBlock);
|
||
}
|
||
estimateBlockSizeFromEntries(entries) {
|
||
let size = 0;
|
||
for (const [key, value] of entries) {
|
||
size += 4 + key.length + JSON.stringify(value).length;
|
||
}
|
||
return size;
|
||
}
|
||
computeBlockSize(block) {
|
||
// entryCount (u32) + 每对: keyLen(u16) + key + valueLen(u16) + value json
|
||
let size = 4;
|
||
for (const [key, value] of block) {
|
||
const json = JSON.stringify(value);
|
||
size += 2 + key.length + 2 + json.length;
|
||
}
|
||
return size;
|
||
}
|
||
writeDataBlock(view, offset, block, bloomFilter) {
|
||
// entry count
|
||
view.setUint32(offset, block.length, false);
|
||
offset += 4;
|
||
for (const [key, value] of block) {
|
||
const encoder = new TextEncoder();
|
||
const keyBytes = encoder.encode(key);
|
||
const valueBytes = encoder.encode(JSON.stringify(value));
|
||
// key length
|
||
view.setUint16(offset, keyBytes.length, false);
|
||
offset += 2;
|
||
// key
|
||
new Uint8Array(view.buffer).set(keyBytes, offset);
|
||
offset += keyBytes.length;
|
||
// value length
|
||
view.setUint16(offset, valueBytes.length, false);
|
||
offset += 2;
|
||
// value
|
||
new Uint8Array(view.buffer).set(valueBytes, offset);
|
||
offset += valueBytes.length;
|
||
// 插入 bloom filter
|
||
bloomFilter.insert(key);
|
||
}
|
||
return offset;
|
||
}
|
||
estimateIndexBlockSize(entries) {
|
||
// entryCount(u32) + each: keyLen(u16)+key+blockOffset(u32)+blockSize(u32)
|
||
let size = 4;
|
||
for (const entry of entries) {
|
||
size += 2 + entry.key.length + 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.setUint16(offset, keyBytes.length, false);
|
||
offset += 2;
|
||
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
|
||
*/
|
||
const SSTABLE_MAGIC = 0x53535442; // "SSTB"
|
||
// ---------------------------------------------------------------------------
|
||
// SSTableReader
|
||
// ---------------------------------------------------------------------------
|
||
class SSTableReader {
|
||
constructor(data, meta) {
|
||
this.indexEntries = [];
|
||
this.entryCount = 0;
|
||
this.data = data;
|
||
this.view = new DataView(data.buffer, data.byteOffset, data.byteLength);
|
||
this.meta = meta;
|
||
this.parseFooter();
|
||
}
|
||
// -----------------------------------------------------------------------
|
||
// 查询
|
||
// -----------------------------------------------------------------------
|
||
/** 精确查找 key */
|
||
get(key) {
|
||
const blockIdx = this.locateBlock(key);
|
||
if (blockIdx < 0)
|
||
return null;
|
||
const entry = this.indexEntries[blockIdx];
|
||
const blockData = new Uint8Array(this.data.buffer, this.data.byteOffset + entry.blockOffset, entry.blockSize);
|
||
const blockView = new DataView(blockData.buffer, blockData.byteOffset, blockData.byteLength);
|
||
const entryCount = blockView.getUint32(0, false);
|
||
let offset = 4;
|
||
// 二分查找 block 内的 key
|
||
// 为简单起见,这里使用顺序扫描(生产中应二分查找)
|
||
for (let i = 0; i < entryCount; i++) {
|
||
const keyLen = blockView.getUint16(offset, false);
|
||
offset += 2;
|
||
const key = new TextDecoder().decode(blockData.slice(offset, offset + keyLen));
|
||
offset += keyLen;
|
||
const valLen = blockView.getUint16(offset, false);
|
||
offset += 2;
|
||
const valBytes = blockData.slice(offset, offset + valLen);
|
||
offset += valLen;
|
||
if (key === key) {
|
||
return JSON.parse(new TextDecoder().decode(valBytes));
|
||
}
|
||
}
|
||
return null;
|
||
}
|
||
/** 范围扫描 */
|
||
rangeScan(startKey, endKey, callback) {
|
||
const startBlockIdx = Math.max(0, this.locateBlockGE(startKey));
|
||
const endBlockIdx = Math.min(this.indexEntries.length - 1, this.locateBlockLE(endKey));
|
||
for (let bi = startBlockIdx; bi <= endBlockIdx && bi >= 0; bi++) {
|
||
const entry = this.indexEntries[bi];
|
||
const blockData = new Uint8Array(this.data.buffer, this.data.byteOffset + entry.blockOffset, entry.blockSize);
|
||
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++) {
|
||
const keyLen = blockView.getUint16(offset, false);
|
||
offset += 2;
|
||
const key = new TextDecoder().decode(blockData.slice(offset, offset + keyLen));
|
||
offset += keyLen;
|
||
const valLen = blockView.getUint16(offset, false);
|
||
offset += 2;
|
||
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) {
|
||
for (const entry of this.indexEntries) {
|
||
const blockData = new Uint8Array(this.data.buffer, this.data.byteOffset + entry.blockOffset, entry.blockSize);
|
||
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++) {
|
||
const keyLen = blockView.getUint16(offset, false);
|
||
offset += 2;
|
||
const key = new TextDecoder().decode(blockData.slice(offset, offset + keyLen));
|
||
offset += keyLen;
|
||
const valLen = blockView.getUint16(offset, false);
|
||
offset += 2;
|
||
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;
|
||
// 验证魔数
|
||
const magic = this.view.getUint32(footerOffset + 24, false);
|
||
if (magic !== SSTABLE_MAGIC) {
|
||
throw new Error(`Invalid SSTable magic: expected ${SSTABLE_MAGIC}, got ${magic}`);
|
||
}
|
||
const indexOffset = this.view.getUint32(footerOffset, false);
|
||
const indexSize = this.view.getUint32(footerOffset + 4, false);
|
||
this.entryCount = this.view.getUint32(footerOffset + 20, false);
|
||
// 解析索引块
|
||
this.parseIndexBlock(indexOffset, indexSize);
|
||
}
|
||
parseIndexBlock(offset, _size) {
|
||
const entryCount = this.view.getUint32(offset, false);
|
||
offset += 4;
|
||
for (let i = 0; i < entryCount; i++) {
|
||
const keyLen = this.view.getUint16(offset, false);
|
||
offset += 2;
|
||
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;
|
||
this.indexEntries.push({ key, blockOffset, 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) {
|
||
for (let i = 0; i < this.indexEntries.length; i++) {
|
||
if (this.indexEntries[i].key >= key)
|
||
return i;
|
||
}
|
||
return this.indexEntries.length - 1;
|
||
}
|
||
locateBlockLE(key) {
|
||
for (let i = this.indexEntries.length - 1; i >= 0; i--) {
|
||
if (this.indexEntries[i].key <= key)
|
||
return i;
|
||
}
|
||
return 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 node = this.heap.pop();
|
||
const key = node.key;
|
||
const value = node.value;
|
||
// 刷新此来源的下一个值
|
||
this.seedFromSource(node.sourceIndex);
|
||
// 跳过重复 key:取最新的(堆顶的即是最新的,因为来源下标越小越新)
|
||
while (this.heap.peek() && this.heap.peek().key === key) {
|
||
const dup = this.heap.pop();
|
||
this.seedFromSource(dup.sourceIndex);
|
||
}
|
||
return [key, 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;
|
||
this.levels = [];
|
||
this.sstableCache = new Map();
|
||
this.nextSSTableId = 1;
|
||
this.operationCount = 0;
|
||
this.initialized = false;
|
||
this.memtable = new MemTable(config.memtableSizeThreshold ?? DEFAULT_MEMTABLE_SIZE);
|
||
this.levelSizeMultiplier = config.levelSizeMultiplier ?? DEFAULT_LEVEL_SIZE_MULTIPLIER;
|
||
this.blockSize = config.blockSize ?? 4096;
|
||
this.sstableStore = config.sstableStore;
|
||
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();
|
||
// 按层级分组
|
||
for (const meta of metas) {
|
||
if (meta.level >= 0 && meta.level < MAX_LSM_LEVELS) {
|
||
this.levels[meta.level].push(meta);
|
||
}
|
||
}
|
||
// 各层级按 minKey 排序(方便后续范围查询剪枝)
|
||
for (let i = 0; i < MAX_LSM_LEVELS; i++) {
|
||
this.levels[i].sort((a, b) => (a.minKey < b.minKey ? -1 : a.minKey > b.minKey ? 1 : 0));
|
||
}
|
||
// 恢复 nextSSTableId
|
||
if (metas.length > 0) {
|
||
this.nextSSTableId = Math.max(...metas.map((m) => m.id)) + 1;
|
||
}
|
||
this.initialized = true;
|
||
}
|
||
// =======================================================================
|
||
// 写入
|
||
// =======================================================================
|
||
put(key, value) {
|
||
this.memtable.put(key, value);
|
||
this.operationCount++;
|
||
if (this.memtable.shouldFlush()) {
|
||
this.freezeMemtable();
|
||
}
|
||
}
|
||
delete(key) {
|
||
this.memtable.put(key, { __tombstone: true });
|
||
this.operationCount++;
|
||
if (this.memtable.shouldFlush()) {
|
||
this.freezeMemtable();
|
||
}
|
||
}
|
||
freezeMemtable() {
|
||
if (this.immutableMemtable) {
|
||
this.flushImmutableSync();
|
||
}
|
||
this.immutableMemtable = this.memtable;
|
||
this.memtable = new MemTable(this.memtable.getEstimatedSize());
|
||
}
|
||
/** 同步等待 Immutable MemTable 刷盘完成 */
|
||
flushImmutableSync() {
|
||
if (!this.immutableMemtable)
|
||
return;
|
||
const entries = this.immutableMemtable.getAllEntries();
|
||
if (entries.length === 0) {
|
||
this.immutableMemtable = null;
|
||
return;
|
||
}
|
||
const id = this.nextSSTableId++;
|
||
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.sstableCache.set(id, sstableData);
|
||
// 持久化:先存数据,再存元数据
|
||
this.sstableStore.save(id, sstableData).catch(() => { });
|
||
this.sstableStore.saveMeta(meta).catch(() => { });
|
||
this.levels[0].push(meta);
|
||
this.immutableMemtable = null;
|
||
if (this.levels[0].length >= 4) {
|
||
this.compactLevelSync(0);
|
||
}
|
||
}
|
||
// =======================================================================
|
||
// 读取
|
||
// =======================================================================
|
||
get(key) {
|
||
// 1. 活跃 MemTable
|
||
let result = this.memtable.get(key);
|
||
if (result !== null)
|
||
return this.unwrapTombstone(result);
|
||
// 2. 不可变 MemTable
|
||
if (this.immutableMemtable) {
|
||
result = this.immutableMemtable.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 mergeIter = new MergeIterator();
|
||
// MemTable(最新优先)
|
||
mergeIter.addSource(new ArrayEntrySource(this.memtable.rangeScan(startKey, endKey)));
|
||
if (this.immutableMemtable) {
|
||
mergeIter.addSource(new ArrayEntrySource(this.immutableMemtable.rangeScan(startKey, endKey)));
|
||
}
|
||
// SSTable
|
||
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;
|
||
const entries = [];
|
||
reader.rangeScan(startKey, endKey, (k, v) => entries.push([k, v]));
|
||
mergeIter.addSource(new ArrayEntrySource(entries));
|
||
}
|
||
}
|
||
const merged = mergeIter.drain();
|
||
return merged
|
||
.filter(([, v]) => !v.__tombstone);
|
||
}
|
||
getAllEntries() {
|
||
const result = new Map();
|
||
// 从最旧层级开始聚合
|
||
for (let level = MAX_LSM_LEVELS - 1; level >= 0; level--) {
|
||
for (const meta of this.levels[level]) {
|
||
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);
|
||
}
|
||
if (this.immutableMemtable) {
|
||
for (const [k, v] of this.immutableMemtable.getAllEntries()) {
|
||
result.set(k, v);
|
||
}
|
||
}
|
||
return Array.from(result.entries()).filter(([, v]) => !v.__tombstone);
|
||
}
|
||
// =======================================================================
|
||
// Compaction
|
||
// =======================================================================
|
||
/** 同步执行 Compaction(简化版,仅供内部调用) */
|
||
compactLevelSync(level) {
|
||
if (level >= MAX_LSM_LEVELS - 1)
|
||
return;
|
||
if (this.levels[level].length < 4)
|
||
return;
|
||
const sstables = this.levels[level].splice(0, this.levels[level].length);
|
||
const mergeIter = new MergeIterator();
|
||
for (const meta of sstables) {
|
||
const reader = this.loadSSTableReader(meta);
|
||
if (!reader)
|
||
continue;
|
||
const entries = [];
|
||
reader.scanAll((k, v) => entries.push([k, v]));
|
||
mergeIter.addSource(new ArrayEntrySource(entries));
|
||
}
|
||
const merged = mergeIter.drain();
|
||
if (merged.length === 0)
|
||
return;
|
||
const id = this.nextSSTableId++;
|
||
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.sstableCache.set(id, sstableData);
|
||
this.sstableStore.save(id, sstableData).catch(() => { });
|
||
this.sstableStore.saveMeta(meta).catch(() => { });
|
||
this.levels[level + 1].push(meta);
|
||
// 删除旧 SSTable
|
||
for (const old of sstables) {
|
||
this.sstableCache.delete(old.id);
|
||
this.sstableStore.delete(old.id).catch(() => { });
|
||
this.sstableStore.deleteMeta(old.id).catch(() => { });
|
||
}
|
||
}
|
||
async flush() {
|
||
if (this.immutableMemtable) {
|
||
this.flushImmutableSync();
|
||
}
|
||
if (this.memtable.getEntryCount() > 0) {
|
||
this.freezeMemtable();
|
||
this.flushImmutableSync();
|
||
}
|
||
// 等待存储完成
|
||
await new Promise((r) => setTimeout(r, 10));
|
||
}
|
||
async clear() {
|
||
this.memtable.clear();
|
||
this.immutableMemtable = null;
|
||
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.nextSSTableId = 1;
|
||
}
|
||
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;
|
||
}
|
||
// =======================================================================
|
||
// 内部
|
||
// =======================================================================
|
||
unwrapTombstone(value) {
|
||
if (!value)
|
||
return null;
|
||
if (value.__tombstone)
|
||
return null;
|
||
return value;
|
||
}
|
||
/** 尝试从缓存或存储加载 SSTable,返回 Reader */
|
||
loadSSTableReader(meta) {
|
||
// 先检查缓存
|
||
let data = this.sstableCache.get(meta.id);
|
||
if (!data) {
|
||
return null; // 异步加载已不可用,返回 null(调用方处理)
|
||
}
|
||
try {
|
||
return new SSTableReader(data, meta);
|
||
}
|
||
catch {
|
||
return null;
|
||
}
|
||
}
|
||
/** 预加载 SSTable 到缓存(供外部在需要时调用) */
|
||
async preloadSSTable(id) {
|
||
if (this.sstableCache.has(id))
|
||
return;
|
||
const data = await this.sstableStore.load(id);
|
||
if (data) {
|
||
this.sstableCache.set(id, data);
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 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 = [];
|
||
this.store = store;
|
||
this.enabled = enabled;
|
||
this.syncMode = syncMode;
|
||
}
|
||
// =======================================================================
|
||
// 写入
|
||
// =======================================================================
|
||
/** 追加一条 WAL 记录 */
|
||
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') {
|
||
this.store.append(bytes).catch(() => {
|
||
// eslint-disable-next-line no-console
|
||
console.warn('[AriaEngine WAL] Failed to append record');
|
||
});
|
||
}
|
||
else if (this.syncMode === 'batch') {
|
||
this.buffer.push(bytes);
|
||
}
|
||
// 'none' mode: 不写 WAL
|
||
}
|
||
/** 批量刷新缓冲的 WAL 记录 */
|
||
async flush() {
|
||
if (!this.enabled || this.buffer.length === 0)
|
||
return;
|
||
const totalLen = this.buffer.reduce((sum, b) => sum + b.byteLength, 0);
|
||
const combined = new Uint8Array(totalLen);
|
||
let offset = 0;
|
||
for (const buf of this.buffer) {
|
||
combined.set(buf, offset);
|
||
offset += buf.byteLength;
|
||
}
|
||
await this.store.append(combined);
|
||
this.buffer = [];
|
||
}
|
||
// =======================================================================
|
||
// 恢复
|
||
// =======================================================================
|
||
/** 从 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;
|
||
}
|
||
// =======================================================================
|
||
// 统计
|
||
// =======================================================================
|
||
isEnabled() {
|
||
return this.enabled;
|
||
}
|
||
getLSN() {
|
||
return this.lsn;
|
||
}
|
||
getBufferedCount() {
|
||
return this.buffer.length;
|
||
}
|
||
// -----------------------------------------------------------------------
|
||
// 编解码
|
||
// -----------------------------------------------------------------------
|
||
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 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;
|
||
const tableName = new TextDecoder().decode(data.slice(offset, offset + tableLen));
|
||
offset += tableLen;
|
||
const keyLen = view.getUint16(offset, false);
|
||
offset += 2;
|
||
const key = new TextDecoder().decode(data.slice(offset, offset + keyLen));
|
||
offset += keyLen;
|
||
const jsonLen = view.getUint32(offset, false);
|
||
offset += 4;
|
||
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
|
||
offset += 4;
|
||
records.push({
|
||
lsn,
|
||
type,
|
||
txnId,
|
||
tableName,
|
||
key,
|
||
data: recordData,
|
||
checksum: 0,
|
||
});
|
||
}
|
||
catch {
|
||
break;
|
||
}
|
||
}
|
||
return records;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* AriaEngine Checkpoint — 检查点机制
|
||
* @module engine/aria/wal/checkpoint
|
||
*/
|
||
// ---------------------------------------------------------------------------
|
||
// CheckpointManager
|
||
// ---------------------------------------------------------------------------
|
||
class CheckpointManager {
|
||
constructor(lsm, wal, flushable = null, interval = 1000) {
|
||
this.opCount = 0;
|
||
this.lsm = lsm;
|
||
this.wal = wal;
|
||
this.flushable = flushable;
|
||
this.interval = interval;
|
||
}
|
||
async tick() {
|
||
this.opCount++;
|
||
if (this.opCount >= this.interval) {
|
||
await this.checkpoint();
|
||
}
|
||
}
|
||
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
|
||
*
|
||
* 封装底层浏览器存储 API(IndexedDB / 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'));
|
||
});
|
||
}
|
||
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'));
|
||
});
|
||
}
|
||
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 delete(key) {
|
||
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();
|
||
}
|
||
}
|
||
|
||
/**
|
||
* AriaEngine — 自研页面式存储引擎主类
|
||
* @module engine/aria/index
|
||
*
|
||
* 实现 IStorageEngine 接口。
|
||
*
|
||
* v0.2.1: 完整持久化
|
||
* - Schema 存入 __aria_schemas
|
||
* - SSTable 元数据存入 __aria_lsm_meta
|
||
* - WAL 恢复包含行数据
|
||
* - 启动时自动加载 Schema + SSTable
|
||
*/
|
||
// ---------------------------------------------------------------------------
|
||
// AriaEngine
|
||
// ---------------------------------------------------------------------------
|
||
class AriaEngine {
|
||
constructor(config = {}) {
|
||
this.name = 'aria';
|
||
this.opened = false;
|
||
this.dbName = '';
|
||
// 表结构
|
||
this.schemas = new Map();
|
||
this.tablePKs = new Map();
|
||
this.opCounter = 0;
|
||
// 事务
|
||
this.currentTxnId = null;
|
||
this.txnSnapshot = null;
|
||
this.config = { ...DEFAULT_ARIA_CONFIG, ...config };
|
||
}
|
||
// =======================================================================
|
||
// 生命周期
|
||
// =======================================================================
|
||
async open(dbName, _version) {
|
||
if (this.opened)
|
||
return;
|
||
this.dbName = dbName;
|
||
// 1. 存储后端
|
||
if (this.config.storageBackend === 'indexeddb') {
|
||
this.backend = new IndexedDBBackend();
|
||
}
|
||
else {
|
||
this.backend = new MemoryBackend();
|
||
}
|
||
await this.backend.open(dbName);
|
||
// 2. 构建 SSTableStore
|
||
const sstableStore = this.createSSTableStore();
|
||
// 3. 初始化 LSM
|
||
this.lsm = new LSM({
|
||
memtableSizeThreshold: this.config.memtableSizeThreshold,
|
||
levelSizeMultiplier: this.config.levelSizeMultiplier,
|
||
blockSize: this.config.pageSize,
|
||
bloomBitsPerKey: this.config.bloomFilterBitsPerKey,
|
||
sstableStore,
|
||
});
|
||
// 4. 初始化 WAL
|
||
this.wal = new WAL({
|
||
append: async (data) => {
|
||
// Store each record as a separate numbered key
|
||
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.write(`__wal_${idx}`, copy);
|
||
await this.setWALCount(idx + 1);
|
||
},
|
||
readAll: async () => {
|
||
const count = await this.getWALCount();
|
||
if (count === 0)
|
||
return new Uint8Array(0);
|
||
// Read all records and concatenate
|
||
const chunks = [];
|
||
for (let i = 0; i < count; i++) {
|
||
const d = await this.backend.read(`__wal_${i}`);
|
||
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 () => {
|
||
const count = await this.getWALCount();
|
||
for (let i = 0; i < count; i++) {
|
||
await this.backend.delete(`__wal_${i}`);
|
||
}
|
||
await this.setWALCount(0);
|
||
},
|
||
exists: async () => {
|
||
const count = await this.getWALCount();
|
||
return count > 0;
|
||
},
|
||
}, this.config.walEnabled, this.config.walSyncMode);
|
||
// 5. 恢复 Schema
|
||
await this.loadSchemas();
|
||
// 6. 初始化 LSM(加载 SSTable 元数据)
|
||
await this.lsm.init();
|
||
// 7. WAL 恢复(恢复未刷盘的数据)
|
||
await this.wal.recover((record) => this.applyWALRecord(record));
|
||
// 8. Checkpoint Manager(BufferPool 暂简化,使用 flush 替代)
|
||
this.checkpointManager = new CheckpointManager(this.lsm, this.wal, { flushAll: async () => { await this.lsm.flush(); } }, this.config.checkpointInterval);
|
||
this.opened = true;
|
||
}
|
||
async close() {
|
||
if (!this.opened)
|
||
return;
|
||
await this.persistSchemas();
|
||
await this.lsm.flush();
|
||
await this.wal.flush();
|
||
await this.backend.close();
|
||
this.schemas.clear();
|
||
this.opened = false;
|
||
}
|
||
isOpen() { return this.opened; }
|
||
// =======================================================================
|
||
// 表管理
|
||
// =======================================================================
|
||
async createTable(schema) {
|
||
this.ensureOpen();
|
||
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));
|
||
await this.persistSchemas();
|
||
this.wal.append({
|
||
type: WALRecordType.CREATE_TABLE,
|
||
txnId: 0,
|
||
tableName: schema.name,
|
||
key: '',
|
||
data: { schema: JSON.stringify(schema) },
|
||
});
|
||
}
|
||
async dropTable(tableName) {
|
||
this.ensureOpen();
|
||
this.ensureTable(tableName);
|
||
// 删除表中所有行
|
||
const rows = this.getAllRows(tableName);
|
||
for (const row of rows) {
|
||
const pkCol = this.tablePKs.get(tableName);
|
||
this.lsm.delete(`${tableName}:${row[pkCol]}`);
|
||
}
|
||
this.schemas.delete(tableName);
|
||
this.tablePKs.delete(tableName);
|
||
await this.persistSchemas();
|
||
this.wal.append({
|
||
type: WALRecordType.DROP_TABLE,
|
||
txnId: 0,
|
||
tableName,
|
||
key: '',
|
||
});
|
||
}
|
||
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 = [];
|
||
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
|
||
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
|
||
this.txnSnapshot.set(key, validated);
|
||
}
|
||
else {
|
||
// Direct write to LSM
|
||
this.lsm.put(key, validated);
|
||
}
|
||
pks.push(pkValue);
|
||
this.wal.append({
|
||
type: WALRecordType.INSERT,
|
||
txnId: this.currentTxnId ?? 0,
|
||
tableName,
|
||
key: pkValue,
|
||
data: validated,
|
||
});
|
||
}
|
||
this.opCounter += rows.length;
|
||
await this.checkpointManager.tick();
|
||
return pks;
|
||
}
|
||
async find(tableName, query) {
|
||
this.ensureOpen();
|
||
this.ensureTable(tableName);
|
||
let rows;
|
||
// Try index lookup
|
||
const fastPath = this.tryIndexLookup(tableName, query);
|
||
if (fastPath !== null) {
|
||
rows = fastPath;
|
||
}
|
||
else {
|
||
rows = this.getAllRows(tableName);
|
||
}
|
||
// Merge transaction snapshot writes (uncommitted data visible within txn)
|
||
if (this.currentTxnId && this.txnSnapshot) {
|
||
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);
|
||
}
|
||
}
|
||
}
|
||
// 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));
|
||
}
|
||
return rows;
|
||
}
|
||
async update(tableName, query, updates) {
|
||
this.ensureOpen();
|
||
this.ensureTable(tableName);
|
||
const schema = this.schemas.get(tableName);
|
||
const rows = this.getAllRows(tableName);
|
||
let count = 0;
|
||
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);
|
||
if (this.currentTxnId && this.txnSnapshot) {
|
||
this.txnSnapshot.set(key, updated);
|
||
}
|
||
else {
|
||
this.lsm.put(key, updated);
|
||
}
|
||
count++;
|
||
this.wal.append({
|
||
type: WALRecordType.UPDATE,
|
||
txnId: this.currentTxnId ?? 0,
|
||
tableName,
|
||
key: String(row[pkCol]),
|
||
data: updated,
|
||
});
|
||
}
|
||
}
|
||
this.opCounter += count;
|
||
await this.checkpointManager.tick();
|
||
return count;
|
||
}
|
||
async delete(tableName, query) {
|
||
this.ensureOpen();
|
||
this.ensureTable(tableName);
|
||
const rows = this.getAllRows(tableName);
|
||
let count = 0;
|
||
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)) {
|
||
if (this.currentTxnId && this.txnSnapshot) {
|
||
// Buffer delete in snapshot
|
||
this.txnSnapshot.set(key, { __txn_deleted: true });
|
||
}
|
||
else {
|
||
this.lsm.delete(key);
|
||
}
|
||
count++;
|
||
this.wal.append({
|
||
type: WALRecordType.DELETE,
|
||
txnId: this.currentTxnId ?? 0,
|
||
tableName,
|
||
key: String(row[pkCol]),
|
||
});
|
||
}
|
||
}
|
||
this.opCounter += count;
|
||
await this.checkpointManager.tick();
|
||
return count;
|
||
}
|
||
async count(tableName, query) {
|
||
this.ensureOpen();
|
||
const rows = this.getAllRows(tableName);
|
||
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 = this.getAllRows(tableName);
|
||
for (const row of rows) {
|
||
const pkCol = this.tablePKs.get(tableName);
|
||
this.lsm.delete(`${tableName}:${row[pkCol]}`);
|
||
}
|
||
}
|
||
// =======================================================================
|
||
// 事务
|
||
// =======================================================================
|
||
async beginTransaction() {
|
||
if (this.currentTxnId)
|
||
throw new DatabaseError('Transaction already in progress', 'TX_ACTIVE');
|
||
this.currentTxnId = Date.now();
|
||
this.txnSnapshot = new Map();
|
||
this.wal.append({
|
||
type: WALRecordType.BEGIN,
|
||
txnId: this.currentTxnId,
|
||
tableName: '',
|
||
key: '',
|
||
});
|
||
}
|
||
async commitTransaction() {
|
||
if (!this.currentTxnId)
|
||
throw new DatabaseError('No active transaction', 'TX_NONE');
|
||
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.wal.append({
|
||
type: WALRecordType.COMMIT,
|
||
txnId: this.currentTxnId,
|
||
tableName: '',
|
||
key: '',
|
||
});
|
||
this.currentTxnId = null;
|
||
this.txnSnapshot = null;
|
||
await this.wal.flush();
|
||
}
|
||
async rollbackTransaction() {
|
||
if (!this.currentTxnId)
|
||
throw new DatabaseError('No active transaction', 'TX_NONE');
|
||
this.txnSnapshot = null;
|
||
this.wal.append({
|
||
type: WALRecordType.ROLLBACK,
|
||
txnId: this.currentTxnId,
|
||
tableName: '',
|
||
key: '',
|
||
});
|
||
this.currentTxnId = null;
|
||
}
|
||
// =======================================================================
|
||
// 内部
|
||
// =======================================================================
|
||
getAllRows(tableName) {
|
||
const pkCol = this.tablePKs.get(tableName);
|
||
const prefix = `${tableName}:`;
|
||
const entries = this.lsm.rangeScan(prefix, `${prefix}\uffff`);
|
||
return entries.map(([key, value]) => {
|
||
const row = { ...value };
|
||
row[pkCol] = key.slice(prefix.length);
|
||
return row;
|
||
});
|
||
}
|
||
tryIndexLookup(tableName, query) {
|
||
if (!query.where)
|
||
return null;
|
||
const pkCol = this.tablePKs.get(tableName);
|
||
for (const [col, condition] of Object.entries(query.where)) {
|
||
if (col !== pkCol)
|
||
continue;
|
||
// 等值条件
|
||
if (typeof condition !== 'object' || condition === null) {
|
||
const key = `${tableName}:${condition}`;
|
||
const value = this.lsm.get(key);
|
||
return value ? [{ ...value, [pkCol]: condition }] : [];
|
||
}
|
||
const cond = condition;
|
||
if ('$eq' in cond) {
|
||
const key = `${tableName}:${cond.$eq}`;
|
||
const value = this.lsm.get(key);
|
||
return value ? [{ ...value, [pkCol]: cond.$eq }] : [];
|
||
}
|
||
}
|
||
return null;
|
||
}
|
||
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);
|
||
}
|
||
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;
|
||
}
|
||
}
|
||
// =======================================================================
|
||
// 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 构建
|
||
// =======================================================================
|
||
createSSTableStore() {
|
||
const META_KEY = '__aria_lsm_meta';
|
||
return {
|
||
save: async (id, data) => {
|
||
const buf = data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength);
|
||
await this.backend.write(`sst_${id}`, buf);
|
||
},
|
||
load: async (id) => {
|
||
const buf = await this.backend.read(`sst_${id}`);
|
||
return buf ? new Uint8Array(buf) : null;
|
||
},
|
||
delete: async (id) => {
|
||
await this.backend.delete(`sst_${id}`);
|
||
},
|
||
allocateId: async () => Date.now(),
|
||
listMeta: async () => {
|
||
const raw = await this.backend.read(META_KEY);
|
||
if (!raw)
|
||
return [];
|
||
try {
|
||
return JSON.parse(new TextDecoder().decode(raw));
|
||
}
|
||
catch {
|
||
return [];
|
||
}
|
||
},
|
||
saveMeta: async (meta) => {
|
||
const existing = await this.backend.read(META_KEY);
|
||
const list = existing
|
||
? JSON.parse(new TextDecoder().decode(existing))
|
||
: [];
|
||
// 更新或添加
|
||
const idx = list.findIndex((m) => m.id === meta.id);
|
||
if (idx >= 0)
|
||
list[idx] = meta;
|
||
else
|
||
list.push(meta);
|
||
const json = JSON.stringify(list);
|
||
const buf = new TextEncoder().encode(json).buffer;
|
||
await this.backend.write(META_KEY, buf);
|
||
},
|
||
deleteMeta: async (id) => {
|
||
const existing = await this.backend.read(META_KEY);
|
||
if (!existing)
|
||
return;
|
||
const list = JSON.parse(new TextDecoder().decode(existing));
|
||
const filtered = list.filter((m) => m.id !== id);
|
||
const json = JSON.stringify(filtered);
|
||
const buf = new TextEncoder().encode(json).buffer;
|
||
await this.backend.write(META_KEY, buf);
|
||
},
|
||
};
|
||
}
|
||
// =======================================================================
|
||
// 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;
|
||
}
|
||
}
|
||
// =======================================================================
|
||
// 辅助
|
||
// =======================================================================
|
||
ensureOpen() {
|
||
if (!this.opened)
|
||
throw new DatabaseError('AriaEngine not opened', 'DB_NOT_OPEN');
|
||
}
|
||
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.memoryEngine = new MemoryEngine();
|
||
this.diskEngineType = diskEngine;
|
||
this.diskEngine = diskEngine === 'opfs' ? new OPFSEngine() : new IndexedDBEngine();
|
||
}
|
||
// ---- 生命周期 ----
|
||
async open(dbName, version) {
|
||
// 先打开磁盘引擎
|
||
await this.diskEngine.open(dbName, version);
|
||
// 再打开内存引擎
|
||
await this.memoryEngine.open(dbName, version);
|
||
// 从磁盘加载现存表
|
||
const tableNames = await this.diskEngine.getTableNames();
|
||
for (const tableName of tableNames) {
|
||
const schema = await this.diskEngine.getTableSchema(tableName);
|
||
if (!schema)
|
||
continue;
|
||
// 在内存中创建表
|
||
await this.memoryEngine.createTable(schema);
|
||
// 从磁盘加载数据到内存
|
||
const rows = await this.diskEngine.find(tableName, { table: tableName });
|
||
if (rows.length > 0) {
|
||
try {
|
||
await this.memoryEngine.insert(tableName, rows);
|
||
}
|
||
catch (e) {
|
||
// eslint-disable-next-line no-console
|
||
console.warn(`[metona-sqlark] Failed to load table "${tableName}" data from disk:`, e);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
async close() {
|
||
await this.memoryEngine.close();
|
||
await this.diskEngine.close();
|
||
}
|
||
isOpen() {
|
||
return this.memoryEngine.isOpen() && this.diskEngine.isOpen();
|
||
}
|
||
// ---- 表管理 ----
|
||
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);
|
||
}
|
||
// ---- CRUD(write-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);
|
||
}
|
||
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);
|
||
}
|
||
// ---- 事务 ----
|
||
async beginTransaction() {
|
||
await this.memoryEngine.beginTransaction();
|
||
await this.diskEngine.beginTransaction();
|
||
}
|
||
async commitTransaction() {
|
||
await this.memoryEngine.commitTransaction();
|
||
await this.diskEngine.commitTransaction();
|
||
}
|
||
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) {
|
||
this.engine = engine;
|
||
this.tableName = tableName;
|
||
this._updates = _updates;
|
||
this._where = {};
|
||
}
|
||
where(condition) {
|
||
this._where = { ...this._where, ...condition };
|
||
return this;
|
||
}
|
||
async execute() {
|
||
return this.engine.update(this.tableName, { table: this.tableName, where: this._where }, this._updates);
|
||
}
|
||
toAST() {
|
||
return { type: 'UPDATE', table: this.tableName, sets: this._updates, where: this._where };
|
||
}
|
||
}
|
||
// ---------------------------------------------------------------------------
|
||
// DeleteQueryBuilder
|
||
// ---------------------------------------------------------------------------
|
||
class DeleteQueryBuilder {
|
||
constructor(engine, tableName) {
|
||
this.engine = engine;
|
||
this.tableName = tableName;
|
||
this._where = {};
|
||
}
|
||
where(condition) {
|
||
this._where = { ...this._where, ...condition };
|
||
return this;
|
||
}
|
||
async execute() {
|
||
return this.engine.delete(this.tableName, { table: this.tableName, where: this._where });
|
||
}
|
||
toAST() {
|
||
return { type: 'DELETE', from: this.tableName, where: this._where };
|
||
}
|
||
}
|
||
|
||
/**
|
||
* metona-sqlark Table — 表操作 API
|
||
* @module table/table
|
||
*/
|
||
// ---------------------------------------------------------------------------
|
||
// Table
|
||
// ---------------------------------------------------------------------------
|
||
class Table {
|
||
constructor(engine, tableName, executor) {
|
||
this.schema = null;
|
||
this.engine = engine;
|
||
this.name = tableName;
|
||
this.executor = executor;
|
||
}
|
||
// ---- 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]);
|
||
return pks[0];
|
||
}
|
||
async insertMany(rows) {
|
||
return this.engine.insert(this.name, rows);
|
||
}
|
||
// ---- 查询 ----
|
||
select(columns = ['*']) {
|
||
return new SelectQueryBuilder(this.engine, this.name, columns, this.executor);
|
||
}
|
||
// ---- 更新 ----
|
||
update(updates) {
|
||
return new UpdateQueryBuilder(this.engine, this.name, updates);
|
||
}
|
||
// ---- 删除 ----
|
||
delete() {
|
||
return new DeleteQueryBuilder(this.engine, this.name);
|
||
}
|
||
// ---- 聚合 ----
|
||
async count(where) {
|
||
return this.engine.count(this.name, where ? { table: this.name, where } : undefined);
|
||
}
|
||
// ---- 管理 ----
|
||
async clear() {
|
||
return this.engine.clear(this.name);
|
||
}
|
||
async drop() {
|
||
return this.engine.dropTable(this.name);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 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');
|
||
}
|
||
}
|
||
/** 将 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,
|
||
};
|
||
}
|
||
|
||
/**
|
||
* 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 Query Executor — AST 执行器
|
||
* @module query/executor
|
||
*
|
||
* JOIN / GROUP BY / DISTINCT 逻辑在此层处理。
|
||
*/
|
||
// ---------------------------------------------------------------------------
|
||
// Executor
|
||
// ---------------------------------------------------------------------------
|
||
class QueryExecutor {
|
||
constructor(engine) {
|
||
this.engine = engine;
|
||
}
|
||
async execute(stmt) {
|
||
switch (stmt.type) {
|
||
case 'SELECT': return this.executeSelect(stmt);
|
||
case 'INSERT': return this.executeInsert(stmt);
|
||
case 'UPDATE': return this.executeUpdate(stmt);
|
||
case 'DELETE': return this.executeDelete(stmt);
|
||
case 'CREATE_TABLE': return this.executeCreateTable(stmt);
|
||
case 'DROP_TABLE': return this.executeDropTable(stmt);
|
||
default: throw new DatabaseError('Unknown statement type', 'UNKNOWN_STATEMENT');
|
||
}
|
||
}
|
||
// ===================================================================
|
||
// SELECT
|
||
// ===================================================================
|
||
async executeSelect(stmt) {
|
||
// 先解析子查询
|
||
if (stmt.where && Object.keys(stmt.where).length > 0) {
|
||
stmt.where = await this.resolveSubqueries(stmt.where);
|
||
}
|
||
const hasGroupBy = !!(stmt.groupBy && stmt.groupBy.length > 0);
|
||
const hasAggregate = !hasGroupBy && this._hasAggregateColumn(stmt.columns);
|
||
let rows;
|
||
if (!stmt.joins || stmt.joins.length === 0) {
|
||
const plan = compileStatement(hasGroupBy || hasAggregate ? { ...stmt, columns: ['*'] } : stmt);
|
||
rows = await this.engine.find(plan.table, plan);
|
||
}
|
||
else {
|
||
rows = await this.executeJoinSelect(stmt);
|
||
}
|
||
// 无 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) {
|
||
rows = rows.filter((row) => matchWhere(row, stmt.having));
|
||
}
|
||
if (stmt.orderBy && stmt.orderBy.length > 0)
|
||
rows = applyOrderBy(rows, stmt.orderBy);
|
||
const offset = stmt.offset ?? 0;
|
||
const limit = stmt.limit ?? rows.length;
|
||
rows = rows.slice(offset, offset + limit);
|
||
if (!hasGroupBy && !hasAggregate && stmt.columns.length > 0 && stmt.columns[0] !== '*') {
|
||
rows = rows.map((row) => projectColumns(row, stmt.columns));
|
||
}
|
||
return rows;
|
||
}
|
||
// ---- JOIN ----
|
||
async executeJoinSelect(stmt) {
|
||
const mainAlias = stmt.alias ?? stmt.from;
|
||
const mainRows = (await this.engine.find(stmt.from, { table: stmt.from }))
|
||
.map((row) => this.prefixRow(row, mainAlias));
|
||
let resultRows = mainRows;
|
||
for (const join of stmt.joins) {
|
||
const joinAlias = join.alias ?? join.table;
|
||
const joinRows = (await this.engine.find(join.table, { table: join.table }))
|
||
.map((row) => this.prefixRow(row, joinAlias));
|
||
resultRows = this.joinRows(resultRows, joinRows, join);
|
||
}
|
||
if (stmt.where && Object.keys(stmt.where).length > 0) {
|
||
resultRows = resultRows.filter((row) => matchWhere(row, stmt.where));
|
||
}
|
||
return resultRows;
|
||
}
|
||
prefixRow(row, alias) {
|
||
const prefixed = {};
|
||
for (const [key, value] of Object.entries(row))
|
||
prefixed[`${alias}.${key}`] = value;
|
||
return prefixed;
|
||
}
|
||
/** 嵌套循环连接(优化:避免 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 = [];
|
||
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;
|
||
aggregated[alias || colExpr] = this.computeAggregate(func.toUpperCase(), groupRows, arg.trim());
|
||
}
|
||
else if (!stmt.groupBy.includes(colExpr)) {
|
||
aggregated[colExpr] = groupRows[0][colExpr];
|
||
}
|
||
}
|
||
result.push(aggregated);
|
||
}
|
||
return result;
|
||
}
|
||
computeAggregate(func, rows, col) {
|
||
const nums = rows.map((r) => r[col]).filter((v) => v !== null && v !== undefined).map(Number);
|
||
switch (func) {
|
||
case 'COUNT': return col === '*' ? rows.length : nums.length;
|
||
case 'SUM': return nums.reduce((a, b) => a + b, 0);
|
||
case 'AVG': return nums.length === 0 ? 0 : nums.reduce((a, b) => a + b, 0) / nums.length;
|
||
case 'MIN': return nums.length === 0 ? 0 : Math.min(...nums);
|
||
case 'MAX': return nums.length === 0 ? 0 : Math.max(...nums);
|
||
default: return 0;
|
||
}
|
||
}
|
||
// ---- DISTINCT(优化:列值拼接代替 JSON.stringify) ----
|
||
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);
|
||
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);
|
||
}
|
||
getEngine() { return this.engine; }
|
||
// ===================================================================
|
||
// 无 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;
|
||
}
|
||
// ===================================================================
|
||
// 子查询解析
|
||
// ===================================================================
|
||
/**
|
||
* 递归扫描 WHERE 条件,找到 $subquery 标记并执行子查询,
|
||
* 将结果替换为具体值。
|
||
*/
|
||
async resolveSubqueries(where) {
|
||
const resolved = {};
|
||
for (const [key, value] of Object.entries(where)) {
|
||
// 逻辑组合操作符
|
||
if (key === '$and' && Array.isArray(value)) {
|
||
resolved.$and = await Promise.all(value.map((sub) => this.resolveSubqueries(sub)));
|
||
continue;
|
||
}
|
||
if (key === '$or' && Array.isArray(value)) {
|
||
resolved.$or = await Promise.all(value.map((sub) => this.resolveSubqueries(sub)));
|
||
continue;
|
||
}
|
||
if (key === '$not' && typeof value === 'object' && value !== null) {
|
||
resolved.$not = await this.resolveSubqueries(value);
|
||
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 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";
|
||
// 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";
|
||
// 标识符 & 字面量
|
||
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,
|
||
// 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,
|
||
};
|
||
|
||
/**
|
||
* 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 !== quote && this.ch !== '') {
|
||
// 处理转义
|
||
if (this.ch === '\\' && this.peekChar() === quote) {
|
||
this.readChar();
|
||
value += quote;
|
||
}
|
||
else {
|
||
value += this.ch;
|
||
}
|
||
this.readChar();
|
||
}
|
||
// 跳过结束引号(在 readChar 之后才会调用,所以这里不需要处理)
|
||
return {
|
||
type: TokenType.STRING,
|
||
value,
|
||
position: start,
|
||
};
|
||
}
|
||
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.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.parseCreateTable();
|
||
case TokenType.DROP:
|
||
return this.parseDropTable();
|
||
default:
|
||
throw this.error(`Unexpected token "${this.curToken.value}"`);
|
||
}
|
||
}
|
||
// ===================================================================
|
||
// 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());
|
||
}
|
||
// FROM
|
||
this.expect(TokenType.FROM);
|
||
const tableName = this.expectIdentifier('table name');
|
||
// 表别名(可选)
|
||
let alias;
|
||
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: {},
|
||
};
|
||
// JOIN 子句(可选,支持多个)
|
||
const joins = this.parseJoinClauses();
|
||
if (joins.length > 0) {
|
||
stmt.joins = joins;
|
||
}
|
||
// WHERE(可选)
|
||
if (this.curTokenIs(TokenType.WHERE)) {
|
||
this.nextToken();
|
||
stmt.where = this.parseCondition();
|
||
}
|
||
// GROUP BY(可选)
|
||
if (this.curTokenIs(TokenType.GROUP)) {
|
||
this.nextToken();
|
||
this.expect(TokenType.BY);
|
||
stmt.groupBy = this.parseIdentifierList();
|
||
}
|
||
// HAVING(可选)
|
||
if (this.curTokenIs(TokenType.HAVING)) {
|
||
this.nextToken();
|
||
stmt.having = this.parseCondition();
|
||
}
|
||
// ORDER BY(可选)
|
||
if (this.curTokenIs(TokenType.ORDER)) {
|
||
this.nextToken();
|
||
this.expect(TokenType.BY);
|
||
stmt.orderBy = this.parseOrderByList();
|
||
}
|
||
// LIMIT(可选)
|
||
if (this.curTokenIs(TokenType.LIMIT)) {
|
||
this.nextToken();
|
||
stmt.limit = this.expectNumber('LIMIT value');
|
||
}
|
||
// OFFSET(可选)
|
||
if (this.curTokenIs(TokenType.OFFSET)) {
|
||
this.nextToken();
|
||
stmt.offset = this.expectNumber('OFFSET value');
|
||
}
|
||
return stmt;
|
||
}
|
||
/** 解析 JOIN 子句列表 */
|
||
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);
|
||
}
|
||
// 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.CREATE);
|
||
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';
|
||
}
|
||
// ===================================================================
|
||
// DROP TABLE
|
||
// ===================================================================
|
||
parseDropTable() {
|
||
this.expect(TokenType.DROP);
|
||
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;
|
||
}
|
||
/** simple_cond → column op value | column IS [NOT] NULL | column [NOT] LIKE pattern
|
||
* | column [NOT] IN (values) | NOT condition | (condition) */
|
||
parseSimpleCondition() {
|
||
// 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 IN(NOT 后紧跟 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;
|
||
}
|
||
// 比较运算符
|
||
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;
|
||
}
|
||
/** 判断当前 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.parseColumnRef());
|
||
while (this.curTokenIs(TokenType.COMMA)) {
|
||
this.nextToken();
|
||
cols.push(this.parseColumnRef());
|
||
}
|
||
return cols;
|
||
}
|
||
/** 解析列引用:支持 'col'、'table.col' 和 'COUNT(*)'/'SUM(col)' 等 */
|
||
parseColumnRef() {
|
||
// 聚合函数?
|
||
if (this.curTokenIs(TokenType.COUNT) ||
|
||
this.curTokenIs(TokenType.SUM) ||
|
||
this.curTokenIs(TokenType.AVG) ||
|
||
this.curTokenIs(TokenType.MIN) ||
|
||
this.curTokenIs(TokenType.MAX)) {
|
||
return this.parseAggregateCall();
|
||
}
|
||
const first = this.expectIdentifier('column name');
|
||
if (this.curTokenIs(TokenType.DOT)) {
|
||
this.nextToken();
|
||
const second = this.expectIdentifier('column name');
|
||
return `${first}.${second}`;
|
||
}
|
||
return first;
|
||
}
|
||
/** 解析聚合函数调用: COUNT(*), SUM(col), AVG(col), MIN(col), MAX(col) */
|
||
parseAggregateCall() {
|
||
const func = this.curToken.value.toUpperCase();
|
||
this.nextToken();
|
||
this.expect(TokenType.LPAREN);
|
||
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();
|
||
}
|
||
if (alias) {
|
||
return `${func}(${arg}) AS ${alias}`;
|
||
}
|
||
return `${func}(${arg})`;
|
||
}
|
||
_isAggregateAlias() {
|
||
return !this._isReservedAfterFrom() && !this._isJoinKeyword();
|
||
}
|
||
parseIdentifierList() {
|
||
const ids = [];
|
||
ids.push(this.expectIdentifier('identifier'));
|
||
while (this.curTokenIs(TokenType.COMMA)) {
|
||
this.nextToken();
|
||
ids.push(this.expectIdentifier('identifier'));
|
||
}
|
||
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.expectIdentifier('column name');
|
||
let direction = 'asc';
|
||
if (this.curTokenIs(TokenType.ASC)) {
|
||
this.nextToken();
|
||
}
|
||
else if (this.curTokenIs(TokenType.DESC)) {
|
||
direction = 'desc';
|
||
this.nextToken();
|
||
}
|
||
return { column, direction };
|
||
}
|
||
/** 解析字面量值 */
|
||
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;
|
||
}
|
||
|
||
/**
|
||
* 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) {
|
||
// 按优先级插入
|
||
const priority = plugin.priority ?? 0;
|
||
const insertIndex = this.plugins.findIndex((p) => (p.priority ?? 0) < priority);
|
||
if (insertIndex === -1) {
|
||
this.plugins.push(plugin);
|
||
}
|
||
else {
|
||
this.plugins.splice(insertIndex, 0, plugin);
|
||
}
|
||
// 安装
|
||
plugin.install(null); // 实际引用由 MetonaSqlark 注入
|
||
}
|
||
/** 卸载插件 */
|
||
unregister(pluginName) {
|
||
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; }
|
||
constructor(config) {
|
||
this.ready = false;
|
||
this.tableCache = new Map();
|
||
// ---- 发布订阅 ----
|
||
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();
|
||
}
|
||
// ---- 初始化 ----
|
||
/** 初始化数据库(创建引擎、打开连接) */
|
||
async init() {
|
||
// 创建引擎
|
||
this.engine = this.createEngine();
|
||
// 打开连接
|
||
await this.engine.open(this.name, this.version);
|
||
// 初始化执行器和事务管理器
|
||
this.executor = new QueryExecutor(this.engine);
|
||
this.transactionManager = new TransactionManager(this.engine);
|
||
// 注册插件
|
||
if (this.config.plugins) {
|
||
for (const plugin of this.config.plugins) {
|
||
this.pluginManager.register(plugin);
|
||
}
|
||
}
|
||
this.ready = true;
|
||
// 回调
|
||
if (this.config.onReady) {
|
||
this.config.onReady(this);
|
||
}
|
||
}
|
||
/** 检查是否就绪 */
|
||
isReady() {
|
||
return this.ready;
|
||
}
|
||
// ---- 表管理 ----
|
||
/** 创建表 */
|
||
async defineTable(name, columns) {
|
||
this.ensureReady();
|
||
const schema = createSchema(name, columns);
|
||
await this.pluginManager.trigger('beforeCreateTable', schema);
|
||
await this.engine.createTable(schema);
|
||
await this.pluginManager.trigger('afterCreateTable', schema);
|
||
// 清除缓存
|
||
this.tableCache.delete(name);
|
||
}
|
||
/** 获取表操作对象 */
|
||
table(name) {
|
||
this.ensureReady();
|
||
let t = this.tableCache.get(name);
|
||
if (!t) {
|
||
t = new Table(this.engine, name, this.executor);
|
||
this.tableCache.set(name, t);
|
||
}
|
||
return t;
|
||
}
|
||
/** 删除表 */
|
||
async dropTable(name) {
|
||
this.ensureReady();
|
||
await this.pluginManager.trigger('beforeDropTable', name);
|
||
await this.engine.dropTable(name);
|
||
await this.pluginManager.trigger('afterDropTable', name);
|
||
this.tableCache.delete(name);
|
||
}
|
||
/** 获取所有表名 */
|
||
async getTableNames() {
|
||
this.ensureReady();
|
||
return this.engine.getTableNames();
|
||
}
|
||
// ---- SQL 查询 ----
|
||
/** 执行 SQL 字符串查询 */
|
||
async query(sql) {
|
||
this.ensureReady();
|
||
await this.pluginManager.trigger('beforeQuery', sql);
|
||
const stmt = parse(sql);
|
||
const result = await this.executor.execute(stmt);
|
||
await this.pluginManager.trigger('afterQuery', sql, result);
|
||
return result;
|
||
}
|
||
// ---- 事务 ----
|
||
/** 执行事务 */
|
||
async transaction(fn) {
|
||
this.ensureReady();
|
||
await this.pluginManager.trigger('beforeTransaction');
|
||
const result = await this.transactionManager.execute(fn);
|
||
await this.pluginManager.trigger('afterTransaction');
|
||
return result;
|
||
}
|
||
// ---- 导入导出 ----
|
||
/** 导出表数据为 JSON */
|
||
async exportTable(tableName) {
|
||
this.ensureReady();
|
||
return this.engine.find(tableName, { table: tableName });
|
||
}
|
||
/** 导入 JSON 数据到表 */
|
||
async importTable(tableName, data) {
|
||
this.ensureReady();
|
||
return this.engine.insert(tableName, data);
|
||
}
|
||
/** 导出整个数据库为 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));
|
||
}
|
||
/** 注册迁移 */
|
||
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;
|
||
}
|
||
}
|
||
}
|
||
// ---- 插件 ----
|
||
/** 获取插件管理器 */
|
||
getPluginManager() {
|
||
return this.pluginManager;
|
||
}
|
||
/** 注册钩子 */
|
||
on(hook, callback) {
|
||
this.pluginManager.on(hook, callback);
|
||
}
|
||
// ---- 生命周期 ----
|
||
/** 关闭数据库 */
|
||
async close() {
|
||
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' ? 'memory' : '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');
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 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.1.12
|
||
*
|
||
* 前端关系型数据库,内存与磁盘双模式。
|
||
* 支持 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;
|
||
|
||
export { AriaEngine, HybridEngine, IndexedDBEngine, MeSqlark, MemoryEngine, MetonaSqlark, OPFSEngine, Table, VERSION, api, create, api as default, parse, tokenize };
|
||
//# sourceMappingURL=metona-sqlark.esm.js.map
|