release: v0.2.1 生产加固 — WAL CRC/约束激活/Hybrid提交顺序/RESTRICT外键/浏览器兼容表/debug模式/onError回调
This commit is contained in:
Vendored
+59
-4
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+12
@@ -58,6 +58,10 @@ interface DatabaseConfig {
|
||||
onReady?: (db: unknown) => void;
|
||||
/** 错误回调 */
|
||||
onError?: (error: Error) => void;
|
||||
/** 查询结果行数上限(默认 10000,0 表示不限制) */
|
||||
maxRowsPerQuery?: number;
|
||||
/** 调试模式(启用后输出详细操作日志) */
|
||||
debug?: boolean;
|
||||
}
|
||||
/** Where 条件操作符 */
|
||||
type WhereOperator = '$eq' | '$ne' | '$gt' | '$gte' | '$lt' | '$lte' | '$in' | '$nin' | '$like' | '$and' | '$or' | '$not';
|
||||
@@ -434,6 +438,10 @@ declare class MetonaSqlark {
|
||||
private config;
|
||||
private ready;
|
||||
private tableCache;
|
||||
/** 查询结果行数上限 */
|
||||
get maxRowsPerQuery(): number;
|
||||
/** 调试模式 */
|
||||
get debug(): boolean;
|
||||
constructor(config: DatabaseConfig);
|
||||
/** 初始化数据库(创建引擎、打开连接) */
|
||||
init(): Promise<void>;
|
||||
@@ -483,6 +491,10 @@ declare class MetonaSqlark {
|
||||
getEngine(): IStorageEngine;
|
||||
private createEngine;
|
||||
private ensureReady;
|
||||
/** 错误回调分发 */
|
||||
private _onError;
|
||||
/** 调试日志 */
|
||||
private _debug;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+59
-4
@@ -17,6 +17,8 @@
|
||||
mode: 'hybrid',
|
||||
diskEngine: 'indexeddb',
|
||||
version: 1,
|
||||
maxRowsPerQuery: 0, // 0 = 不限制
|
||||
debug: false,
|
||||
});
|
||||
// ---------------------------------------------------------------------------
|
||||
// 错误类型
|
||||
@@ -499,7 +501,7 @@
|
||||
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)
|
||||
@@ -514,6 +516,10 @@
|
||||
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) {
|
||||
@@ -1802,8 +1808,12 @@
|
||||
}
|
||||
/** 范围扫描 */
|
||||
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);
|
||||
@@ -2534,6 +2544,7 @@
|
||||
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);
|
||||
@@ -2542,14 +2553,20 @@
|
||||
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));
|
||||
@@ -2559,8 +2576,19 @@
|
||||
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,
|
||||
@@ -2568,7 +2596,7 @@
|
||||
tableName,
|
||||
key,
|
||||
data: recordData,
|
||||
checksum: 0,
|
||||
checksum: storedCrc,
|
||||
});
|
||||
}
|
||||
catch {
|
||||
@@ -3480,8 +3508,16 @@
|
||||
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();
|
||||
@@ -5414,6 +5450,10 @@
|
||||
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();
|
||||
@@ -5597,6 +5637,21 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
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