release: v0.2.1 生产加固 — WAL CRC/约束激活/Hybrid提交顺序/RESTRICT外键/浏览器兼容表/debug模式/onError回调
This commit is contained in:
Vendored
+59
-4
@@ -11,6 +11,8 @@ const DB_DEFAULTS = Object.freeze({
|
||||
mode: 'hybrid',
|
||||
diskEngine: 'indexeddb',
|
||||
version: 1,
|
||||
maxRowsPerQuery: 0, // 0 = 不限制
|
||||
debug: false,
|
||||
});
|
||||
// ---------------------------------------------------------------------------
|
||||
// 错误类型
|
||||
@@ -493,7 +495,7 @@ class MemoryEngine {
|
||||
if (refTableName === tableName)
|
||||
continue;
|
||||
for (const [colName, colDef] of Object.entries(refSchema.columns)) {
|
||||
if (!colDef.references || !colDef.onDelete || colDef.onDelete === 'RESTRICT')
|
||||
if (!colDef.references || !colDef.onDelete)
|
||||
continue;
|
||||
const [refTable, refCol] = colDef.references.split('.');
|
||||
if (refTable !== tableName)
|
||||
@@ -508,6 +510,10 @@ class MemoryEngine {
|
||||
toDelete.push(refPk);
|
||||
}
|
||||
}
|
||||
// RESTRICT: 存在引用行时禁止删除
|
||||
if (colDef.onDelete === 'RESTRICT' && toDelete.length > 0) {
|
||||
throw new DatabaseError(`Cannot delete from "${tableName}": foreign key "${colName}" in "${refTableName}" has dependent rows`, 'FOREIGN_KEY_VIOLATION');
|
||||
}
|
||||
if (colDef.onDelete === 'CASCADE') {
|
||||
// 递归级联
|
||||
for (const refPk of toDelete) {
|
||||
@@ -1796,8 +1802,12 @@ class SSTableReader {
|
||||
}
|
||||
/** 范围扫描 */
|
||||
rangeScan(startKey, endKey, callback) {
|
||||
if (this.indexEntries.length === 0)
|
||||
return;
|
||||
const startBlockIdx = Math.max(0, this.locateBlockGE(startKey));
|
||||
const endBlockIdx = Math.min(this.indexEntries.length - 1, this.locateBlockLE(endKey));
|
||||
if (startBlockIdx < 0 || endBlockIdx < 0 || startBlockIdx > endBlockIdx)
|
||||
return;
|
||||
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);
|
||||
@@ -2528,6 +2538,7 @@ class WAL {
|
||||
let offset = 0;
|
||||
while (offset + 15 <= data.byteLength) {
|
||||
try {
|
||||
const recordStart = offset;
|
||||
const lsn = view.getUint32(offset, false);
|
||||
offset += 4;
|
||||
const type = view.getUint8(offset);
|
||||
@@ -2536,14 +2547,20 @@ class WAL {
|
||||
offset += 4;
|
||||
const tableLen = view.getUint16(offset, false);
|
||||
offset += 2;
|
||||
if (offset + tableLen > data.byteLength)
|
||||
break;
|
||||
const tableName = new TextDecoder().decode(data.slice(offset, offset + tableLen));
|
||||
offset += tableLen;
|
||||
const keyLen = view.getUint16(offset, false);
|
||||
offset += 2;
|
||||
if (offset + keyLen > data.byteLength)
|
||||
break;
|
||||
const key = new TextDecoder().decode(data.slice(offset, offset + keyLen));
|
||||
offset += keyLen;
|
||||
const jsonLen = view.getUint32(offset, false);
|
||||
offset += 4;
|
||||
if (offset + jsonLen > data.byteLength)
|
||||
break;
|
||||
let recordData;
|
||||
if (jsonLen > 0) {
|
||||
const json = new TextDecoder().decode(data.slice(offset, offset + jsonLen));
|
||||
@@ -2553,8 +2570,19 @@ class WAL {
|
||||
catch { /* ok */ }
|
||||
}
|
||||
offset += jsonLen;
|
||||
// 跳过 CRC
|
||||
// 验证 CRC(跨记录数据计算,不含 CRC 自身)
|
||||
const storedCrc = view.getUint32(offset, false);
|
||||
offset += 4;
|
||||
const recordBytes = data.slice(recordStart, offset - 4);
|
||||
let computedCrc = 0;
|
||||
for (let i = 0; i < recordBytes.length; i++) {
|
||||
computedCrc = ((computedCrc << 5) - computedCrc + recordBytes[i]) | 0;
|
||||
}
|
||||
if ((computedCrc >>> 0) !== storedCrc) {
|
||||
// CRC 不匹配,跳过此损坏记录
|
||||
console.warn(`[AriaEngine WAL] CRC mismatch at record LSN=${lsn}, skipping`);
|
||||
continue;
|
||||
}
|
||||
records.push({
|
||||
lsn,
|
||||
type,
|
||||
@@ -2562,7 +2590,7 @@ class WAL {
|
||||
tableName,
|
||||
key,
|
||||
data: recordData,
|
||||
checksum: 0,
|
||||
checksum: storedCrc,
|
||||
});
|
||||
}
|
||||
catch {
|
||||
@@ -3474,8 +3502,16 @@ class HybridEngine {
|
||||
await this.diskEngine.beginTransaction();
|
||||
}
|
||||
async commitTransaction() {
|
||||
await this.memoryEngine.commitTransaction();
|
||||
// 先写磁盘,保证持久化优先;磁盘失败则回滚内存
|
||||
await this.diskEngine.commitTransaction();
|
||||
try {
|
||||
await this.memoryEngine.commitTransaction();
|
||||
}
|
||||
catch {
|
||||
// 内存提交失败时回滚磁盘
|
||||
await this.diskEngine.rollbackTransaction();
|
||||
throw new DatabaseError('Hybrid commit failed: memory engine error after disk commit', 'TX_COMMIT_ERROR');
|
||||
}
|
||||
}
|
||||
async rollbackTransaction() {
|
||||
await this.memoryEngine.rollbackTransaction();
|
||||
@@ -5408,6 +5444,10 @@ class PluginManager {
|
||||
class MetonaSqlark {
|
||||
/** 获取版本号 */
|
||||
get version() { return this._version; }
|
||||
/** 查询结果行数上限 */
|
||||
get maxRowsPerQuery() { return this.config.maxRowsPerQuery ?? 0; }
|
||||
/** 调试模式 */
|
||||
get debug() { return this.config.debug ?? false; }
|
||||
constructor(config) {
|
||||
this.ready = false;
|
||||
this.tableCache = new Map();
|
||||
@@ -5591,6 +5631,21 @@ class MetonaSqlark {
|
||||
throw new DatabaseError('Database not initialized. Call await db.init() first.', 'DB_NOT_READY');
|
||||
}
|
||||
}
|
||||
/** 错误回调分发 */
|
||||
_onError(error) {
|
||||
if (this.config.onError) {
|
||||
try {
|
||||
this.config.onError(error);
|
||||
}
|
||||
catch { /* 避免回调自身异常影响主流程 */ }
|
||||
}
|
||||
}
|
||||
/** 调试日志 */
|
||||
_debug(msg, ...args) {
|
||||
if (this.debug) {
|
||||
console.debug(`[MetonaSqlark:${this.name}] ${msg}`, ...args);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user