release: v0.2.1 生产加固 — WAL CRC/约束激活/Hybrid提交顺序/RESTRICT外键/浏览器兼容表/debug模式/onError回调
CI / test (20.x) (push) Successful in 9m58s
CI / test (18.x) (push) Successful in 10m0s
CI / test (22.x) (push) Successful in 10m0s
CI / test (24.x) (push) Successful in 9m56s

This commit is contained in:
thzxx
2026-07-27 20:47:30 +08:00
parent 620cd11521
commit e6efa39d48
18 changed files with 357 additions and 27 deletions
+59 -4
View File
@@ -15,6 +15,8 @@ const DB_DEFAULTS = Object.freeze({
mode: 'hybrid',
diskEngine: 'indexeddb',
version: 1,
maxRowsPerQuery: 0, // 0 = 不限制
debug: false,
});
// ---------------------------------------------------------------------------
// 错误类型
@@ -497,7 +499,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)
@@ -512,6 +514,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) {
@@ -1800,8 +1806,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);
@@ -2532,6 +2542,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);
@@ -2540,14 +2551,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));
@@ -2557,8 +2574,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,
@@ -2566,7 +2594,7 @@ class WAL {
tableName,
key,
data: recordData,
checksum: 0,
checksum: storedCrc,
});
}
catch {
@@ -3478,8 +3506,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();
@@ -5412,6 +5448,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();
@@ -5595,6 +5635,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);
}
}
}
/**