release: v0.2.5 — 质量加固 + Bug修复 + 性能优化 + SQL扩展
This commit is contained in:
Vendored
+346
-50
@@ -33,7 +33,7 @@ class DatabaseError extends Error {
|
||||
// ---------------------------------------------------------------------------
|
||||
// 版本
|
||||
// ---------------------------------------------------------------------------
|
||||
const VERSION = '0.2.0';
|
||||
const VERSION = '0.2.5';
|
||||
|
||||
/**
|
||||
* metona-sqlark Shared WHERE Matcher — 统一的条件匹配逻辑
|
||||
@@ -720,6 +720,13 @@ class IndexedDBEngine {
|
||||
}
|
||||
async idbFind(tableName, query) {
|
||||
const db = this.ensureDB();
|
||||
// 尝试使用 IDB 索引进行等值查询
|
||||
if (query.where) {
|
||||
const indexResult = await this.tryIDBIndexLookup(db, tableName, query);
|
||||
if (indexResult !== null)
|
||||
return indexResult;
|
||||
}
|
||||
// 回退到全量 getAll + 内存过滤
|
||||
return new Promise((resolve, reject) => {
|
||||
const tx = db.transaction(tableName, 'readonly');
|
||||
const req = tx.objectStore(tableName).getAll();
|
||||
@@ -742,6 +749,64 @@ class IndexedDBEngine {
|
||||
req.onerror = () => reject(new DatabaseError(`Find failed for "${tableName}"`, 'IDB_READ_ERROR', req.error));
|
||||
});
|
||||
}
|
||||
/** 尝试使用 IDB 索引进行等值查询,成功返回结果,不适用返回 null */
|
||||
async tryIDBIndexLookup(db, tableName, query) {
|
||||
if (!query.where)
|
||||
return null;
|
||||
for (const [col, condition] of Object.entries(query.where)) {
|
||||
// 跳过逻辑组合符
|
||||
if (col === '$and' || col === '$or' || col === '$not')
|
||||
continue;
|
||||
// 只处理等值查询
|
||||
let targetValue;
|
||||
if (typeof condition !== 'object' || condition === null) {
|
||||
targetValue = condition;
|
||||
}
|
||||
else if ('$eq' in condition) {
|
||||
targetValue = condition.$eq;
|
||||
}
|
||||
else {
|
||||
continue;
|
||||
}
|
||||
// 检查是否有对应的 IDB 索引
|
||||
const indexName = `idx_${col}`;
|
||||
try {
|
||||
return await new Promise((resolve, reject) => {
|
||||
const tx = db.transaction(tableName, 'readonly');
|
||||
const store = tx.objectStore(tableName);
|
||||
if (!store.indexNames.contains(indexName)) {
|
||||
resolve(null); // 没有索引,回退
|
||||
return;
|
||||
}
|
||||
const index = store.index(indexName);
|
||||
const req = index.getAll(targetValue);
|
||||
req.onsuccess = () => {
|
||||
let results = req.result ?? [];
|
||||
// 如果有其他 WHERE 条件,继续过滤
|
||||
const otherKeys = Object.keys(query.where).filter((k) => k !== col && k !== '$and' && k !== '$or' && k !== '$not');
|
||||
if (otherKeys.length > 0) {
|
||||
results = results.filter((row) => matchWhere(row, query.where));
|
||||
}
|
||||
if (query.orderBy && query.orderBy.length > 0) {
|
||||
results = applyOrderBy(results, query.orderBy);
|
||||
}
|
||||
const offset = query.offset ?? 0;
|
||||
const limit = query.limit ?? results.length;
|
||||
results = results.slice(offset, offset + limit);
|
||||
if (query.columns && query.columns.length > 0 && query.columns[0] !== '*') {
|
||||
results = results.map((r) => projectColumns(r, query.columns));
|
||||
}
|
||||
resolve(results);
|
||||
};
|
||||
req.onerror = () => reject(new DatabaseError(`Index lookup failed for "${tableName}.${col}"`, 'IDB_READ_ERROR', req.error));
|
||||
});
|
||||
}
|
||||
catch {
|
||||
return null; // 索引不可用,回退
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
async idbUpdate(tableName, query, updates) {
|
||||
const db = this.ensureDB();
|
||||
return new Promise((resolve, reject) => {
|
||||
@@ -2171,18 +2236,26 @@ class SSTableReader {
|
||||
return -1;
|
||||
}
|
||||
locateBlockGE(key) {
|
||||
for (let i = 0; i < this.indexEntries.length; i++) {
|
||||
if (this.indexEntries[i].key >= key)
|
||||
return i;
|
||||
let lo = 0, hi = this.indexEntries.length;
|
||||
while (lo < hi) {
|
||||
const mid = (lo + hi) >> 1;
|
||||
if (this.indexEntries[mid].key < key)
|
||||
lo = mid + 1;
|
||||
else
|
||||
hi = mid;
|
||||
}
|
||||
return this.indexEntries.length - 1;
|
||||
return lo < this.indexEntries.length ? lo : this.indexEntries.length - 1;
|
||||
}
|
||||
locateBlockLE(key) {
|
||||
for (let i = this.indexEntries.length - 1; i >= 0; i--) {
|
||||
if (this.indexEntries[i].key <= key)
|
||||
return i;
|
||||
let lo = 0, hi = this.indexEntries.length;
|
||||
while (lo < hi) {
|
||||
const mid = (lo + hi) >> 1;
|
||||
if (this.indexEntries[mid].key <= key)
|
||||
lo = mid + 1;
|
||||
else
|
||||
hi = mid;
|
||||
}
|
||||
return 0;
|
||||
return lo > 0 ? lo - 1 : 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2558,7 +2631,11 @@ class LSM {
|
||||
// =======================================================================
|
||||
// Compaction
|
||||
// =======================================================================
|
||||
/** 同步执行 Compaction(简化版,仅供内部调用) */
|
||||
/** 同步执行 Compaction(public,供 VACUUM 等外部调用) */
|
||||
compactLevel(level) {
|
||||
this.compactLevelSync(level);
|
||||
}
|
||||
/** 同步执行 Compaction(简化版,内部实现) */
|
||||
compactLevelSync(level) {
|
||||
if (level >= MAX_LSM_LEVELS - 1)
|
||||
return;
|
||||
@@ -2711,8 +2788,8 @@ class WAL {
|
||||
// =======================================================================
|
||||
// 写入
|
||||
// =======================================================================
|
||||
/** 追加一条 WAL 记录 */
|
||||
append(record) {
|
||||
/** 追加一条 WAL 记录(full 模式同步等待写入完成) */
|
||||
async append(record) {
|
||||
if (!this.enabled)
|
||||
return;
|
||||
this.lsn++;
|
||||
@@ -2723,10 +2800,13 @@ class WAL {
|
||||
};
|
||||
const bytes = this.encodeRecord(fullRecord);
|
||||
if (this.syncMode === 'full') {
|
||||
this.store.append(bytes).catch(() => {
|
||||
try {
|
||||
await this.store.append(bytes);
|
||||
}
|
||||
catch {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn('[AriaEngine WAL] Failed to append record');
|
||||
});
|
||||
}
|
||||
}
|
||||
else if (this.syncMode === 'batch') {
|
||||
this.buffer.push(bytes);
|
||||
@@ -2913,19 +2993,26 @@ class WAL {
|
||||
// CheckpointManager
|
||||
// ---------------------------------------------------------------------------
|
||||
class CheckpointManager {
|
||||
constructor(lsm, wal, flushable = null, interval = 1000) {
|
||||
constructor(lsm, wal, flushable = null, interval = 1000, walSizeThreshold = 16 * 1024 * 1024) {
|
||||
this.opCount = 0;
|
||||
this.lsm = lsm;
|
||||
this.wal = wal;
|
||||
this.flushable = flushable;
|
||||
this.interval = interval;
|
||||
this.walSizeThreshold = walSizeThreshold;
|
||||
}
|
||||
async tick() {
|
||||
this.opCount++;
|
||||
if (this.opCount >= this.interval) {
|
||||
// 检查操作计数或 WAL 大小是否超阈值
|
||||
if (this.opCount >= this.interval || this.getWALEstimatedSize() >= this.walSizeThreshold) {
|
||||
await this.checkpoint();
|
||||
}
|
||||
}
|
||||
/** 估算 WAL 大小 */
|
||||
getWALEstimatedSize() {
|
||||
const count = typeof this.wal.getBufferedCount === 'function' ? this.wal.getBufferedCount() : 0;
|
||||
return count * 200;
|
||||
}
|
||||
async checkpoint() {
|
||||
await this.lsm.flush();
|
||||
if (this.flushable) {
|
||||
@@ -3907,11 +3994,70 @@ function decompressLZ4(input, originalSize) {
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* AriaEngine Crypto — 页面级 AES-GCM 加密
|
||||
* @module engine/aria/crypto
|
||||
*
|
||||
* v0.2.5: 改为实例化 CryptoManager,避免多实例共享全局状态。
|
||||
* 保留全局函数兼容旧代码(委托给全局单例)。
|
||||
*/
|
||||
const ALGO = 'AES-GCM';
|
||||
const IV_LENGTH = 12;
|
||||
/**
|
||||
* CryptoManager — 实例级加密管理器
|
||||
* 每个 AriaEngine 实例可拥有独立的加密配置。
|
||||
*/
|
||||
class CryptoManager {
|
||||
constructor() {
|
||||
this.cryptoKey = null;
|
||||
this._enabled = false;
|
||||
}
|
||||
get enabled() { return this._enabled; }
|
||||
async init(password, salt) {
|
||||
const enc = new TextEncoder();
|
||||
const keyMaterial = await crypto.subtle.importKey('raw', enc.encode(password), 'PBKDF2', false, ['deriveKey']);
|
||||
const actualSalt = salt || crypto.getRandomValues(new Uint8Array(16));
|
||||
this.cryptoKey = await crypto.subtle.deriveKey({ name: 'PBKDF2', salt: actualSalt, iterations: 100000, hash: 'SHA-256' }, keyMaterial, { name: ALGO, length: 256 }, false, ['encrypt', 'decrypt']);
|
||||
this._enabled = true;
|
||||
return actualSalt;
|
||||
}
|
||||
async encryptPage(data) {
|
||||
if (!this.cryptoKey)
|
||||
throw new Error('Crypto not initialized');
|
||||
const iv = crypto.getRandomValues(new Uint8Array(IV_LENGTH));
|
||||
const ciphertext = await crypto.subtle.encrypt({ name: ALGO, iv }, this.cryptoKey, data);
|
||||
return { iv: iv, data: ciphertext };
|
||||
}
|
||||
async decryptPage(iv, data) {
|
||||
if (!this.cryptoKey)
|
||||
throw new Error('Crypto not initialized');
|
||||
return crypto.subtle.decrypt({ name: ALGO, iv }, this.cryptoKey, data);
|
||||
}
|
||||
close() {
|
||||
this.cryptoKey = null;
|
||||
this._enabled = false;
|
||||
}
|
||||
}
|
||||
// ---------------------------------------------------------------------------
|
||||
// 全局兼容层(旧代码仍可使用全局函数)
|
||||
// ---------------------------------------------------------------------------
|
||||
const globalCrypto = new CryptoManager();
|
||||
/** @deprecated 使用 CryptoManager 实例代替 */
|
||||
function isCryptoEnabled() { return globalCrypto.enabled; }
|
||||
/** @deprecated 使用 CryptoManager 实例代替 */
|
||||
async function encryptPage(data) {
|
||||
return globalCrypto.encryptPage(data);
|
||||
}
|
||||
/** @deprecated 使用 CryptoManager 实例代替 */
|
||||
async function decryptPage(iv, data) {
|
||||
return globalCrypto.decryptPage(iv, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* AriaEngine — 自研页面式存储引擎主类
|
||||
* @module engine/aria/index
|
||||
*
|
||||
* v0.2.4: 二级索引 + MVCC 集成 + 生产加固
|
||||
* v0.2.5: WAL 同步修复 + MVCC 接入 + 版本统一 + 生产加固
|
||||
*/
|
||||
// ---------------------------------------------------------------------------
|
||||
// AriaEngine
|
||||
@@ -4031,8 +4177,8 @@ class AriaEngine {
|
||||
this.applyWALRecord(r);
|
||||
}
|
||||
}
|
||||
// 8. Checkpoint Manager(BufferPool 暂简化,使用 flush 替代)
|
||||
this.checkpointManager = new CheckpointManager(this.lsm, this.wal, { flushAll: async () => { await this.lsm.flush(); } }, this.config.checkpointInterval);
|
||||
// 8. Checkpoint Manager(接入 WAL 大小阈值)
|
||||
this.checkpointManager = new CheckpointManager(this.lsm, this.wal, { flushAll: async () => { await this.lsm.flush(); } }, this.config.checkpointInterval, this.config.walSizeThreshold);
|
||||
this.opened = true;
|
||||
}
|
||||
async close() {
|
||||
@@ -4075,7 +4221,7 @@ class AriaEngine {
|
||||
}
|
||||
}
|
||||
await this.persistSchemas();
|
||||
this.wal.append({
|
||||
await this.wal.append({
|
||||
type: WALRecordType.CREATE_TABLE,
|
||||
txnId: 0,
|
||||
tableName: schema.name,
|
||||
@@ -4095,7 +4241,7 @@ class AriaEngine {
|
||||
this.schemas.delete(tableName);
|
||||
this.tablePKs.delete(tableName);
|
||||
await this.persistSchemas();
|
||||
this.wal.append({
|
||||
await this.wal.append({
|
||||
type: WALRecordType.DROP_TABLE,
|
||||
txnId: 0,
|
||||
tableName,
|
||||
@@ -4132,8 +4278,9 @@ class AriaEngine {
|
||||
throw new DatabaseError(`Duplicate primary key "${pkValue}" in table "${tableName}"`, 'DUPLICATE_KEY');
|
||||
}
|
||||
if (this.currentTxnId && this.txnSnapshot) {
|
||||
// Within transaction: buffer to snapshot
|
||||
// Within transaction: buffer to snapshot + MVCC version chain
|
||||
this.txnSnapshot.set(key, validated);
|
||||
this.mvcc.writeVersion(tableName, pkValue, validated, this.currentTxnId);
|
||||
}
|
||||
else {
|
||||
// Direct write to LSM (PK index)
|
||||
@@ -4142,7 +4289,7 @@ class AriaEngine {
|
||||
// 更新二级索引
|
||||
this.updateSecondaryIndexes(tableName, pkValue, validated, null);
|
||||
pks.push(pkValue);
|
||||
this.wal.append({
|
||||
await this.wal.append({
|
||||
type: WALRecordType.INSERT,
|
||||
txnId: this.currentTxnId ?? 0,
|
||||
tableName,
|
||||
@@ -4223,12 +4370,13 @@ class AriaEngine {
|
||||
this.validateRow(schema, updated);
|
||||
if (this.currentTxnId && this.txnSnapshot) {
|
||||
this.txnSnapshot.set(key, updated);
|
||||
this.mvcc.writeVersion(tableName, String(row[pkCol]), updated, this.currentTxnId);
|
||||
}
|
||||
else {
|
||||
this.lsm.put(key, updated);
|
||||
}
|
||||
count++;
|
||||
this.wal.append({
|
||||
await this.wal.append({
|
||||
type: WALRecordType.UPDATE,
|
||||
txnId: this.currentTxnId ?? 0,
|
||||
tableName,
|
||||
@@ -4253,14 +4401,15 @@ class AriaEngine {
|
||||
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
|
||||
// Buffer delete in snapshot + MVCC tombstone
|
||||
this.txnSnapshot.set(key, { __txn_deleted: true });
|
||||
this.mvcc.deleteVersion(tableName, String(row[pkCol]), this.currentTxnId);
|
||||
}
|
||||
else {
|
||||
this.lsm.delete(key);
|
||||
}
|
||||
count++;
|
||||
this.wal.append({
|
||||
await this.wal.append({
|
||||
type: WALRecordType.DELETE,
|
||||
txnId: this.currentTxnId ?? 0,
|
||||
tableName,
|
||||
@@ -4299,7 +4448,7 @@ class AriaEngine {
|
||||
throw new DatabaseError('Transaction already in progress', 'TX_ACTIVE');
|
||||
this.currentTxnId = this.mvcc.beginTransaction();
|
||||
this.txnSnapshot = new Map();
|
||||
this.wal.append({
|
||||
await this.wal.append({
|
||||
type: WALRecordType.BEGIN,
|
||||
txnId: this.currentTxnId,
|
||||
tableName: '',
|
||||
@@ -4320,7 +4469,7 @@ class AriaEngine {
|
||||
}
|
||||
}
|
||||
this.mvcc.commitTransaction(this.currentTxnId);
|
||||
this.wal.append({
|
||||
await this.wal.append({
|
||||
type: WALRecordType.COMMIT,
|
||||
txnId: this.currentTxnId,
|
||||
tableName: '',
|
||||
@@ -4335,7 +4484,7 @@ class AriaEngine {
|
||||
throw new DatabaseError('No active transaction', 'TX_NONE');
|
||||
this.mvcc.rollbackTransaction(this.currentTxnId);
|
||||
this.txnSnapshot = null;
|
||||
this.wal.append({
|
||||
await this.wal.append({
|
||||
type: WALRecordType.ROLLBACK,
|
||||
txnId: this.currentTxnId,
|
||||
tableName: '',
|
||||
@@ -4467,6 +4616,17 @@ class AriaEngine {
|
||||
const compressed = compressLZ4(new Uint8Array(buf));
|
||||
buf = compressed.buffer.slice(compressed.byteOffset, compressed.byteOffset + compressed.byteLength);
|
||||
}
|
||||
// 加密(若启用)
|
||||
if (isCryptoEnabled()) {
|
||||
const enc = await encryptPage(buf);
|
||||
const header = new Uint8Array(12 + 4); // IV(12) + originalLen(4)
|
||||
header.set(enc.iv, 0);
|
||||
new DataView(header.buffer).setUint32(12, data.byteLength, false);
|
||||
const combined = new Uint8Array(header.length + enc.data.byteLength);
|
||||
combined.set(header, 0);
|
||||
combined.set(new Uint8Array(enc.data), header.length);
|
||||
buf = combined.buffer;
|
||||
}
|
||||
await this.backend.write(`sst_${id}`, buf);
|
||||
},
|
||||
load: async (id) => {
|
||||
@@ -4474,6 +4634,14 @@ class AriaEngine {
|
||||
if (!raw)
|
||||
return null;
|
||||
let buf = new Uint8Array(raw);
|
||||
// 解密(若数据带加密头)
|
||||
if (isCryptoEnabled() && buf.length > 16) {
|
||||
const iv = buf.slice(0, 12);
|
||||
const origLen = new DataView(buf.buffer, buf.byteOffset + 12, 4).getUint32(0, false);
|
||||
const ciphertext = buf.slice(16).buffer;
|
||||
const decrypted = await decryptPage(iv, ciphertext);
|
||||
buf = new Uint8Array(decrypted, 0, origLen);
|
||||
}
|
||||
// 解压(若启用)
|
||||
if (this.config.compression) {
|
||||
const decompressed = decompressLZ4(buf, buf.length * 2); // 估计原始大小
|
||||
@@ -4693,6 +4861,10 @@ class AriaEngine {
|
||||
this.mvcc.gc(50);
|
||||
}
|
||||
}
|
||||
/** 估算 WAL 大小(字节) */
|
||||
getWALEstimatedSize() {
|
||||
return this.wal.getBufferedCount() * 200; // 粗略估算每条 ~200B
|
||||
}
|
||||
/**
|
||||
* ANALYZE: 收集表统计信息
|
||||
* 返回行数、平均行大小、索引深度等
|
||||
@@ -4763,7 +4935,7 @@ class AriaEngine {
|
||||
// 压缩各层级
|
||||
for (let level = 0; level < 6; level++) {
|
||||
if (this.lsm.getStats().levelCounts[level] >= 2) {
|
||||
this.lsm.compactLevelSync(level);
|
||||
this.lsm.compactLevel(level);
|
||||
}
|
||||
}
|
||||
// GC MVCC 版本(保留最新 10 个)
|
||||
@@ -5224,8 +5396,13 @@ function compileUpdate(stmt) {
|
||||
// Executor
|
||||
// ---------------------------------------------------------------------------
|
||||
class QueryExecutor {
|
||||
constructor(engine) {
|
||||
constructor(engine, maxRowsPerQuery = 0) {
|
||||
this.engine = engine;
|
||||
this.maxRowsPerQuery = maxRowsPerQuery;
|
||||
}
|
||||
/** 设置查询结果行数上限 */
|
||||
setMaxRowsPerQuery(max) {
|
||||
this.maxRowsPerQuery = max;
|
||||
}
|
||||
async execute(stmt) {
|
||||
switch (stmt.type) {
|
||||
@@ -5236,6 +5413,8 @@ class QueryExecutor {
|
||||
case 'DELETE': return this.executeDelete(stmt);
|
||||
case 'CREATE_TABLE': return this.executeCreateTable(stmt);
|
||||
case 'DROP_TABLE': return this.executeDropTable(stmt);
|
||||
case 'ALTER_TABLE': return this.executeAlterTable(stmt);
|
||||
case 'TRUNCATE_TABLE': return this.executeTruncateTable(stmt);
|
||||
default: throw new DatabaseError('Unknown statement type', 'UNKNOWN_STATEMENT');
|
||||
}
|
||||
}
|
||||
@@ -5300,6 +5479,10 @@ class QueryExecutor {
|
||||
if (!hasGroupBy && !hasAggregate && stmt.columns.length > 0 && stmt.columns[0] !== '*') {
|
||||
rows = rows.map((row) => projectColumns(row, stmt.columns));
|
||||
}
|
||||
// 全局行数上限保护
|
||||
if (this.maxRowsPerQuery > 0 && rows.length > this.maxRowsPerQuery) {
|
||||
rows = rows.slice(0, this.maxRowsPerQuery);
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
// ---- JOIN ----
|
||||
@@ -5466,6 +5649,35 @@ class QueryExecutor {
|
||||
}
|
||||
return this.engine.dropTable(stmt.name);
|
||||
}
|
||||
async executeAlterTable(stmt) {
|
||||
const exists = await this.engine.hasTable(stmt.name);
|
||||
if (!exists)
|
||||
throw new DatabaseError(`Table "${stmt.name}" does not exist`, 'TABLE_NOT_FOUND');
|
||||
const schema = await this.engine.getTableSchema(stmt.name);
|
||||
if (!schema)
|
||||
return;
|
||||
if (stmt.action === 'ADD') {
|
||||
if (schema.columns[stmt.column.name]) {
|
||||
throw new DatabaseError(`Column "${stmt.column.name}" already exists in table "${stmt.name}"`, 'COLUMN_EXISTS');
|
||||
}
|
||||
schema.columns[stmt.column.name] = astColumnToColumnDef(stmt.column);
|
||||
}
|
||||
else if (stmt.action === 'DROP') {
|
||||
if (!schema.columns[stmt.column.name]) {
|
||||
throw new DatabaseError(`Column "${stmt.column.name}" does not exist in table "${stmt.name}"`, 'COLUMN_NOT_FOUND');
|
||||
}
|
||||
delete schema.columns[stmt.column.name];
|
||||
}
|
||||
// 重建表结构
|
||||
await this.engine.dropTable(stmt.name);
|
||||
await this.engine.createTable(schema);
|
||||
}
|
||||
async executeTruncateTable(stmt) {
|
||||
const exists = await this.engine.hasTable(stmt.name);
|
||||
if (!exists)
|
||||
throw new DatabaseError(`Table "${stmt.name}" does not exist`, 'TABLE_NOT_FOUND');
|
||||
return this.engine.clear(stmt.name);
|
||||
}
|
||||
getEngine() { return this.engine; }
|
||||
// ===================================================================
|
||||
// 无 GROUP BY 时的聚合计算
|
||||
@@ -5620,6 +5832,9 @@ var TokenType;
|
||||
TokenType["IF"] = "IF";
|
||||
TokenType["EXISTS"] = "EXISTS";
|
||||
TokenType["FALSE"] = "FALSE";
|
||||
TokenType["ALTER"] = "ALTER";
|
||||
TokenType["ADD"] = "ADD";
|
||||
TokenType["TRUNCATE"] = "TRUNCATE";
|
||||
// JOIN 相关
|
||||
TokenType["INNER"] = "INNER";
|
||||
TokenType["LEFT"] = "LEFT";
|
||||
@@ -5698,6 +5913,9 @@ const KEYWORDS = {
|
||||
'BETWEEN': TokenType.BETWEEN,
|
||||
'IF': TokenType.IF,
|
||||
'EXISTS': TokenType.EXISTS,
|
||||
'ALTER': TokenType.ALTER,
|
||||
'ADD': TokenType.ADD,
|
||||
'TRUNCATE': TokenType.TRUNCATE,
|
||||
// JOIN
|
||||
'INNER': TokenType.INNER,
|
||||
'LEFT': TokenType.LEFT,
|
||||
@@ -5980,6 +6198,10 @@ class Parser {
|
||||
return this.parseCreateTable();
|
||||
case TokenType.DROP:
|
||||
return this.parseDropTable();
|
||||
case TokenType.ALTER:
|
||||
return this.parseAlterTable();
|
||||
case TokenType.TRUNCATE:
|
||||
return this.parseTruncateTable();
|
||||
default:
|
||||
throw this.error(`Unexpected token "${this.curToken.value}"`);
|
||||
}
|
||||
@@ -6310,6 +6532,49 @@ class Parser {
|
||||
return 'RESTRICT';
|
||||
}
|
||||
// ===================================================================
|
||||
// ALTER TABLE
|
||||
// ===================================================================
|
||||
parseAlterTable() {
|
||||
this.expect(TokenType.ALTER);
|
||||
this.expect(TokenType.TABLE);
|
||||
const tableName = this.expectIdentifier('table name');
|
||||
// ADD COLUMN / DROP COLUMN
|
||||
let action;
|
||||
if (this.curTokenIs(TokenType.ADD)) {
|
||||
action = 'ADD';
|
||||
this.nextToken();
|
||||
// Optional COLUMN keyword
|
||||
if (this.curToken.type === TokenType.IDENTIFIER && this.curToken.value.toUpperCase() === 'COLUMN') {
|
||||
this.nextToken();
|
||||
}
|
||||
const col = this.parseColumnDef();
|
||||
return { type: 'ALTER_TABLE', name: tableName, action, column: col };
|
||||
}
|
||||
else if (this.curTokenIs(TokenType.DROP) ||
|
||||
(this._isKeywordAsIdent() && this.curToken.value.toUpperCase() === 'DROP')) {
|
||||
action = 'DROP';
|
||||
this.nextToken();
|
||||
// Optional COLUMN keyword
|
||||
if (this.curToken.type === TokenType.IDENTIFIER && this.curToken.value.toUpperCase() === 'COLUMN') {
|
||||
this.nextToken();
|
||||
}
|
||||
const colName = this.expectIdentifier('column name');
|
||||
return { type: 'ALTER_TABLE', name: tableName, action, column: { name: colName, type: 'string' } };
|
||||
}
|
||||
else {
|
||||
throw this.error('Expected ADD or DROP in ALTER TABLE');
|
||||
}
|
||||
}
|
||||
// ===================================================================
|
||||
// TRUNCATE TABLE
|
||||
// ===================================================================
|
||||
parseTruncateTable() {
|
||||
this.expect(TokenType.TRUNCATE);
|
||||
this.expect(TokenType.TABLE);
|
||||
const tableName = this.expectIdentifier('table name');
|
||||
return { type: 'TRUNCATE_TABLE', name: tableName };
|
||||
}
|
||||
// ===================================================================
|
||||
// DROP TABLE
|
||||
// ===================================================================
|
||||
parseDropTable() {
|
||||
@@ -6777,7 +7042,7 @@ class PluginManager {
|
||||
this.hooks = new Map();
|
||||
}
|
||||
/** 注册插件 */
|
||||
register(plugin) {
|
||||
register(plugin, db) {
|
||||
// 按优先级插入
|
||||
const priority = plugin.priority ?? 0;
|
||||
const insertIndex = this.plugins.findIndex((p) => (p.priority ?? 0) < priority);
|
||||
@@ -6787,8 +7052,8 @@ class PluginManager {
|
||||
else {
|
||||
this.plugins.splice(insertIndex, 0, plugin);
|
||||
}
|
||||
// 安装
|
||||
plugin.install(null); // 实际引用由 MetonaSqlark 注入
|
||||
// 安装(传入 db 实例)
|
||||
plugin.install(db);
|
||||
}
|
||||
/** 卸载插件 */
|
||||
unregister(pluginName) {
|
||||
@@ -6879,12 +7144,12 @@ class MetonaSqlark {
|
||||
// 打开连接
|
||||
await this.engine.open(this.name, this.version);
|
||||
// 初始化执行器和事务管理器
|
||||
this.executor = new QueryExecutor(this.engine);
|
||||
this.executor = new QueryExecutor(this.engine, this.maxRowsPerQuery);
|
||||
this.transactionManager = new TransactionManager(this.engine);
|
||||
// 注册插件
|
||||
if (this.config.plugins) {
|
||||
for (const plugin of this.config.plugins) {
|
||||
this.pluginManager.register(plugin);
|
||||
this.pluginManager.register(plugin, this);
|
||||
}
|
||||
}
|
||||
this.ready = true;
|
||||
@@ -6902,9 +7167,15 @@ class MetonaSqlark {
|
||||
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);
|
||||
try {
|
||||
await this.pluginManager.trigger('beforeCreateTable', schema);
|
||||
await this.engine.createTable(schema);
|
||||
await this.pluginManager.trigger('afterCreateTable', schema);
|
||||
}
|
||||
catch (error) {
|
||||
this._onError(error);
|
||||
throw error;
|
||||
}
|
||||
// 清除缓存
|
||||
this.tableCache.delete(name);
|
||||
}
|
||||
@@ -6921,9 +7192,15 @@ class MetonaSqlark {
|
||||
/** 删除表 */
|
||||
async dropTable(name) {
|
||||
this.ensureReady();
|
||||
await this.pluginManager.trigger('beforeDropTable', name);
|
||||
await this.engine.dropTable(name);
|
||||
await this.pluginManager.trigger('afterDropTable', name);
|
||||
try {
|
||||
await this.pluginManager.trigger('beforeDropTable', name);
|
||||
await this.engine.dropTable(name);
|
||||
await this.pluginManager.trigger('afterDropTable', name);
|
||||
}
|
||||
catch (error) {
|
||||
this._onError(error);
|
||||
throw error;
|
||||
}
|
||||
this.tableCache.delete(name);
|
||||
}
|
||||
/** 获取所有表名 */
|
||||
@@ -6937,8 +7214,15 @@ class MetonaSqlark {
|
||||
this.ensureReady();
|
||||
const startTime = this.debug ? Date.now() : 0;
|
||||
await this.pluginManager.trigger('beforeQuery', sql);
|
||||
const stmt = parse(sql);
|
||||
const result = await this.executor.execute(stmt);
|
||||
let result;
|
||||
try {
|
||||
const stmt = parse(sql);
|
||||
result = await this.executor.execute(stmt);
|
||||
}
|
||||
catch (error) {
|
||||
this._onError(error);
|
||||
throw error;
|
||||
}
|
||||
await this.pluginManager.trigger('afterQuery', sql, result);
|
||||
if (this.debug) {
|
||||
const elapsed = Date.now() - startTime;
|
||||
@@ -6952,9 +7236,15 @@ class MetonaSqlark {
|
||||
async transaction(fn) {
|
||||
this.ensureReady();
|
||||
await this.pluginManager.trigger('beforeTransaction');
|
||||
const result = await this.transactionManager.execute(fn);
|
||||
await this.pluginManager.trigger('afterTransaction');
|
||||
return result;
|
||||
try {
|
||||
const result = await this.transactionManager.execute(fn);
|
||||
await this.pluginManager.trigger('afterTransaction');
|
||||
return result;
|
||||
}
|
||||
catch (error) {
|
||||
this._onError(error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
// ---- 导入导出 ----
|
||||
/** 导出表数据为 JSON */
|
||||
@@ -6965,7 +7255,13 @@ class MetonaSqlark {
|
||||
/** 导入 JSON 数据到表 */
|
||||
async importTable(tableName, data) {
|
||||
this.ensureReady();
|
||||
return this.engine.insert(tableName, data);
|
||||
try {
|
||||
return await this.engine.insert(tableName, data);
|
||||
}
|
||||
catch (error) {
|
||||
this._onError(error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
/** 导出整个数据库为 JSON */
|
||||
async exportAll() {
|
||||
@@ -7035,7 +7331,7 @@ class MetonaSqlark {
|
||||
case 'disk':
|
||||
return diskEngine === 'opfs' ? new OPFSEngine() : new IndexedDBEngine();
|
||||
case 'aria':
|
||||
return new AriaEngine({ storageBackend: diskEngine === 'opfs' ? 'memory' : 'indexeddb' });
|
||||
return new AriaEngine({ storageBackend: diskEngine === 'opfs' ? 'opfs' : 'indexeddb' });
|
||||
case 'hybrid':
|
||||
return new HybridEngine(diskEngine);
|
||||
default:
|
||||
@@ -7182,7 +7478,7 @@ M.getActiveConnections = () => manager.getActiveConnections();
|
||||
/**
|
||||
* metona-sqlark — 入口文件
|
||||
* @module metona-sqlark
|
||||
* @version 0.1.12
|
||||
* @version 0.2.5
|
||||
*
|
||||
* 前端关系型数据库,内存与磁盘双模式。
|
||||
* 支持 Query Builder 链式 API 和 SQL 字符串查询。
|
||||
|
||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+29
-7
@@ -58,7 +58,7 @@ interface DatabaseConfig {
|
||||
onReady?: (db: unknown) => void;
|
||||
/** 错误回调 */
|
||||
onError?: (error: Error) => void;
|
||||
/** 查询结果行数上限(默认 10000,0 表示不限制) */
|
||||
/** 查询结果行数上限(默认 0,0 表示不限制) */
|
||||
maxRowsPerQuery?: number;
|
||||
/** 调试模式(启用后输出详细操作日志) */
|
||||
debug?: boolean;
|
||||
@@ -114,7 +114,7 @@ interface MetonaPlugin {
|
||||
/** 销毁 */
|
||||
destroy(): void;
|
||||
}
|
||||
declare const VERSION = "0.2.0";
|
||||
declare const VERSION = "0.2.5";
|
||||
|
||||
/**
|
||||
* metona-sqlark Plugin — 插件系统
|
||||
@@ -128,7 +128,7 @@ declare class PluginManager {
|
||||
private plugins;
|
||||
private hooks;
|
||||
/** 注册插件 */
|
||||
register(plugin: MetonaPlugin): void;
|
||||
register(plugin: MetonaPlugin, db?: unknown): void;
|
||||
/** 卸载插件 */
|
||||
unregister(pluginName: string): void;
|
||||
/** 获取所有已注册插件 */
|
||||
@@ -285,7 +285,17 @@ interface SelectStatement {
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
}
|
||||
type Statement = SelectStatement | ExplainStatement | InsertStatement | UpdateStatement | DeleteStatement | CreateTableStatement | DropTableStatement;
|
||||
interface AlterTableStatement {
|
||||
type: 'ALTER_TABLE';
|
||||
name: string;
|
||||
action: 'ADD' | 'DROP';
|
||||
column: ASTColumnDef;
|
||||
}
|
||||
interface TruncateTableStatement {
|
||||
type: 'TRUNCATE_TABLE';
|
||||
name: string;
|
||||
}
|
||||
type Statement = SelectStatement | ExplainStatement | InsertStatement | UpdateStatement | DeleteStatement | CreateTableStatement | DropTableStatement | AlterTableStatement | TruncateTableStatement;
|
||||
|
||||
/**
|
||||
* metona-sqlark Query Executor — AST 执行器
|
||||
@@ -296,7 +306,10 @@ type Statement = SelectStatement | ExplainStatement | InsertStatement | UpdateSt
|
||||
|
||||
declare class QueryExecutor {
|
||||
private engine;
|
||||
constructor(engine: IStorageEngine);
|
||||
private maxRowsPerQuery;
|
||||
constructor(engine: IStorageEngine, maxRowsPerQuery?: number);
|
||||
/** 设置查询结果行数上限 */
|
||||
setMaxRowsPerQuery(max: number): void;
|
||||
execute(stmt: Statement): Promise<unknown>;
|
||||
/** EXPLAIN: 输出查询计划 */
|
||||
private executeExplain;
|
||||
@@ -313,6 +326,8 @@ declare class QueryExecutor {
|
||||
private executeDelete;
|
||||
private executeCreateTable;
|
||||
private executeDropTable;
|
||||
private executeAlterTable;
|
||||
private executeTruncateTable;
|
||||
getEngine(): IStorageEngine;
|
||||
/** 检查 SELECT 列列表中是否包含聚合函数 */
|
||||
private _hasAggregateColumn;
|
||||
@@ -595,6 +610,8 @@ declare class IndexedDBEngine implements IStorageEngine {
|
||||
private idbDropTable;
|
||||
private idbInsert;
|
||||
private idbFind;
|
||||
/** 尝试使用 IDB 索引进行等值查询,成功返回结果,不适用返回 null */
|
||||
private tryIDBIndexLookup;
|
||||
private idbUpdate;
|
||||
private idbDelete;
|
||||
private idbClear;
|
||||
@@ -674,7 +691,7 @@ interface AriaEngineConfig {
|
||||
* AriaEngine — 自研页面式存储引擎主类
|
||||
* @module engine/aria/index
|
||||
*
|
||||
* v0.2.4: 二级索引 + MVCC 集成 + 生产加固
|
||||
* v0.2.5: WAL 同步修复 + MVCC 接入 + 版本统一 + 生产加固
|
||||
*/
|
||||
|
||||
declare class AriaEngine implements IStorageEngine {
|
||||
@@ -736,6 +753,8 @@ declare class AriaEngine implements IStorageEngine {
|
||||
private tryGC;
|
||||
/** 检查内存预算,超出时强制 flush + GC */
|
||||
private checkMemoryBudget;
|
||||
/** 估算 WAL 大小(字节) */
|
||||
getWALEstimatedSize(): number;
|
||||
/**
|
||||
* ANALYZE: 收集表统计信息
|
||||
* 返回行数、平均行大小、索引深度等
|
||||
@@ -857,6 +876,9 @@ declare enum TokenType {
|
||||
IF = "IF",
|
||||
EXISTS = "EXISTS",
|
||||
FALSE = "FALSE",
|
||||
ALTER = "ALTER",
|
||||
ADD = "ADD",
|
||||
TRUNCATE = "TRUNCATE",
|
||||
INNER = "INNER",
|
||||
LEFT = "LEFT",
|
||||
RIGHT = "RIGHT",
|
||||
@@ -964,7 +986,7 @@ declare class OPFSBackend implements IStorageBackend {
|
||||
/**
|
||||
* metona-sqlark — 入口文件
|
||||
* @module metona-sqlark
|
||||
* @version 0.1.12
|
||||
* @version 0.2.5
|
||||
*
|
||||
* 前端关系型数据库,内存与磁盘双模式。
|
||||
* 支持 Query Builder 链式 API 和 SQL 字符串查询。
|
||||
|
||||
Vendored
+346
-50
@@ -29,7 +29,7 @@ class DatabaseError extends Error {
|
||||
// ---------------------------------------------------------------------------
|
||||
// 版本
|
||||
// ---------------------------------------------------------------------------
|
||||
const VERSION = '0.2.0';
|
||||
const VERSION = '0.2.5';
|
||||
|
||||
/**
|
||||
* metona-sqlark Shared WHERE Matcher — 统一的条件匹配逻辑
|
||||
@@ -716,6 +716,13 @@ class IndexedDBEngine {
|
||||
}
|
||||
async idbFind(tableName, query) {
|
||||
const db = this.ensureDB();
|
||||
// 尝试使用 IDB 索引进行等值查询
|
||||
if (query.where) {
|
||||
const indexResult = await this.tryIDBIndexLookup(db, tableName, query);
|
||||
if (indexResult !== null)
|
||||
return indexResult;
|
||||
}
|
||||
// 回退到全量 getAll + 内存过滤
|
||||
return new Promise((resolve, reject) => {
|
||||
const tx = db.transaction(tableName, 'readonly');
|
||||
const req = tx.objectStore(tableName).getAll();
|
||||
@@ -738,6 +745,64 @@ class IndexedDBEngine {
|
||||
req.onerror = () => reject(new DatabaseError(`Find failed for "${tableName}"`, 'IDB_READ_ERROR', req.error));
|
||||
});
|
||||
}
|
||||
/** 尝试使用 IDB 索引进行等值查询,成功返回结果,不适用返回 null */
|
||||
async tryIDBIndexLookup(db, tableName, query) {
|
||||
if (!query.where)
|
||||
return null;
|
||||
for (const [col, condition] of Object.entries(query.where)) {
|
||||
// 跳过逻辑组合符
|
||||
if (col === '$and' || col === '$or' || col === '$not')
|
||||
continue;
|
||||
// 只处理等值查询
|
||||
let targetValue;
|
||||
if (typeof condition !== 'object' || condition === null) {
|
||||
targetValue = condition;
|
||||
}
|
||||
else if ('$eq' in condition) {
|
||||
targetValue = condition.$eq;
|
||||
}
|
||||
else {
|
||||
continue;
|
||||
}
|
||||
// 检查是否有对应的 IDB 索引
|
||||
const indexName = `idx_${col}`;
|
||||
try {
|
||||
return await new Promise((resolve, reject) => {
|
||||
const tx = db.transaction(tableName, 'readonly');
|
||||
const store = tx.objectStore(tableName);
|
||||
if (!store.indexNames.contains(indexName)) {
|
||||
resolve(null); // 没有索引,回退
|
||||
return;
|
||||
}
|
||||
const index = store.index(indexName);
|
||||
const req = index.getAll(targetValue);
|
||||
req.onsuccess = () => {
|
||||
let results = req.result ?? [];
|
||||
// 如果有其他 WHERE 条件,继续过滤
|
||||
const otherKeys = Object.keys(query.where).filter((k) => k !== col && k !== '$and' && k !== '$or' && k !== '$not');
|
||||
if (otherKeys.length > 0) {
|
||||
results = results.filter((row) => matchWhere(row, query.where));
|
||||
}
|
||||
if (query.orderBy && query.orderBy.length > 0) {
|
||||
results = applyOrderBy(results, query.orderBy);
|
||||
}
|
||||
const offset = query.offset ?? 0;
|
||||
const limit = query.limit ?? results.length;
|
||||
results = results.slice(offset, offset + limit);
|
||||
if (query.columns && query.columns.length > 0 && query.columns[0] !== '*') {
|
||||
results = results.map((r) => projectColumns(r, query.columns));
|
||||
}
|
||||
resolve(results);
|
||||
};
|
||||
req.onerror = () => reject(new DatabaseError(`Index lookup failed for "${tableName}.${col}"`, 'IDB_READ_ERROR', req.error));
|
||||
});
|
||||
}
|
||||
catch {
|
||||
return null; // 索引不可用,回退
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
async idbUpdate(tableName, query, updates) {
|
||||
const db = this.ensureDB();
|
||||
return new Promise((resolve, reject) => {
|
||||
@@ -2167,18 +2232,26 @@ class SSTableReader {
|
||||
return -1;
|
||||
}
|
||||
locateBlockGE(key) {
|
||||
for (let i = 0; i < this.indexEntries.length; i++) {
|
||||
if (this.indexEntries[i].key >= key)
|
||||
return i;
|
||||
let lo = 0, hi = this.indexEntries.length;
|
||||
while (lo < hi) {
|
||||
const mid = (lo + hi) >> 1;
|
||||
if (this.indexEntries[mid].key < key)
|
||||
lo = mid + 1;
|
||||
else
|
||||
hi = mid;
|
||||
}
|
||||
return this.indexEntries.length - 1;
|
||||
return lo < this.indexEntries.length ? lo : this.indexEntries.length - 1;
|
||||
}
|
||||
locateBlockLE(key) {
|
||||
for (let i = this.indexEntries.length - 1; i >= 0; i--) {
|
||||
if (this.indexEntries[i].key <= key)
|
||||
return i;
|
||||
let lo = 0, hi = this.indexEntries.length;
|
||||
while (lo < hi) {
|
||||
const mid = (lo + hi) >> 1;
|
||||
if (this.indexEntries[mid].key <= key)
|
||||
lo = mid + 1;
|
||||
else
|
||||
hi = mid;
|
||||
}
|
||||
return 0;
|
||||
return lo > 0 ? lo - 1 : 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2554,7 +2627,11 @@ class LSM {
|
||||
// =======================================================================
|
||||
// Compaction
|
||||
// =======================================================================
|
||||
/** 同步执行 Compaction(简化版,仅供内部调用) */
|
||||
/** 同步执行 Compaction(public,供 VACUUM 等外部调用) */
|
||||
compactLevel(level) {
|
||||
this.compactLevelSync(level);
|
||||
}
|
||||
/** 同步执行 Compaction(简化版,内部实现) */
|
||||
compactLevelSync(level) {
|
||||
if (level >= MAX_LSM_LEVELS - 1)
|
||||
return;
|
||||
@@ -2707,8 +2784,8 @@ class WAL {
|
||||
// =======================================================================
|
||||
// 写入
|
||||
// =======================================================================
|
||||
/** 追加一条 WAL 记录 */
|
||||
append(record) {
|
||||
/** 追加一条 WAL 记录(full 模式同步等待写入完成) */
|
||||
async append(record) {
|
||||
if (!this.enabled)
|
||||
return;
|
||||
this.lsn++;
|
||||
@@ -2719,10 +2796,13 @@ class WAL {
|
||||
};
|
||||
const bytes = this.encodeRecord(fullRecord);
|
||||
if (this.syncMode === 'full') {
|
||||
this.store.append(bytes).catch(() => {
|
||||
try {
|
||||
await this.store.append(bytes);
|
||||
}
|
||||
catch {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn('[AriaEngine WAL] Failed to append record');
|
||||
});
|
||||
}
|
||||
}
|
||||
else if (this.syncMode === 'batch') {
|
||||
this.buffer.push(bytes);
|
||||
@@ -2909,19 +2989,26 @@ class WAL {
|
||||
// CheckpointManager
|
||||
// ---------------------------------------------------------------------------
|
||||
class CheckpointManager {
|
||||
constructor(lsm, wal, flushable = null, interval = 1000) {
|
||||
constructor(lsm, wal, flushable = null, interval = 1000, walSizeThreshold = 16 * 1024 * 1024) {
|
||||
this.opCount = 0;
|
||||
this.lsm = lsm;
|
||||
this.wal = wal;
|
||||
this.flushable = flushable;
|
||||
this.interval = interval;
|
||||
this.walSizeThreshold = walSizeThreshold;
|
||||
}
|
||||
async tick() {
|
||||
this.opCount++;
|
||||
if (this.opCount >= this.interval) {
|
||||
// 检查操作计数或 WAL 大小是否超阈值
|
||||
if (this.opCount >= this.interval || this.getWALEstimatedSize() >= this.walSizeThreshold) {
|
||||
await this.checkpoint();
|
||||
}
|
||||
}
|
||||
/** 估算 WAL 大小 */
|
||||
getWALEstimatedSize() {
|
||||
const count = typeof this.wal.getBufferedCount === 'function' ? this.wal.getBufferedCount() : 0;
|
||||
return count * 200;
|
||||
}
|
||||
async checkpoint() {
|
||||
await this.lsm.flush();
|
||||
if (this.flushable) {
|
||||
@@ -3903,11 +3990,70 @@ function decompressLZ4(input, originalSize) {
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* AriaEngine Crypto — 页面级 AES-GCM 加密
|
||||
* @module engine/aria/crypto
|
||||
*
|
||||
* v0.2.5: 改为实例化 CryptoManager,避免多实例共享全局状态。
|
||||
* 保留全局函数兼容旧代码(委托给全局单例)。
|
||||
*/
|
||||
const ALGO = 'AES-GCM';
|
||||
const IV_LENGTH = 12;
|
||||
/**
|
||||
* CryptoManager — 实例级加密管理器
|
||||
* 每个 AriaEngine 实例可拥有独立的加密配置。
|
||||
*/
|
||||
class CryptoManager {
|
||||
constructor() {
|
||||
this.cryptoKey = null;
|
||||
this._enabled = false;
|
||||
}
|
||||
get enabled() { return this._enabled; }
|
||||
async init(password, salt) {
|
||||
const enc = new TextEncoder();
|
||||
const keyMaterial = await crypto.subtle.importKey('raw', enc.encode(password), 'PBKDF2', false, ['deriveKey']);
|
||||
const actualSalt = salt || crypto.getRandomValues(new Uint8Array(16));
|
||||
this.cryptoKey = await crypto.subtle.deriveKey({ name: 'PBKDF2', salt: actualSalt, iterations: 100000, hash: 'SHA-256' }, keyMaterial, { name: ALGO, length: 256 }, false, ['encrypt', 'decrypt']);
|
||||
this._enabled = true;
|
||||
return actualSalt;
|
||||
}
|
||||
async encryptPage(data) {
|
||||
if (!this.cryptoKey)
|
||||
throw new Error('Crypto not initialized');
|
||||
const iv = crypto.getRandomValues(new Uint8Array(IV_LENGTH));
|
||||
const ciphertext = await crypto.subtle.encrypt({ name: ALGO, iv }, this.cryptoKey, data);
|
||||
return { iv: iv, data: ciphertext };
|
||||
}
|
||||
async decryptPage(iv, data) {
|
||||
if (!this.cryptoKey)
|
||||
throw new Error('Crypto not initialized');
|
||||
return crypto.subtle.decrypt({ name: ALGO, iv }, this.cryptoKey, data);
|
||||
}
|
||||
close() {
|
||||
this.cryptoKey = null;
|
||||
this._enabled = false;
|
||||
}
|
||||
}
|
||||
// ---------------------------------------------------------------------------
|
||||
// 全局兼容层(旧代码仍可使用全局函数)
|
||||
// ---------------------------------------------------------------------------
|
||||
const globalCrypto = new CryptoManager();
|
||||
/** @deprecated 使用 CryptoManager 实例代替 */
|
||||
function isCryptoEnabled() { return globalCrypto.enabled; }
|
||||
/** @deprecated 使用 CryptoManager 实例代替 */
|
||||
async function encryptPage(data) {
|
||||
return globalCrypto.encryptPage(data);
|
||||
}
|
||||
/** @deprecated 使用 CryptoManager 实例代替 */
|
||||
async function decryptPage(iv, data) {
|
||||
return globalCrypto.decryptPage(iv, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* AriaEngine — 自研页面式存储引擎主类
|
||||
* @module engine/aria/index
|
||||
*
|
||||
* v0.2.4: 二级索引 + MVCC 集成 + 生产加固
|
||||
* v0.2.5: WAL 同步修复 + MVCC 接入 + 版本统一 + 生产加固
|
||||
*/
|
||||
// ---------------------------------------------------------------------------
|
||||
// AriaEngine
|
||||
@@ -4027,8 +4173,8 @@ class AriaEngine {
|
||||
this.applyWALRecord(r);
|
||||
}
|
||||
}
|
||||
// 8. Checkpoint Manager(BufferPool 暂简化,使用 flush 替代)
|
||||
this.checkpointManager = new CheckpointManager(this.lsm, this.wal, { flushAll: async () => { await this.lsm.flush(); } }, this.config.checkpointInterval);
|
||||
// 8. Checkpoint Manager(接入 WAL 大小阈值)
|
||||
this.checkpointManager = new CheckpointManager(this.lsm, this.wal, { flushAll: async () => { await this.lsm.flush(); } }, this.config.checkpointInterval, this.config.walSizeThreshold);
|
||||
this.opened = true;
|
||||
}
|
||||
async close() {
|
||||
@@ -4071,7 +4217,7 @@ class AriaEngine {
|
||||
}
|
||||
}
|
||||
await this.persistSchemas();
|
||||
this.wal.append({
|
||||
await this.wal.append({
|
||||
type: WALRecordType.CREATE_TABLE,
|
||||
txnId: 0,
|
||||
tableName: schema.name,
|
||||
@@ -4091,7 +4237,7 @@ class AriaEngine {
|
||||
this.schemas.delete(tableName);
|
||||
this.tablePKs.delete(tableName);
|
||||
await this.persistSchemas();
|
||||
this.wal.append({
|
||||
await this.wal.append({
|
||||
type: WALRecordType.DROP_TABLE,
|
||||
txnId: 0,
|
||||
tableName,
|
||||
@@ -4128,8 +4274,9 @@ class AriaEngine {
|
||||
throw new DatabaseError(`Duplicate primary key "${pkValue}" in table "${tableName}"`, 'DUPLICATE_KEY');
|
||||
}
|
||||
if (this.currentTxnId && this.txnSnapshot) {
|
||||
// Within transaction: buffer to snapshot
|
||||
// Within transaction: buffer to snapshot + MVCC version chain
|
||||
this.txnSnapshot.set(key, validated);
|
||||
this.mvcc.writeVersion(tableName, pkValue, validated, this.currentTxnId);
|
||||
}
|
||||
else {
|
||||
// Direct write to LSM (PK index)
|
||||
@@ -4138,7 +4285,7 @@ class AriaEngine {
|
||||
// 更新二级索引
|
||||
this.updateSecondaryIndexes(tableName, pkValue, validated, null);
|
||||
pks.push(pkValue);
|
||||
this.wal.append({
|
||||
await this.wal.append({
|
||||
type: WALRecordType.INSERT,
|
||||
txnId: this.currentTxnId ?? 0,
|
||||
tableName,
|
||||
@@ -4219,12 +4366,13 @@ class AriaEngine {
|
||||
this.validateRow(schema, updated);
|
||||
if (this.currentTxnId && this.txnSnapshot) {
|
||||
this.txnSnapshot.set(key, updated);
|
||||
this.mvcc.writeVersion(tableName, String(row[pkCol]), updated, this.currentTxnId);
|
||||
}
|
||||
else {
|
||||
this.lsm.put(key, updated);
|
||||
}
|
||||
count++;
|
||||
this.wal.append({
|
||||
await this.wal.append({
|
||||
type: WALRecordType.UPDATE,
|
||||
txnId: this.currentTxnId ?? 0,
|
||||
tableName,
|
||||
@@ -4249,14 +4397,15 @@ class AriaEngine {
|
||||
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
|
||||
// Buffer delete in snapshot + MVCC tombstone
|
||||
this.txnSnapshot.set(key, { __txn_deleted: true });
|
||||
this.mvcc.deleteVersion(tableName, String(row[pkCol]), this.currentTxnId);
|
||||
}
|
||||
else {
|
||||
this.lsm.delete(key);
|
||||
}
|
||||
count++;
|
||||
this.wal.append({
|
||||
await this.wal.append({
|
||||
type: WALRecordType.DELETE,
|
||||
txnId: this.currentTxnId ?? 0,
|
||||
tableName,
|
||||
@@ -4295,7 +4444,7 @@ class AriaEngine {
|
||||
throw new DatabaseError('Transaction already in progress', 'TX_ACTIVE');
|
||||
this.currentTxnId = this.mvcc.beginTransaction();
|
||||
this.txnSnapshot = new Map();
|
||||
this.wal.append({
|
||||
await this.wal.append({
|
||||
type: WALRecordType.BEGIN,
|
||||
txnId: this.currentTxnId,
|
||||
tableName: '',
|
||||
@@ -4316,7 +4465,7 @@ class AriaEngine {
|
||||
}
|
||||
}
|
||||
this.mvcc.commitTransaction(this.currentTxnId);
|
||||
this.wal.append({
|
||||
await this.wal.append({
|
||||
type: WALRecordType.COMMIT,
|
||||
txnId: this.currentTxnId,
|
||||
tableName: '',
|
||||
@@ -4331,7 +4480,7 @@ class AriaEngine {
|
||||
throw new DatabaseError('No active transaction', 'TX_NONE');
|
||||
this.mvcc.rollbackTransaction(this.currentTxnId);
|
||||
this.txnSnapshot = null;
|
||||
this.wal.append({
|
||||
await this.wal.append({
|
||||
type: WALRecordType.ROLLBACK,
|
||||
txnId: this.currentTxnId,
|
||||
tableName: '',
|
||||
@@ -4463,6 +4612,17 @@ class AriaEngine {
|
||||
const compressed = compressLZ4(new Uint8Array(buf));
|
||||
buf = compressed.buffer.slice(compressed.byteOffset, compressed.byteOffset + compressed.byteLength);
|
||||
}
|
||||
// 加密(若启用)
|
||||
if (isCryptoEnabled()) {
|
||||
const enc = await encryptPage(buf);
|
||||
const header = new Uint8Array(12 + 4); // IV(12) + originalLen(4)
|
||||
header.set(enc.iv, 0);
|
||||
new DataView(header.buffer).setUint32(12, data.byteLength, false);
|
||||
const combined = new Uint8Array(header.length + enc.data.byteLength);
|
||||
combined.set(header, 0);
|
||||
combined.set(new Uint8Array(enc.data), header.length);
|
||||
buf = combined.buffer;
|
||||
}
|
||||
await this.backend.write(`sst_${id}`, buf);
|
||||
},
|
||||
load: async (id) => {
|
||||
@@ -4470,6 +4630,14 @@ class AriaEngine {
|
||||
if (!raw)
|
||||
return null;
|
||||
let buf = new Uint8Array(raw);
|
||||
// 解密(若数据带加密头)
|
||||
if (isCryptoEnabled() && buf.length > 16) {
|
||||
const iv = buf.slice(0, 12);
|
||||
const origLen = new DataView(buf.buffer, buf.byteOffset + 12, 4).getUint32(0, false);
|
||||
const ciphertext = buf.slice(16).buffer;
|
||||
const decrypted = await decryptPage(iv, ciphertext);
|
||||
buf = new Uint8Array(decrypted, 0, origLen);
|
||||
}
|
||||
// 解压(若启用)
|
||||
if (this.config.compression) {
|
||||
const decompressed = decompressLZ4(buf, buf.length * 2); // 估计原始大小
|
||||
@@ -4689,6 +4857,10 @@ class AriaEngine {
|
||||
this.mvcc.gc(50);
|
||||
}
|
||||
}
|
||||
/** 估算 WAL 大小(字节) */
|
||||
getWALEstimatedSize() {
|
||||
return this.wal.getBufferedCount() * 200; // 粗略估算每条 ~200B
|
||||
}
|
||||
/**
|
||||
* ANALYZE: 收集表统计信息
|
||||
* 返回行数、平均行大小、索引深度等
|
||||
@@ -4759,7 +4931,7 @@ class AriaEngine {
|
||||
// 压缩各层级
|
||||
for (let level = 0; level < 6; level++) {
|
||||
if (this.lsm.getStats().levelCounts[level] >= 2) {
|
||||
this.lsm.compactLevelSync(level);
|
||||
this.lsm.compactLevel(level);
|
||||
}
|
||||
}
|
||||
// GC MVCC 版本(保留最新 10 个)
|
||||
@@ -5220,8 +5392,13 @@ function compileUpdate(stmt) {
|
||||
// Executor
|
||||
// ---------------------------------------------------------------------------
|
||||
class QueryExecutor {
|
||||
constructor(engine) {
|
||||
constructor(engine, maxRowsPerQuery = 0) {
|
||||
this.engine = engine;
|
||||
this.maxRowsPerQuery = maxRowsPerQuery;
|
||||
}
|
||||
/** 设置查询结果行数上限 */
|
||||
setMaxRowsPerQuery(max) {
|
||||
this.maxRowsPerQuery = max;
|
||||
}
|
||||
async execute(stmt) {
|
||||
switch (stmt.type) {
|
||||
@@ -5232,6 +5409,8 @@ class QueryExecutor {
|
||||
case 'DELETE': return this.executeDelete(stmt);
|
||||
case 'CREATE_TABLE': return this.executeCreateTable(stmt);
|
||||
case 'DROP_TABLE': return this.executeDropTable(stmt);
|
||||
case 'ALTER_TABLE': return this.executeAlterTable(stmt);
|
||||
case 'TRUNCATE_TABLE': return this.executeTruncateTable(stmt);
|
||||
default: throw new DatabaseError('Unknown statement type', 'UNKNOWN_STATEMENT');
|
||||
}
|
||||
}
|
||||
@@ -5296,6 +5475,10 @@ class QueryExecutor {
|
||||
if (!hasGroupBy && !hasAggregate && stmt.columns.length > 0 && stmt.columns[0] !== '*') {
|
||||
rows = rows.map((row) => projectColumns(row, stmt.columns));
|
||||
}
|
||||
// 全局行数上限保护
|
||||
if (this.maxRowsPerQuery > 0 && rows.length > this.maxRowsPerQuery) {
|
||||
rows = rows.slice(0, this.maxRowsPerQuery);
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
// ---- JOIN ----
|
||||
@@ -5462,6 +5645,35 @@ class QueryExecutor {
|
||||
}
|
||||
return this.engine.dropTable(stmt.name);
|
||||
}
|
||||
async executeAlterTable(stmt) {
|
||||
const exists = await this.engine.hasTable(stmt.name);
|
||||
if (!exists)
|
||||
throw new DatabaseError(`Table "${stmt.name}" does not exist`, 'TABLE_NOT_FOUND');
|
||||
const schema = await this.engine.getTableSchema(stmt.name);
|
||||
if (!schema)
|
||||
return;
|
||||
if (stmt.action === 'ADD') {
|
||||
if (schema.columns[stmt.column.name]) {
|
||||
throw new DatabaseError(`Column "${stmt.column.name}" already exists in table "${stmt.name}"`, 'COLUMN_EXISTS');
|
||||
}
|
||||
schema.columns[stmt.column.name] = astColumnToColumnDef(stmt.column);
|
||||
}
|
||||
else if (stmt.action === 'DROP') {
|
||||
if (!schema.columns[stmt.column.name]) {
|
||||
throw new DatabaseError(`Column "${stmt.column.name}" does not exist in table "${stmt.name}"`, 'COLUMN_NOT_FOUND');
|
||||
}
|
||||
delete schema.columns[stmt.column.name];
|
||||
}
|
||||
// 重建表结构
|
||||
await this.engine.dropTable(stmt.name);
|
||||
await this.engine.createTable(schema);
|
||||
}
|
||||
async executeTruncateTable(stmt) {
|
||||
const exists = await this.engine.hasTable(stmt.name);
|
||||
if (!exists)
|
||||
throw new DatabaseError(`Table "${stmt.name}" does not exist`, 'TABLE_NOT_FOUND');
|
||||
return this.engine.clear(stmt.name);
|
||||
}
|
||||
getEngine() { return this.engine; }
|
||||
// ===================================================================
|
||||
// 无 GROUP BY 时的聚合计算
|
||||
@@ -5616,6 +5828,9 @@ var TokenType;
|
||||
TokenType["IF"] = "IF";
|
||||
TokenType["EXISTS"] = "EXISTS";
|
||||
TokenType["FALSE"] = "FALSE";
|
||||
TokenType["ALTER"] = "ALTER";
|
||||
TokenType["ADD"] = "ADD";
|
||||
TokenType["TRUNCATE"] = "TRUNCATE";
|
||||
// JOIN 相关
|
||||
TokenType["INNER"] = "INNER";
|
||||
TokenType["LEFT"] = "LEFT";
|
||||
@@ -5694,6 +5909,9 @@ const KEYWORDS = {
|
||||
'BETWEEN': TokenType.BETWEEN,
|
||||
'IF': TokenType.IF,
|
||||
'EXISTS': TokenType.EXISTS,
|
||||
'ALTER': TokenType.ALTER,
|
||||
'ADD': TokenType.ADD,
|
||||
'TRUNCATE': TokenType.TRUNCATE,
|
||||
// JOIN
|
||||
'INNER': TokenType.INNER,
|
||||
'LEFT': TokenType.LEFT,
|
||||
@@ -5976,6 +6194,10 @@ class Parser {
|
||||
return this.parseCreateTable();
|
||||
case TokenType.DROP:
|
||||
return this.parseDropTable();
|
||||
case TokenType.ALTER:
|
||||
return this.parseAlterTable();
|
||||
case TokenType.TRUNCATE:
|
||||
return this.parseTruncateTable();
|
||||
default:
|
||||
throw this.error(`Unexpected token "${this.curToken.value}"`);
|
||||
}
|
||||
@@ -6306,6 +6528,49 @@ class Parser {
|
||||
return 'RESTRICT';
|
||||
}
|
||||
// ===================================================================
|
||||
// ALTER TABLE
|
||||
// ===================================================================
|
||||
parseAlterTable() {
|
||||
this.expect(TokenType.ALTER);
|
||||
this.expect(TokenType.TABLE);
|
||||
const tableName = this.expectIdentifier('table name');
|
||||
// ADD COLUMN / DROP COLUMN
|
||||
let action;
|
||||
if (this.curTokenIs(TokenType.ADD)) {
|
||||
action = 'ADD';
|
||||
this.nextToken();
|
||||
// Optional COLUMN keyword
|
||||
if (this.curToken.type === TokenType.IDENTIFIER && this.curToken.value.toUpperCase() === 'COLUMN') {
|
||||
this.nextToken();
|
||||
}
|
||||
const col = this.parseColumnDef();
|
||||
return { type: 'ALTER_TABLE', name: tableName, action, column: col };
|
||||
}
|
||||
else if (this.curTokenIs(TokenType.DROP) ||
|
||||
(this._isKeywordAsIdent() && this.curToken.value.toUpperCase() === 'DROP')) {
|
||||
action = 'DROP';
|
||||
this.nextToken();
|
||||
// Optional COLUMN keyword
|
||||
if (this.curToken.type === TokenType.IDENTIFIER && this.curToken.value.toUpperCase() === 'COLUMN') {
|
||||
this.nextToken();
|
||||
}
|
||||
const colName = this.expectIdentifier('column name');
|
||||
return { type: 'ALTER_TABLE', name: tableName, action, column: { name: colName, type: 'string' } };
|
||||
}
|
||||
else {
|
||||
throw this.error('Expected ADD or DROP in ALTER TABLE');
|
||||
}
|
||||
}
|
||||
// ===================================================================
|
||||
// TRUNCATE TABLE
|
||||
// ===================================================================
|
||||
parseTruncateTable() {
|
||||
this.expect(TokenType.TRUNCATE);
|
||||
this.expect(TokenType.TABLE);
|
||||
const tableName = this.expectIdentifier('table name');
|
||||
return { type: 'TRUNCATE_TABLE', name: tableName };
|
||||
}
|
||||
// ===================================================================
|
||||
// DROP TABLE
|
||||
// ===================================================================
|
||||
parseDropTable() {
|
||||
@@ -6773,7 +7038,7 @@ class PluginManager {
|
||||
this.hooks = new Map();
|
||||
}
|
||||
/** 注册插件 */
|
||||
register(plugin) {
|
||||
register(plugin, db) {
|
||||
// 按优先级插入
|
||||
const priority = plugin.priority ?? 0;
|
||||
const insertIndex = this.plugins.findIndex((p) => (p.priority ?? 0) < priority);
|
||||
@@ -6783,8 +7048,8 @@ class PluginManager {
|
||||
else {
|
||||
this.plugins.splice(insertIndex, 0, plugin);
|
||||
}
|
||||
// 安装
|
||||
plugin.install(null); // 实际引用由 MetonaSqlark 注入
|
||||
// 安装(传入 db 实例)
|
||||
plugin.install(db);
|
||||
}
|
||||
/** 卸载插件 */
|
||||
unregister(pluginName) {
|
||||
@@ -6875,12 +7140,12 @@ class MetonaSqlark {
|
||||
// 打开连接
|
||||
await this.engine.open(this.name, this.version);
|
||||
// 初始化执行器和事务管理器
|
||||
this.executor = new QueryExecutor(this.engine);
|
||||
this.executor = new QueryExecutor(this.engine, this.maxRowsPerQuery);
|
||||
this.transactionManager = new TransactionManager(this.engine);
|
||||
// 注册插件
|
||||
if (this.config.plugins) {
|
||||
for (const plugin of this.config.plugins) {
|
||||
this.pluginManager.register(plugin);
|
||||
this.pluginManager.register(plugin, this);
|
||||
}
|
||||
}
|
||||
this.ready = true;
|
||||
@@ -6898,9 +7163,15 @@ class MetonaSqlark {
|
||||
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);
|
||||
try {
|
||||
await this.pluginManager.trigger('beforeCreateTable', schema);
|
||||
await this.engine.createTable(schema);
|
||||
await this.pluginManager.trigger('afterCreateTable', schema);
|
||||
}
|
||||
catch (error) {
|
||||
this._onError(error);
|
||||
throw error;
|
||||
}
|
||||
// 清除缓存
|
||||
this.tableCache.delete(name);
|
||||
}
|
||||
@@ -6917,9 +7188,15 @@ class MetonaSqlark {
|
||||
/** 删除表 */
|
||||
async dropTable(name) {
|
||||
this.ensureReady();
|
||||
await this.pluginManager.trigger('beforeDropTable', name);
|
||||
await this.engine.dropTable(name);
|
||||
await this.pluginManager.trigger('afterDropTable', name);
|
||||
try {
|
||||
await this.pluginManager.trigger('beforeDropTable', name);
|
||||
await this.engine.dropTable(name);
|
||||
await this.pluginManager.trigger('afterDropTable', name);
|
||||
}
|
||||
catch (error) {
|
||||
this._onError(error);
|
||||
throw error;
|
||||
}
|
||||
this.tableCache.delete(name);
|
||||
}
|
||||
/** 获取所有表名 */
|
||||
@@ -6933,8 +7210,15 @@ class MetonaSqlark {
|
||||
this.ensureReady();
|
||||
const startTime = this.debug ? Date.now() : 0;
|
||||
await this.pluginManager.trigger('beforeQuery', sql);
|
||||
const stmt = parse(sql);
|
||||
const result = await this.executor.execute(stmt);
|
||||
let result;
|
||||
try {
|
||||
const stmt = parse(sql);
|
||||
result = await this.executor.execute(stmt);
|
||||
}
|
||||
catch (error) {
|
||||
this._onError(error);
|
||||
throw error;
|
||||
}
|
||||
await this.pluginManager.trigger('afterQuery', sql, result);
|
||||
if (this.debug) {
|
||||
const elapsed = Date.now() - startTime;
|
||||
@@ -6948,9 +7232,15 @@ class MetonaSqlark {
|
||||
async transaction(fn) {
|
||||
this.ensureReady();
|
||||
await this.pluginManager.trigger('beforeTransaction');
|
||||
const result = await this.transactionManager.execute(fn);
|
||||
await this.pluginManager.trigger('afterTransaction');
|
||||
return result;
|
||||
try {
|
||||
const result = await this.transactionManager.execute(fn);
|
||||
await this.pluginManager.trigger('afterTransaction');
|
||||
return result;
|
||||
}
|
||||
catch (error) {
|
||||
this._onError(error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
// ---- 导入导出 ----
|
||||
/** 导出表数据为 JSON */
|
||||
@@ -6961,7 +7251,13 @@ class MetonaSqlark {
|
||||
/** 导入 JSON 数据到表 */
|
||||
async importTable(tableName, data) {
|
||||
this.ensureReady();
|
||||
return this.engine.insert(tableName, data);
|
||||
try {
|
||||
return await this.engine.insert(tableName, data);
|
||||
}
|
||||
catch (error) {
|
||||
this._onError(error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
/** 导出整个数据库为 JSON */
|
||||
async exportAll() {
|
||||
@@ -7031,7 +7327,7 @@ class MetonaSqlark {
|
||||
case 'disk':
|
||||
return diskEngine === 'opfs' ? new OPFSEngine() : new IndexedDBEngine();
|
||||
case 'aria':
|
||||
return new AriaEngine({ storageBackend: diskEngine === 'opfs' ? 'memory' : 'indexeddb' });
|
||||
return new AriaEngine({ storageBackend: diskEngine === 'opfs' ? 'opfs' : 'indexeddb' });
|
||||
case 'hybrid':
|
||||
return new HybridEngine(diskEngine);
|
||||
default:
|
||||
@@ -7178,7 +7474,7 @@ M.getActiveConnections = () => manager.getActiveConnections();
|
||||
/**
|
||||
* metona-sqlark — 入口文件
|
||||
* @module metona-sqlark
|
||||
* @version 0.1.12
|
||||
* @version 0.2.5
|
||||
*
|
||||
* 前端关系型数据库,内存与磁盘双模式。
|
||||
* 支持 Query Builder 链式 API 和 SQL 字符串查询。
|
||||
|
||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+346
-50
@@ -35,7 +35,7 @@
|
||||
// ---------------------------------------------------------------------------
|
||||
// 版本
|
||||
// ---------------------------------------------------------------------------
|
||||
const VERSION = '0.2.0';
|
||||
const VERSION = '0.2.5';
|
||||
|
||||
/**
|
||||
* metona-sqlark Shared WHERE Matcher — 统一的条件匹配逻辑
|
||||
@@ -722,6 +722,13 @@
|
||||
}
|
||||
async idbFind(tableName, query) {
|
||||
const db = this.ensureDB();
|
||||
// 尝试使用 IDB 索引进行等值查询
|
||||
if (query.where) {
|
||||
const indexResult = await this.tryIDBIndexLookup(db, tableName, query);
|
||||
if (indexResult !== null)
|
||||
return indexResult;
|
||||
}
|
||||
// 回退到全量 getAll + 内存过滤
|
||||
return new Promise((resolve, reject) => {
|
||||
const tx = db.transaction(tableName, 'readonly');
|
||||
const req = tx.objectStore(tableName).getAll();
|
||||
@@ -744,6 +751,64 @@
|
||||
req.onerror = () => reject(new DatabaseError(`Find failed for "${tableName}"`, 'IDB_READ_ERROR', req.error));
|
||||
});
|
||||
}
|
||||
/** 尝试使用 IDB 索引进行等值查询,成功返回结果,不适用返回 null */
|
||||
async tryIDBIndexLookup(db, tableName, query) {
|
||||
if (!query.where)
|
||||
return null;
|
||||
for (const [col, condition] of Object.entries(query.where)) {
|
||||
// 跳过逻辑组合符
|
||||
if (col === '$and' || col === '$or' || col === '$not')
|
||||
continue;
|
||||
// 只处理等值查询
|
||||
let targetValue;
|
||||
if (typeof condition !== 'object' || condition === null) {
|
||||
targetValue = condition;
|
||||
}
|
||||
else if ('$eq' in condition) {
|
||||
targetValue = condition.$eq;
|
||||
}
|
||||
else {
|
||||
continue;
|
||||
}
|
||||
// 检查是否有对应的 IDB 索引
|
||||
const indexName = `idx_${col}`;
|
||||
try {
|
||||
return await new Promise((resolve, reject) => {
|
||||
const tx = db.transaction(tableName, 'readonly');
|
||||
const store = tx.objectStore(tableName);
|
||||
if (!store.indexNames.contains(indexName)) {
|
||||
resolve(null); // 没有索引,回退
|
||||
return;
|
||||
}
|
||||
const index = store.index(indexName);
|
||||
const req = index.getAll(targetValue);
|
||||
req.onsuccess = () => {
|
||||
let results = req.result ?? [];
|
||||
// 如果有其他 WHERE 条件,继续过滤
|
||||
const otherKeys = Object.keys(query.where).filter((k) => k !== col && k !== '$and' && k !== '$or' && k !== '$not');
|
||||
if (otherKeys.length > 0) {
|
||||
results = results.filter((row) => matchWhere(row, query.where));
|
||||
}
|
||||
if (query.orderBy && query.orderBy.length > 0) {
|
||||
results = applyOrderBy(results, query.orderBy);
|
||||
}
|
||||
const offset = query.offset ?? 0;
|
||||
const limit = query.limit ?? results.length;
|
||||
results = results.slice(offset, offset + limit);
|
||||
if (query.columns && query.columns.length > 0 && query.columns[0] !== '*') {
|
||||
results = results.map((r) => projectColumns(r, query.columns));
|
||||
}
|
||||
resolve(results);
|
||||
};
|
||||
req.onerror = () => reject(new DatabaseError(`Index lookup failed for "${tableName}.${col}"`, 'IDB_READ_ERROR', req.error));
|
||||
});
|
||||
}
|
||||
catch {
|
||||
return null; // 索引不可用,回退
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
async idbUpdate(tableName, query, updates) {
|
||||
const db = this.ensureDB();
|
||||
return new Promise((resolve, reject) => {
|
||||
@@ -2173,18 +2238,26 @@
|
||||
return -1;
|
||||
}
|
||||
locateBlockGE(key) {
|
||||
for (let i = 0; i < this.indexEntries.length; i++) {
|
||||
if (this.indexEntries[i].key >= key)
|
||||
return i;
|
||||
let lo = 0, hi = this.indexEntries.length;
|
||||
while (lo < hi) {
|
||||
const mid = (lo + hi) >> 1;
|
||||
if (this.indexEntries[mid].key < key)
|
||||
lo = mid + 1;
|
||||
else
|
||||
hi = mid;
|
||||
}
|
||||
return this.indexEntries.length - 1;
|
||||
return lo < this.indexEntries.length ? lo : this.indexEntries.length - 1;
|
||||
}
|
||||
locateBlockLE(key) {
|
||||
for (let i = this.indexEntries.length - 1; i >= 0; i--) {
|
||||
if (this.indexEntries[i].key <= key)
|
||||
return i;
|
||||
let lo = 0, hi = this.indexEntries.length;
|
||||
while (lo < hi) {
|
||||
const mid = (lo + hi) >> 1;
|
||||
if (this.indexEntries[mid].key <= key)
|
||||
lo = mid + 1;
|
||||
else
|
||||
hi = mid;
|
||||
}
|
||||
return 0;
|
||||
return lo > 0 ? lo - 1 : 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2560,7 +2633,11 @@
|
||||
// =======================================================================
|
||||
// Compaction
|
||||
// =======================================================================
|
||||
/** 同步执行 Compaction(简化版,仅供内部调用) */
|
||||
/** 同步执行 Compaction(public,供 VACUUM 等外部调用) */
|
||||
compactLevel(level) {
|
||||
this.compactLevelSync(level);
|
||||
}
|
||||
/** 同步执行 Compaction(简化版,内部实现) */
|
||||
compactLevelSync(level) {
|
||||
if (level >= MAX_LSM_LEVELS - 1)
|
||||
return;
|
||||
@@ -2713,8 +2790,8 @@
|
||||
// =======================================================================
|
||||
// 写入
|
||||
// =======================================================================
|
||||
/** 追加一条 WAL 记录 */
|
||||
append(record) {
|
||||
/** 追加一条 WAL 记录(full 模式同步等待写入完成) */
|
||||
async append(record) {
|
||||
if (!this.enabled)
|
||||
return;
|
||||
this.lsn++;
|
||||
@@ -2725,10 +2802,13 @@
|
||||
};
|
||||
const bytes = this.encodeRecord(fullRecord);
|
||||
if (this.syncMode === 'full') {
|
||||
this.store.append(bytes).catch(() => {
|
||||
try {
|
||||
await this.store.append(bytes);
|
||||
}
|
||||
catch {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn('[AriaEngine WAL] Failed to append record');
|
||||
});
|
||||
}
|
||||
}
|
||||
else if (this.syncMode === 'batch') {
|
||||
this.buffer.push(bytes);
|
||||
@@ -2915,19 +2995,26 @@
|
||||
// CheckpointManager
|
||||
// ---------------------------------------------------------------------------
|
||||
class CheckpointManager {
|
||||
constructor(lsm, wal, flushable = null, interval = 1000) {
|
||||
constructor(lsm, wal, flushable = null, interval = 1000, walSizeThreshold = 16 * 1024 * 1024) {
|
||||
this.opCount = 0;
|
||||
this.lsm = lsm;
|
||||
this.wal = wal;
|
||||
this.flushable = flushable;
|
||||
this.interval = interval;
|
||||
this.walSizeThreshold = walSizeThreshold;
|
||||
}
|
||||
async tick() {
|
||||
this.opCount++;
|
||||
if (this.opCount >= this.interval) {
|
||||
// 检查操作计数或 WAL 大小是否超阈值
|
||||
if (this.opCount >= this.interval || this.getWALEstimatedSize() >= this.walSizeThreshold) {
|
||||
await this.checkpoint();
|
||||
}
|
||||
}
|
||||
/** 估算 WAL 大小 */
|
||||
getWALEstimatedSize() {
|
||||
const count = typeof this.wal.getBufferedCount === 'function' ? this.wal.getBufferedCount() : 0;
|
||||
return count * 200;
|
||||
}
|
||||
async checkpoint() {
|
||||
await this.lsm.flush();
|
||||
if (this.flushable) {
|
||||
@@ -3909,11 +3996,70 @@
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* AriaEngine Crypto — 页面级 AES-GCM 加密
|
||||
* @module engine/aria/crypto
|
||||
*
|
||||
* v0.2.5: 改为实例化 CryptoManager,避免多实例共享全局状态。
|
||||
* 保留全局函数兼容旧代码(委托给全局单例)。
|
||||
*/
|
||||
const ALGO = 'AES-GCM';
|
||||
const IV_LENGTH = 12;
|
||||
/**
|
||||
* CryptoManager — 实例级加密管理器
|
||||
* 每个 AriaEngine 实例可拥有独立的加密配置。
|
||||
*/
|
||||
class CryptoManager {
|
||||
constructor() {
|
||||
this.cryptoKey = null;
|
||||
this._enabled = false;
|
||||
}
|
||||
get enabled() { return this._enabled; }
|
||||
async init(password, salt) {
|
||||
const enc = new TextEncoder();
|
||||
const keyMaterial = await crypto.subtle.importKey('raw', enc.encode(password), 'PBKDF2', false, ['deriveKey']);
|
||||
const actualSalt = salt || crypto.getRandomValues(new Uint8Array(16));
|
||||
this.cryptoKey = await crypto.subtle.deriveKey({ name: 'PBKDF2', salt: actualSalt, iterations: 100000, hash: 'SHA-256' }, keyMaterial, { name: ALGO, length: 256 }, false, ['encrypt', 'decrypt']);
|
||||
this._enabled = true;
|
||||
return actualSalt;
|
||||
}
|
||||
async encryptPage(data) {
|
||||
if (!this.cryptoKey)
|
||||
throw new Error('Crypto not initialized');
|
||||
const iv = crypto.getRandomValues(new Uint8Array(IV_LENGTH));
|
||||
const ciphertext = await crypto.subtle.encrypt({ name: ALGO, iv }, this.cryptoKey, data);
|
||||
return { iv: iv, data: ciphertext };
|
||||
}
|
||||
async decryptPage(iv, data) {
|
||||
if (!this.cryptoKey)
|
||||
throw new Error('Crypto not initialized');
|
||||
return crypto.subtle.decrypt({ name: ALGO, iv }, this.cryptoKey, data);
|
||||
}
|
||||
close() {
|
||||
this.cryptoKey = null;
|
||||
this._enabled = false;
|
||||
}
|
||||
}
|
||||
// ---------------------------------------------------------------------------
|
||||
// 全局兼容层(旧代码仍可使用全局函数)
|
||||
// ---------------------------------------------------------------------------
|
||||
const globalCrypto = new CryptoManager();
|
||||
/** @deprecated 使用 CryptoManager 实例代替 */
|
||||
function isCryptoEnabled() { return globalCrypto.enabled; }
|
||||
/** @deprecated 使用 CryptoManager 实例代替 */
|
||||
async function encryptPage(data) {
|
||||
return globalCrypto.encryptPage(data);
|
||||
}
|
||||
/** @deprecated 使用 CryptoManager 实例代替 */
|
||||
async function decryptPage(iv, data) {
|
||||
return globalCrypto.decryptPage(iv, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* AriaEngine — 自研页面式存储引擎主类
|
||||
* @module engine/aria/index
|
||||
*
|
||||
* v0.2.4: 二级索引 + MVCC 集成 + 生产加固
|
||||
* v0.2.5: WAL 同步修复 + MVCC 接入 + 版本统一 + 生产加固
|
||||
*/
|
||||
// ---------------------------------------------------------------------------
|
||||
// AriaEngine
|
||||
@@ -4033,8 +4179,8 @@
|
||||
this.applyWALRecord(r);
|
||||
}
|
||||
}
|
||||
// 8. Checkpoint Manager(BufferPool 暂简化,使用 flush 替代)
|
||||
this.checkpointManager = new CheckpointManager(this.lsm, this.wal, { flushAll: async () => { await this.lsm.flush(); } }, this.config.checkpointInterval);
|
||||
// 8. Checkpoint Manager(接入 WAL 大小阈值)
|
||||
this.checkpointManager = new CheckpointManager(this.lsm, this.wal, { flushAll: async () => { await this.lsm.flush(); } }, this.config.checkpointInterval, this.config.walSizeThreshold);
|
||||
this.opened = true;
|
||||
}
|
||||
async close() {
|
||||
@@ -4077,7 +4223,7 @@
|
||||
}
|
||||
}
|
||||
await this.persistSchemas();
|
||||
this.wal.append({
|
||||
await this.wal.append({
|
||||
type: WALRecordType.CREATE_TABLE,
|
||||
txnId: 0,
|
||||
tableName: schema.name,
|
||||
@@ -4097,7 +4243,7 @@
|
||||
this.schemas.delete(tableName);
|
||||
this.tablePKs.delete(tableName);
|
||||
await this.persistSchemas();
|
||||
this.wal.append({
|
||||
await this.wal.append({
|
||||
type: WALRecordType.DROP_TABLE,
|
||||
txnId: 0,
|
||||
tableName,
|
||||
@@ -4134,8 +4280,9 @@
|
||||
throw new DatabaseError(`Duplicate primary key "${pkValue}" in table "${tableName}"`, 'DUPLICATE_KEY');
|
||||
}
|
||||
if (this.currentTxnId && this.txnSnapshot) {
|
||||
// Within transaction: buffer to snapshot
|
||||
// Within transaction: buffer to snapshot + MVCC version chain
|
||||
this.txnSnapshot.set(key, validated);
|
||||
this.mvcc.writeVersion(tableName, pkValue, validated, this.currentTxnId);
|
||||
}
|
||||
else {
|
||||
// Direct write to LSM (PK index)
|
||||
@@ -4144,7 +4291,7 @@
|
||||
// 更新二级索引
|
||||
this.updateSecondaryIndexes(tableName, pkValue, validated, null);
|
||||
pks.push(pkValue);
|
||||
this.wal.append({
|
||||
await this.wal.append({
|
||||
type: WALRecordType.INSERT,
|
||||
txnId: this.currentTxnId ?? 0,
|
||||
tableName,
|
||||
@@ -4225,12 +4372,13 @@
|
||||
this.validateRow(schema, updated);
|
||||
if (this.currentTxnId && this.txnSnapshot) {
|
||||
this.txnSnapshot.set(key, updated);
|
||||
this.mvcc.writeVersion(tableName, String(row[pkCol]), updated, this.currentTxnId);
|
||||
}
|
||||
else {
|
||||
this.lsm.put(key, updated);
|
||||
}
|
||||
count++;
|
||||
this.wal.append({
|
||||
await this.wal.append({
|
||||
type: WALRecordType.UPDATE,
|
||||
txnId: this.currentTxnId ?? 0,
|
||||
tableName,
|
||||
@@ -4255,14 +4403,15 @@
|
||||
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
|
||||
// Buffer delete in snapshot + MVCC tombstone
|
||||
this.txnSnapshot.set(key, { __txn_deleted: true });
|
||||
this.mvcc.deleteVersion(tableName, String(row[pkCol]), this.currentTxnId);
|
||||
}
|
||||
else {
|
||||
this.lsm.delete(key);
|
||||
}
|
||||
count++;
|
||||
this.wal.append({
|
||||
await this.wal.append({
|
||||
type: WALRecordType.DELETE,
|
||||
txnId: this.currentTxnId ?? 0,
|
||||
tableName,
|
||||
@@ -4301,7 +4450,7 @@
|
||||
throw new DatabaseError('Transaction already in progress', 'TX_ACTIVE');
|
||||
this.currentTxnId = this.mvcc.beginTransaction();
|
||||
this.txnSnapshot = new Map();
|
||||
this.wal.append({
|
||||
await this.wal.append({
|
||||
type: WALRecordType.BEGIN,
|
||||
txnId: this.currentTxnId,
|
||||
tableName: '',
|
||||
@@ -4322,7 +4471,7 @@
|
||||
}
|
||||
}
|
||||
this.mvcc.commitTransaction(this.currentTxnId);
|
||||
this.wal.append({
|
||||
await this.wal.append({
|
||||
type: WALRecordType.COMMIT,
|
||||
txnId: this.currentTxnId,
|
||||
tableName: '',
|
||||
@@ -4337,7 +4486,7 @@
|
||||
throw new DatabaseError('No active transaction', 'TX_NONE');
|
||||
this.mvcc.rollbackTransaction(this.currentTxnId);
|
||||
this.txnSnapshot = null;
|
||||
this.wal.append({
|
||||
await this.wal.append({
|
||||
type: WALRecordType.ROLLBACK,
|
||||
txnId: this.currentTxnId,
|
||||
tableName: '',
|
||||
@@ -4469,6 +4618,17 @@
|
||||
const compressed = compressLZ4(new Uint8Array(buf));
|
||||
buf = compressed.buffer.slice(compressed.byteOffset, compressed.byteOffset + compressed.byteLength);
|
||||
}
|
||||
// 加密(若启用)
|
||||
if (isCryptoEnabled()) {
|
||||
const enc = await encryptPage(buf);
|
||||
const header = new Uint8Array(12 + 4); // IV(12) + originalLen(4)
|
||||
header.set(enc.iv, 0);
|
||||
new DataView(header.buffer).setUint32(12, data.byteLength, false);
|
||||
const combined = new Uint8Array(header.length + enc.data.byteLength);
|
||||
combined.set(header, 0);
|
||||
combined.set(new Uint8Array(enc.data), header.length);
|
||||
buf = combined.buffer;
|
||||
}
|
||||
await this.backend.write(`sst_${id}`, buf);
|
||||
},
|
||||
load: async (id) => {
|
||||
@@ -4476,6 +4636,14 @@
|
||||
if (!raw)
|
||||
return null;
|
||||
let buf = new Uint8Array(raw);
|
||||
// 解密(若数据带加密头)
|
||||
if (isCryptoEnabled() && buf.length > 16) {
|
||||
const iv = buf.slice(0, 12);
|
||||
const origLen = new DataView(buf.buffer, buf.byteOffset + 12, 4).getUint32(0, false);
|
||||
const ciphertext = buf.slice(16).buffer;
|
||||
const decrypted = await decryptPage(iv, ciphertext);
|
||||
buf = new Uint8Array(decrypted, 0, origLen);
|
||||
}
|
||||
// 解压(若启用)
|
||||
if (this.config.compression) {
|
||||
const decompressed = decompressLZ4(buf, buf.length * 2); // 估计原始大小
|
||||
@@ -4695,6 +4863,10 @@
|
||||
this.mvcc.gc(50);
|
||||
}
|
||||
}
|
||||
/** 估算 WAL 大小(字节) */
|
||||
getWALEstimatedSize() {
|
||||
return this.wal.getBufferedCount() * 200; // 粗略估算每条 ~200B
|
||||
}
|
||||
/**
|
||||
* ANALYZE: 收集表统计信息
|
||||
* 返回行数、平均行大小、索引深度等
|
||||
@@ -4765,7 +4937,7 @@
|
||||
// 压缩各层级
|
||||
for (let level = 0; level < 6; level++) {
|
||||
if (this.lsm.getStats().levelCounts[level] >= 2) {
|
||||
this.lsm.compactLevelSync(level);
|
||||
this.lsm.compactLevel(level);
|
||||
}
|
||||
}
|
||||
// GC MVCC 版本(保留最新 10 个)
|
||||
@@ -5226,8 +5398,13 @@
|
||||
// Executor
|
||||
// ---------------------------------------------------------------------------
|
||||
class QueryExecutor {
|
||||
constructor(engine) {
|
||||
constructor(engine, maxRowsPerQuery = 0) {
|
||||
this.engine = engine;
|
||||
this.maxRowsPerQuery = maxRowsPerQuery;
|
||||
}
|
||||
/** 设置查询结果行数上限 */
|
||||
setMaxRowsPerQuery(max) {
|
||||
this.maxRowsPerQuery = max;
|
||||
}
|
||||
async execute(stmt) {
|
||||
switch (stmt.type) {
|
||||
@@ -5238,6 +5415,8 @@
|
||||
case 'DELETE': return this.executeDelete(stmt);
|
||||
case 'CREATE_TABLE': return this.executeCreateTable(stmt);
|
||||
case 'DROP_TABLE': return this.executeDropTable(stmt);
|
||||
case 'ALTER_TABLE': return this.executeAlterTable(stmt);
|
||||
case 'TRUNCATE_TABLE': return this.executeTruncateTable(stmt);
|
||||
default: throw new DatabaseError('Unknown statement type', 'UNKNOWN_STATEMENT');
|
||||
}
|
||||
}
|
||||
@@ -5302,6 +5481,10 @@
|
||||
if (!hasGroupBy && !hasAggregate && stmt.columns.length > 0 && stmt.columns[0] !== '*') {
|
||||
rows = rows.map((row) => projectColumns(row, stmt.columns));
|
||||
}
|
||||
// 全局行数上限保护
|
||||
if (this.maxRowsPerQuery > 0 && rows.length > this.maxRowsPerQuery) {
|
||||
rows = rows.slice(0, this.maxRowsPerQuery);
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
// ---- JOIN ----
|
||||
@@ -5468,6 +5651,35 @@
|
||||
}
|
||||
return this.engine.dropTable(stmt.name);
|
||||
}
|
||||
async executeAlterTable(stmt) {
|
||||
const exists = await this.engine.hasTable(stmt.name);
|
||||
if (!exists)
|
||||
throw new DatabaseError(`Table "${stmt.name}" does not exist`, 'TABLE_NOT_FOUND');
|
||||
const schema = await this.engine.getTableSchema(stmt.name);
|
||||
if (!schema)
|
||||
return;
|
||||
if (stmt.action === 'ADD') {
|
||||
if (schema.columns[stmt.column.name]) {
|
||||
throw new DatabaseError(`Column "${stmt.column.name}" already exists in table "${stmt.name}"`, 'COLUMN_EXISTS');
|
||||
}
|
||||
schema.columns[stmt.column.name] = astColumnToColumnDef(stmt.column);
|
||||
}
|
||||
else if (stmt.action === 'DROP') {
|
||||
if (!schema.columns[stmt.column.name]) {
|
||||
throw new DatabaseError(`Column "${stmt.column.name}" does not exist in table "${stmt.name}"`, 'COLUMN_NOT_FOUND');
|
||||
}
|
||||
delete schema.columns[stmt.column.name];
|
||||
}
|
||||
// 重建表结构
|
||||
await this.engine.dropTable(stmt.name);
|
||||
await this.engine.createTable(schema);
|
||||
}
|
||||
async executeTruncateTable(stmt) {
|
||||
const exists = await this.engine.hasTable(stmt.name);
|
||||
if (!exists)
|
||||
throw new DatabaseError(`Table "${stmt.name}" does not exist`, 'TABLE_NOT_FOUND');
|
||||
return this.engine.clear(stmt.name);
|
||||
}
|
||||
getEngine() { return this.engine; }
|
||||
// ===================================================================
|
||||
// 无 GROUP BY 时的聚合计算
|
||||
@@ -5622,6 +5834,9 @@
|
||||
TokenType["IF"] = "IF";
|
||||
TokenType["EXISTS"] = "EXISTS";
|
||||
TokenType["FALSE"] = "FALSE";
|
||||
TokenType["ALTER"] = "ALTER";
|
||||
TokenType["ADD"] = "ADD";
|
||||
TokenType["TRUNCATE"] = "TRUNCATE";
|
||||
// JOIN 相关
|
||||
TokenType["INNER"] = "INNER";
|
||||
TokenType["LEFT"] = "LEFT";
|
||||
@@ -5700,6 +5915,9 @@
|
||||
'BETWEEN': TokenType.BETWEEN,
|
||||
'IF': TokenType.IF,
|
||||
'EXISTS': TokenType.EXISTS,
|
||||
'ALTER': TokenType.ALTER,
|
||||
'ADD': TokenType.ADD,
|
||||
'TRUNCATE': TokenType.TRUNCATE,
|
||||
// JOIN
|
||||
'INNER': TokenType.INNER,
|
||||
'LEFT': TokenType.LEFT,
|
||||
@@ -5982,6 +6200,10 @@
|
||||
return this.parseCreateTable();
|
||||
case TokenType.DROP:
|
||||
return this.parseDropTable();
|
||||
case TokenType.ALTER:
|
||||
return this.parseAlterTable();
|
||||
case TokenType.TRUNCATE:
|
||||
return this.parseTruncateTable();
|
||||
default:
|
||||
throw this.error(`Unexpected token "${this.curToken.value}"`);
|
||||
}
|
||||
@@ -6312,6 +6534,49 @@
|
||||
return 'RESTRICT';
|
||||
}
|
||||
// ===================================================================
|
||||
// ALTER TABLE
|
||||
// ===================================================================
|
||||
parseAlterTable() {
|
||||
this.expect(TokenType.ALTER);
|
||||
this.expect(TokenType.TABLE);
|
||||
const tableName = this.expectIdentifier('table name');
|
||||
// ADD COLUMN / DROP COLUMN
|
||||
let action;
|
||||
if (this.curTokenIs(TokenType.ADD)) {
|
||||
action = 'ADD';
|
||||
this.nextToken();
|
||||
// Optional COLUMN keyword
|
||||
if (this.curToken.type === TokenType.IDENTIFIER && this.curToken.value.toUpperCase() === 'COLUMN') {
|
||||
this.nextToken();
|
||||
}
|
||||
const col = this.parseColumnDef();
|
||||
return { type: 'ALTER_TABLE', name: tableName, action, column: col };
|
||||
}
|
||||
else if (this.curTokenIs(TokenType.DROP) ||
|
||||
(this._isKeywordAsIdent() && this.curToken.value.toUpperCase() === 'DROP')) {
|
||||
action = 'DROP';
|
||||
this.nextToken();
|
||||
// Optional COLUMN keyword
|
||||
if (this.curToken.type === TokenType.IDENTIFIER && this.curToken.value.toUpperCase() === 'COLUMN') {
|
||||
this.nextToken();
|
||||
}
|
||||
const colName = this.expectIdentifier('column name');
|
||||
return { type: 'ALTER_TABLE', name: tableName, action, column: { name: colName, type: 'string' } };
|
||||
}
|
||||
else {
|
||||
throw this.error('Expected ADD or DROP in ALTER TABLE');
|
||||
}
|
||||
}
|
||||
// ===================================================================
|
||||
// TRUNCATE TABLE
|
||||
// ===================================================================
|
||||
parseTruncateTable() {
|
||||
this.expect(TokenType.TRUNCATE);
|
||||
this.expect(TokenType.TABLE);
|
||||
const tableName = this.expectIdentifier('table name');
|
||||
return { type: 'TRUNCATE_TABLE', name: tableName };
|
||||
}
|
||||
// ===================================================================
|
||||
// DROP TABLE
|
||||
// ===================================================================
|
||||
parseDropTable() {
|
||||
@@ -6779,7 +7044,7 @@
|
||||
this.hooks = new Map();
|
||||
}
|
||||
/** 注册插件 */
|
||||
register(plugin) {
|
||||
register(plugin, db) {
|
||||
// 按优先级插入
|
||||
const priority = plugin.priority ?? 0;
|
||||
const insertIndex = this.plugins.findIndex((p) => (p.priority ?? 0) < priority);
|
||||
@@ -6789,8 +7054,8 @@
|
||||
else {
|
||||
this.plugins.splice(insertIndex, 0, plugin);
|
||||
}
|
||||
// 安装
|
||||
plugin.install(null); // 实际引用由 MetonaSqlark 注入
|
||||
// 安装(传入 db 实例)
|
||||
plugin.install(db);
|
||||
}
|
||||
/** 卸载插件 */
|
||||
unregister(pluginName) {
|
||||
@@ -6881,12 +7146,12 @@
|
||||
// 打开连接
|
||||
await this.engine.open(this.name, this.version);
|
||||
// 初始化执行器和事务管理器
|
||||
this.executor = new QueryExecutor(this.engine);
|
||||
this.executor = new QueryExecutor(this.engine, this.maxRowsPerQuery);
|
||||
this.transactionManager = new TransactionManager(this.engine);
|
||||
// 注册插件
|
||||
if (this.config.plugins) {
|
||||
for (const plugin of this.config.plugins) {
|
||||
this.pluginManager.register(plugin);
|
||||
this.pluginManager.register(plugin, this);
|
||||
}
|
||||
}
|
||||
this.ready = true;
|
||||
@@ -6904,9 +7169,15 @@
|
||||
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);
|
||||
try {
|
||||
await this.pluginManager.trigger('beforeCreateTable', schema);
|
||||
await this.engine.createTable(schema);
|
||||
await this.pluginManager.trigger('afterCreateTable', schema);
|
||||
}
|
||||
catch (error) {
|
||||
this._onError(error);
|
||||
throw error;
|
||||
}
|
||||
// 清除缓存
|
||||
this.tableCache.delete(name);
|
||||
}
|
||||
@@ -6923,9 +7194,15 @@
|
||||
/** 删除表 */
|
||||
async dropTable(name) {
|
||||
this.ensureReady();
|
||||
await this.pluginManager.trigger('beforeDropTable', name);
|
||||
await this.engine.dropTable(name);
|
||||
await this.pluginManager.trigger('afterDropTable', name);
|
||||
try {
|
||||
await this.pluginManager.trigger('beforeDropTable', name);
|
||||
await this.engine.dropTable(name);
|
||||
await this.pluginManager.trigger('afterDropTable', name);
|
||||
}
|
||||
catch (error) {
|
||||
this._onError(error);
|
||||
throw error;
|
||||
}
|
||||
this.tableCache.delete(name);
|
||||
}
|
||||
/** 获取所有表名 */
|
||||
@@ -6939,8 +7216,15 @@
|
||||
this.ensureReady();
|
||||
const startTime = this.debug ? Date.now() : 0;
|
||||
await this.pluginManager.trigger('beforeQuery', sql);
|
||||
const stmt = parse(sql);
|
||||
const result = await this.executor.execute(stmt);
|
||||
let result;
|
||||
try {
|
||||
const stmt = parse(sql);
|
||||
result = await this.executor.execute(stmt);
|
||||
}
|
||||
catch (error) {
|
||||
this._onError(error);
|
||||
throw error;
|
||||
}
|
||||
await this.pluginManager.trigger('afterQuery', sql, result);
|
||||
if (this.debug) {
|
||||
const elapsed = Date.now() - startTime;
|
||||
@@ -6954,9 +7238,15 @@
|
||||
async transaction(fn) {
|
||||
this.ensureReady();
|
||||
await this.pluginManager.trigger('beforeTransaction');
|
||||
const result = await this.transactionManager.execute(fn);
|
||||
await this.pluginManager.trigger('afterTransaction');
|
||||
return result;
|
||||
try {
|
||||
const result = await this.transactionManager.execute(fn);
|
||||
await this.pluginManager.trigger('afterTransaction');
|
||||
return result;
|
||||
}
|
||||
catch (error) {
|
||||
this._onError(error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
// ---- 导入导出 ----
|
||||
/** 导出表数据为 JSON */
|
||||
@@ -6967,7 +7257,13 @@
|
||||
/** 导入 JSON 数据到表 */
|
||||
async importTable(tableName, data) {
|
||||
this.ensureReady();
|
||||
return this.engine.insert(tableName, data);
|
||||
try {
|
||||
return await this.engine.insert(tableName, data);
|
||||
}
|
||||
catch (error) {
|
||||
this._onError(error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
/** 导出整个数据库为 JSON */
|
||||
async exportAll() {
|
||||
@@ -7037,7 +7333,7 @@
|
||||
case 'disk':
|
||||
return diskEngine === 'opfs' ? new OPFSEngine() : new IndexedDBEngine();
|
||||
case 'aria':
|
||||
return new AriaEngine({ storageBackend: diskEngine === 'opfs' ? 'memory' : 'indexeddb' });
|
||||
return new AriaEngine({ storageBackend: diskEngine === 'opfs' ? 'opfs' : 'indexeddb' });
|
||||
case 'hybrid':
|
||||
return new HybridEngine(diskEngine);
|
||||
default:
|
||||
@@ -7184,7 +7480,7 @@
|
||||
/**
|
||||
* metona-sqlark — 入口文件
|
||||
* @module metona-sqlark
|
||||
* @version 0.1.12
|
||||
* @version 0.2.5
|
||||
*
|
||||
* 前端关系型数据库,内存与磁盘双模式。
|
||||
* 支持 Query Builder 链式 API 和 SQL 字符串查询。
|
||||
|
||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user