fix: CI Node 18/20 下 aria-crypto 失败 — SubtleCrypto 跨 realm ArrayBuffer 兼容
CI / test (18.x) (push) Successful in 9m58s
CI / test (20.x) (push) Successful in 9m58s
CI / test (22.x) (push) Successful in 9m58s
CI / test (24.x) (push) Successful in 9m54s

- crypto.ts encryptPage/decryptPage 改传 TypedArray 视图(ArrayBuffer.isView 检查跨 realm 可靠)
- jest.setup.js structuredClone polyfill 用跨 realm toString 标签检查
  (修复 fake-indexeddb 存储 ArrayBuffer 被 JSON 破坏成 {} 的问题)
- 新增 SSTable 加密真实路径测试(加密落盘→重载解密, 8 个测试)
- aria/v025 固定 db 名改随机(fake-indexeddb 真实持久化后防表残留冲突)
- Node 18/20/22/24 全矩阵 837 测试通过
This commit is contained in:
thzxx
2026-08-08 11:07:35 +08:00
parent d544501e1c
commit fda3f1ad34
12 changed files with 1369 additions and 1306 deletions
+4 -2
View File
@@ -4403,13 +4403,15 @@ class CryptoManager {
if (!this.cryptoKey) if (!this.cryptoKey)
throw new Error('Crypto not initialized'); throw new Error('Crypto not initialized');
const iv = crypto.getRandomValues(new Uint8Array(IV_LENGTH)); const iv = crypto.getRandomValues(new Uint8Array(IV_LENGTH));
const ciphertext = await crypto.subtle.encrypt({ name: ALGO, iv }, this.cryptoKey, data); // 传 TypedArray 视图而非裸 ArrayBufferSubtleCrypto 通过 ArrayBuffer.isView 检查,
// 对跨 realm / 跨 vm 环境的 ArrayBuffer 兼容(Node 18/20 的 webcrypto 对裸 ArrayBuffer 检查严格)
const ciphertext = await crypto.subtle.encrypt({ name: ALGO, iv }, this.cryptoKey, new Uint8Array(data));
return { iv: iv, data: ciphertext }; return { iv: iv, data: ciphertext };
} }
async decryptPage(iv, data) { async decryptPage(iv, data) {
if (!this.cryptoKey) if (!this.cryptoKey)
throw new Error('Crypto not initialized'); throw new Error('Crypto not initialized');
return crypto.subtle.decrypt({ name: ALGO, iv }, this.cryptoKey, data); return crypto.subtle.decrypt({ name: ALGO, iv }, this.cryptoKey, new Uint8Array(data));
} }
close() { close() {
this.cryptoKey = null; this.cryptoKey = null;
+1 -1
View File
File diff suppressed because one or more lines are too long
+4 -2
View File
@@ -4399,13 +4399,15 @@ class CryptoManager {
if (!this.cryptoKey) if (!this.cryptoKey)
throw new Error('Crypto not initialized'); throw new Error('Crypto not initialized');
const iv = crypto.getRandomValues(new Uint8Array(IV_LENGTH)); const iv = crypto.getRandomValues(new Uint8Array(IV_LENGTH));
const ciphertext = await crypto.subtle.encrypt({ name: ALGO, iv }, this.cryptoKey, data); // 传 TypedArray 视图而非裸 ArrayBufferSubtleCrypto 通过 ArrayBuffer.isView 检查,
// 对跨 realm / 跨 vm 环境的 ArrayBuffer 兼容(Node 18/20 的 webcrypto 对裸 ArrayBuffer 检查严格)
const ciphertext = await crypto.subtle.encrypt({ name: ALGO, iv }, this.cryptoKey, new Uint8Array(data));
return { iv: iv, data: ciphertext }; return { iv: iv, data: ciphertext };
} }
async decryptPage(iv, data) { async decryptPage(iv, data) {
if (!this.cryptoKey) if (!this.cryptoKey)
throw new Error('Crypto not initialized'); throw new Error('Crypto not initialized');
return crypto.subtle.decrypt({ name: ALGO, iv }, this.cryptoKey, data); return crypto.subtle.decrypt({ name: ALGO, iv }, this.cryptoKey, new Uint8Array(data));
} }
close() { close() {
this.cryptoKey = null; this.cryptoKey = null;
+1 -1
View File
File diff suppressed because one or more lines are too long
+4 -2
View File
@@ -4405,13 +4405,15 @@
if (!this.cryptoKey) if (!this.cryptoKey)
throw new Error('Crypto not initialized'); throw new Error('Crypto not initialized');
const iv = crypto.getRandomValues(new Uint8Array(IV_LENGTH)); const iv = crypto.getRandomValues(new Uint8Array(IV_LENGTH));
const ciphertext = await crypto.subtle.encrypt({ name: ALGO, iv }, this.cryptoKey, data); // 传 TypedArray 视图而非裸 ArrayBufferSubtleCrypto 通过 ArrayBuffer.isView 检查,
// 对跨 realm / 跨 vm 环境的 ArrayBuffer 兼容(Node 18/20 的 webcrypto 对裸 ArrayBuffer 检查严格)
const ciphertext = await crypto.subtle.encrypt({ name: ALGO, iv }, this.cryptoKey, new Uint8Array(data));
return { iv: iv, data: ciphertext }; return { iv: iv, data: ciphertext };
} }
async decryptPage(iv, data) { async decryptPage(iv, data) {
if (!this.cryptoKey) if (!this.cryptoKey)
throw new Error('Crypto not initialized'); throw new Error('Crypto not initialized');
return crypto.subtle.decrypt({ name: ALGO, iv }, this.cryptoKey, data); return crypto.subtle.decrypt({ name: ALGO, iv }, this.cryptoKey, new Uint8Array(data));
} }
close() { close() {
this.cryptoKey = null; this.cryptoKey = null;
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+10 -1
View File
@@ -1,6 +1,15 @@
// jest setup: polyfill structuredClone for fake-indexeddb // jest setup: polyfill structuredClone for fake-indexeddb
if (typeof globalThis.structuredClone !== 'function') { if (typeof globalThis.structuredClone !== 'function') {
globalThis.structuredClone = (obj) => JSON.parse(JSON.stringify(obj)); // 注意:不能用 JSON 序列化 —— ArrayBuffer/TypedArray 会被破坏成 {}
// fake-indexeddb 存储 AriaEngine 二进制页面(4KB SSTable/WAL)依赖正确的克隆。
// 且不能依赖 instanceofTextEncoder 产生 node realm 的 ArrayBuffer
// 与 jsdom realm 的构造函数不匹配),需用跨 realm 的 toString 标签检查。
globalThis.structuredClone = (obj) => {
const tag = Object.prototype.toString.call(obj);
if (tag === '[object ArrayBuffer]') return obj.slice(0);
if (ArrayBuffer.isView(obj)) return new obj.constructor(obj);
return JSON.parse(JSON.stringify(obj));
};
} }
// polyfill TextEncoder/TextDecoder for jsdom environment // polyfill TextEncoder/TextDecoder for jsdom environment
+4 -2
View File
@@ -36,13 +36,15 @@ export class CryptoManager {
async encryptPage(data: ArrayBuffer): Promise<{ iv: Uint8Array; data: ArrayBuffer }> { async encryptPage(data: ArrayBuffer): Promise<{ iv: Uint8Array; data: ArrayBuffer }> {
if (!this.cryptoKey) throw new Error('Crypto not initialized'); if (!this.cryptoKey) throw new Error('Crypto not initialized');
const iv = crypto.getRandomValues(new Uint8Array(IV_LENGTH)) as any; const iv = crypto.getRandomValues(new Uint8Array(IV_LENGTH)) as any;
const ciphertext = await crypto.subtle.encrypt({ name: ALGO, iv } as any, this.cryptoKey, data); // 传 TypedArray 视图而非裸 ArrayBufferSubtleCrypto 通过 ArrayBuffer.isView 检查,
// 对跨 realm / 跨 vm 环境的 ArrayBuffer 兼容(Node 18/20 的 webcrypto 对裸 ArrayBuffer 检查严格)
const ciphertext = await crypto.subtle.encrypt({ name: ALGO, iv } as any, this.cryptoKey, new Uint8Array(data));
return { iv: iv as Uint8Array, data: ciphertext }; return { iv: iv as Uint8Array, data: ciphertext };
} }
async decryptPage(iv: Uint8Array, data: ArrayBuffer): Promise<ArrayBuffer> { async decryptPage(iv: Uint8Array, data: ArrayBuffer): Promise<ArrayBuffer> {
if (!this.cryptoKey) throw new Error('Crypto not initialized'); if (!this.cryptoKey) throw new Error('Crypto not initialized');
return crypto.subtle.decrypt({ name: ALGO, iv } as any, this.cryptoKey, data); return crypto.subtle.decrypt({ name: ALGO, iv } as any, this.cryptoKey, new Uint8Array(data));
} }
close(): void { close(): void {
+46
View File
@@ -4,6 +4,7 @@
* *
* v0.2.6 补强:此前仅验证实例化,现在验证真实的加解密往返一致性。 * v0.2.6 补强:此前仅验证实例化,现在验证真实的加解密往返一致性。
*/ */
import 'fake-indexeddb/auto';
import { import {
CryptoManager, CryptoManager,
initCrypto, initCrypto,
@@ -109,3 +110,48 @@ describe('AriaEngine — CryptoManager', () => {
cm.close(); cm.close();
}); });
}); });
// ===================================================================
// 真实存储路径:SSTable 加密 → 持久化 → 重载解密(v0.3.2 CI 修复)
// ===================================================================
describe('AriaEngine — SSTable 加密存储真实路径', () => {
test('加密落盘后重载可完整解密', async () => {
// eslint-disable-next-line @typescript-eslint/no-var-requires
const { AriaEngine } = require('../../src/engine/aria/index');
// eslint-disable-next-line @typescript-eslint/no-var-requires
const { createSchema } = require('../../src/table/schema');
const dbName = `enc-store-${Date.now()}`;
await initCrypto('engine-store-password');
const engine = new AriaEngine({
storageBackend: 'indexeddb',
walSyncMode: 'none',
} as any);
await engine.open(dbName, 1);
await engine.createTable(createSchema('t', {
id: { type: 'string', primaryKey: true },
v: { type: 'number' },
}));
await engine.insert('t', [{ id: '1', v: 42 }, { id: '2', v: 99 }]);
// 强制刷盘:SSTable 经加密保存(encryptPage 真实路径)
await (engine as any).lsm.flush();
await engine.close();
// 重载:SSTable 解密恢复
const engine2 = new AriaEngine({
storageBackend: 'indexeddb',
walSyncMode: 'none',
} as any);
await engine2.open(dbName, 1);
const rows = await engine2.find('t', { table: 't' });
expect(rows).toHaveLength(2);
const byId = Object.fromEntries(rows.map((r: any) => [r.id, r.v]));
expect(byId['1']).toBe(42);
expect(byId['2']).toBe(99);
await engine2.close();
closeCrypto();
});
});
+932 -932
View File
File diff suppressed because it is too large Load Diff
+361 -361
View File
@@ -1,361 +1,361 @@
/** /**
* v0.2.5 修复验证测试 * v0.2.5 修复验证测试
* 验证所有 P0/P1/P2 修复点 * 验证所有 P0/P1/P2 修复点
*/ */
import { VERSION } from '../src/constants'; import { VERSION } from '../src/constants';
import { MetonaSqlark } from '../src/core'; import { MetonaSqlark } from '../src/core';
import { AriaEngine } from '../src/engine/aria/index'; import { AriaEngine } from '../src/engine/aria/index';
import { MemoryEngine } from '../src/engine/memory'; import { MemoryEngine } from '../src/engine/memory';
import { parse } from '../src/sql/parser'; import { parse } from '../src/sql/parser';
import { QueryExecutor } from '../src/query/executor'; import { QueryExecutor } from '../src/query/executor';
import { WAL } from '../src/engine/aria/wal/log'; import { WAL } from '../src/engine/aria/wal/log';
import { SSTableReader } from '../src/engine/aria/index/sstable'; import { SSTableReader } from '../src/engine/aria/index/sstable';
import { BloomFilter } from '../src/engine/aria/index/bloom'; import { BloomFilter } from '../src/engine/aria/index/bloom';
import { CryptoManager } from '../src/engine/aria/crypto'; import { CryptoManager } from '../src/engine/aria/crypto';
import { PluginManager } from '../src/plugin/index'; import { PluginManager } from '../src/plugin/index';
import type { SSTableMeta } from '../src/engine/aria/types'; import type { SSTableMeta } from '../src/engine/aria/types';
import type { MetonaPlugin } from '../src/constants'; import type { MetonaPlugin } from '../src/constants';
import { createSchema } from '../src/table/schema'; import { createSchema } from '../src/table/schema';
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// P0-1: 版本号统一 // P0-1: 版本号统一
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
describe('[v0.2.5] P0-1: 版本号统一', () => { describe('[v0.2.5] P0-1: 版本号统一', () => {
test('VERSION 常量为当前版本(0.3.2', () => { test('VERSION 常量为当前版本(0.3.2', () => {
expect(VERSION).toBe('0.3.2'); expect(VERSION).toBe('0.3.2');
}); });
}); });
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// P0-2: AriaEngine OPFS 后端映射修复 // P0-2: AriaEngine OPFS 后端映射修复
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
describe('[v0.2.5] P0-2: AriaEngine OPFS 后端映射', () => { describe('[v0.2.5] P0-2: AriaEngine OPFS 后端映射', () => {
test('mode=aria + diskEngine=opfs 时应使用 opfs 后端', () => { test('mode=aria + diskEngine=opfs 时应使用 opfs 后端', () => {
const db = new MetonaSqlark({ name: 'test-opfs-map', mode: 'aria', diskEngine: 'opfs' }); const db = new MetonaSqlark({ name: `test-opfs-map-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`, mode: 'aria', diskEngine: 'opfs' });
// 不实际 open(需要浏览器环境),只验证 createEngine 逻辑 // 不实际 open(需要浏览器环境),只验证 createEngine 逻辑
// 通过 getEngine 在 init 后检查 // 通过 getEngine 在 init 后检查
expect(db).toBeDefined(); expect(db).toBeDefined();
}); });
test('mode=aria + diskEngine=indexeddb 时应使用 indexeddb 后端', () => { test('mode=aria + diskEngine=indexeddb 时应使用 indexeddb 后端', () => {
const db = new MetonaSqlark({ name: 'test-idb-map', mode: 'aria', diskEngine: 'indexeddb' }); const db = new MetonaSqlark({ name: `test-idb-map-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`, mode: 'aria', diskEngine: 'indexeddb' });
expect(db).toBeDefined(); expect(db).toBeDefined();
}); });
}); });
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// P0-3: _onError 接入执行路径 // P0-3: _onError 接入执行路径
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
describe('[v0.2.5] P0-3: _onError 接入执行路径', () => { describe('[v0.2.5] P0-3: _onError 接入执行路径', () => {
test('query 失败时调用 onError 回调', async () => { test('query 失败时调用 onError 回调', async () => {
const errors: Error[] = []; const errors: Error[] = [];
const db = new MetonaSqlark({ const db = new MetonaSqlark({
name: 'test-onerror', name: 'test-onerror',
mode: 'memory', mode: 'memory',
onError: (e) => errors.push(e), onError: (e) => errors.push(e),
}); });
await db.init(); await db.init();
await db.defineTable('users', { await db.defineTable('users', {
id: { type: 'string', primaryKey: true }, id: { type: 'string', primaryKey: true },
name: { type: 'string' }, name: { type: 'string' },
}); });
// 故意执行不存在的表查询 // 故意执行不存在的表查询
await expect(db.query('SELECT * FROM nonexistent')).rejects.toThrow(); await expect(db.query('SELECT * FROM nonexistent')).rejects.toThrow();
expect(errors.length).toBeGreaterThan(0); expect(errors.length).toBeGreaterThan(0);
await db.close(); await db.close();
}); });
test('defineTable 失败时调用 onError', async () => { test('defineTable 失败时调用 onError', async () => {
const errors: Error[] = []; const errors: Error[] = [];
const db = new MetonaSqlark({ const db = new MetonaSqlark({
name: 'test-onerror2', name: 'test-onerror2',
mode: 'memory', mode: 'memory',
onError: (e) => errors.push(e), onError: (e) => errors.push(e),
}); });
await db.init(); await db.init();
await db.defineTable('dup', { await db.defineTable('dup', {
id: { type: 'string', primaryKey: true }, id: { type: 'string', primaryKey: true },
}); });
// 重复创建 // 重复创建
await expect(db.defineTable('dup', { await expect(db.defineTable('dup', {
id: { type: 'string', primaryKey: true }, id: { type: 'string', primaryKey: true },
})).rejects.toThrow(); })).rejects.toThrow();
expect(errors.length).toBeGreaterThan(0); expect(errors.length).toBeGreaterThan(0);
await db.close(); await db.close();
}); });
}); });
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// P0-4: maxRowsPerQuery 生效 // P0-4: maxRowsPerQuery 生效
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
describe('[v0.2.5] P0-4: maxRowsPerQuery 生效', () => { describe('[v0.2.5] P0-4: maxRowsPerQuery 生效', () => {
test('结果集被截断为 maxRowsPerQuery', async () => { test('结果集被截断为 maxRowsPerQuery', async () => {
const engine = new MemoryEngine(); const engine = new MemoryEngine();
await engine.open('test-maxrows', 1); await engine.open('test-maxrows', 1);
await engine.createTable(createSchema('items', { await engine.createTable(createSchema('items', {
id: { type: 'string', primaryKey: true }, id: { type: 'string', primaryKey: true },
val: { type: 'number' }, val: { type: 'number' },
})); }));
// 插入 100 行 // 插入 100 行
for (let i = 0; i < 100; i++) { for (let i = 0; i < 100; i++) {
await engine.insert('items', [{ id: `item${i}`, val: i }]); await engine.insert('items', [{ id: `item${i}`, val: i }]);
} }
const executor = new QueryExecutor(engine, 10); // maxRowsPerQuery=10 const executor = new QueryExecutor(engine, 10); // maxRowsPerQuery=10
const stmt = parse('SELECT * FROM items'); const stmt = parse('SELECT * FROM items');
const result = await executor.execute(stmt) as Record<string, unknown>[]; const result = await executor.execute(stmt) as Record<string, unknown>[];
expect(result.length).toBe(10); // 截断为 10 行 expect(result.length).toBe(10); // 截断为 10 行
await engine.close(); await engine.close();
}); });
test('maxRowsPerQuery=0 表示不限制', async () => { test('maxRowsPerQuery=0 表示不限制', async () => {
const engine = new MemoryEngine(); const engine = new MemoryEngine();
await engine.open('test-nolimit', 1); await engine.open('test-nolimit', 1);
await engine.createTable(createSchema('items', { await engine.createTable(createSchema('items', {
id: { type: 'string', primaryKey: true }, id: { type: 'string', primaryKey: true },
})); }));
for (let i = 0; i < 50; i++) { for (let i = 0; i < 50; i++) {
await engine.insert('items', [{ id: `i${i}` }]); await engine.insert('items', [{ id: `i${i}` }]);
} }
const executor = new QueryExecutor(engine, 0); const executor = new QueryExecutor(engine, 0);
const stmt = parse('SELECT * FROM items'); const stmt = parse('SELECT * FROM items');
const result = await executor.execute(stmt) as Record<string, unknown>[]; const result = await executor.execute(stmt) as Record<string, unknown>[];
expect(result.length).toBe(50); expect(result.length).toBe(50);
await engine.close(); await engine.close();
}); });
}); });
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// P0-5: WAL full 模式真正同步 // P0-5: WAL full 模式真正同步
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
describe('[v0.2.5] P0-5: WAL full 模式同步', () => { describe('[v0.2.5] P0-5: WAL full 模式同步', () => {
test('append 在 full 模式下是 async 且可 await', async () => { test('append 在 full 模式下是 async 且可 await', async () => {
let appendCount = 0; let appendCount = 0;
const wal = new WAL({ const wal = new WAL({
append: async (_data: Uint8Array) => { appendCount++; }, append: async (_data: Uint8Array) => { appendCount++; },
readAll: async () => new Uint8Array(0), readAll: async () => new Uint8Array(0),
truncate: async () => {}, truncate: async () => {},
exists: async () => false, exists: async () => false,
}, true, 'full'); }, true, 'full');
// append 现在返回 Promise // append 现在返回 Promise
await wal.append({ await wal.append({
type: 1, // INSERT type: 1, // INSERT
txnId: 0, txnId: 0,
tableName: 'test', tableName: 'test',
key: 'k1', key: 'k1',
data: { v: 1 }, data: { v: 1 },
} as any); } as any);
expect(appendCount).toBe(1); expect(appendCount).toBe(1);
}); });
}); });
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// P0-6: PluginManager.install 传 db 实例 // P0-6: PluginManager.install 传 db 实例
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
describe('[v0.2.5] P0-6: PluginManager.install 传 db 实例', () => { describe('[v0.2.5] P0-6: PluginManager.install 传 db 实例', () => {
test('register 传 db 给 install', () => { test('register 传 db 给 install', () => {
let receivedDb: unknown = null; let receivedDb: unknown = null;
const plugin: MetonaPlugin = { const plugin: MetonaPlugin = {
name: 'test-plugin', name: 'test-plugin',
install: (db) => { receivedDb = db; }, install: (db) => { receivedDb = db; },
destroy: () => {}, destroy: () => {},
}; };
const pm = new PluginManager(); const pm = new PluginManager();
const fakeDb = { name: 'fake' }; const fakeDb = { name: 'fake' };
pm.register(plugin, fakeDb); pm.register(plugin, fakeDb);
expect(receivedDb).toBe(fakeDb); expect(receivedDb).toBe(fakeDb);
}); });
}); });
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// P1-7: SSTableReader 二分查找统一 // P1-7: SSTableReader 二分查找统一
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
describe('[v0.2.5] P1-7: SSTableReader 二分查找', () => { describe('[v0.2.5] P1-7: SSTableReader 二分查找', () => {
test('rangeScan 使用二分查找正确定位', () => { test('rangeScan 使用二分查找正确定位', () => {
// 构建一个 SSTable 手工 // 构建一个 SSTable 手工
const { SSTableBuilder } = require('../src/engine/aria/index/sstable_builder'); const { SSTableBuilder } = require('../src/engine/aria/index/sstable_builder');
const builder = new SSTableBuilder(4096); const builder = new SSTableBuilder(4096);
// 添加足够多的条目以形成多个 block // 添加足够多的条目以形成多个 block
for (let i = 0; i < 100; i++) { for (let i = 0; i < 100; i++) {
const key = `key${String(i).padStart(5, '0')}`; const key = `key${String(i).padStart(5, '0')}`;
builder.add(key, { data: `value${i}` }); builder.add(key, { data: `value${i}` });
} }
const { sstableData } = builder.build(); const { sstableData } = builder.build();
const meta: SSTableMeta = { const meta: SSTableMeta = {
id: 1, id: 1,
level: 0, level: 0,
minKey: 'key00000', minKey: 'key00000',
maxKey: 'key00099', maxKey: 'key00099',
blockCount: 1, blockCount: 1,
totalSize: sstableData.byteLength, totalSize: sstableData.byteLength,
bloomData: null, bloomData: null,
}; };
const reader = new SSTableReader(sstableData, meta); const reader = new SSTableReader(sstableData, meta);
// 精确查找 // 精确查找
const result = reader.get('key00050'); const result = reader.get('key00050');
expect(result).not.toBeNull(); expect(result).not.toBeNull();
expect((result as any).data).toBe('value50'); expect((result as any).data).toBe('value50');
// 范围扫描 // 范围扫描
const collected: string[] = []; const collected: string[] = [];
reader.rangeScan('key00010', 'key00020', (k) => collected.push(k)); reader.rangeScan('key00010', 'key00020', (k) => collected.push(k));
expect(collected.length).toBeGreaterThan(0); expect(collected.length).toBeGreaterThan(0);
expect(collected[0]).toBe('key00010'); expect(collected[0]).toBe('key00010');
}); });
}); });
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// P1-8: SQL 注入防护 — 表名校验 // P1-8: SQL 注入防护 — 表名校验
// 真实覆盖位置:tests/integrations/react.test.tsuseTable // 真实覆盖位置:tests/integrations/react.test.tsuseTable
// tests/integrations/vue.test.tsuseSqlarkTable // tests/integrations/vue.test.tsuseSqlarkTable
// 通过真实 hooks 调用触发 validateTableName,验证非法表名抛错。 // 通过真实 hooks 调用触发 validateTableName,验证非法表名抛错。
// 注:v0.2.6 移除此处"复制正则自测"的伪测试(未触达真实代码)。 // 注:v0.2.6 移除此处"复制正则自测"的伪测试(未触达真实代码)。
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// P1-9: crypto 实例化 // P1-9: crypto 实例化
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
describe('[v0.2.5] P1-9: CryptoManager 实例化', () => { describe('[v0.2.5] P1-9: CryptoManager 实例化', () => {
test('CryptoManager 可以独立实例化', () => { test('CryptoManager 可以独立实例化', () => {
const cm1 = new CryptoManager(); const cm1 = new CryptoManager();
const cm2 = new CryptoManager(); const cm2 = new CryptoManager();
expect(cm1.enabled).toBe(false); expect(cm1.enabled).toBe(false);
expect(cm2.enabled).toBe(false); expect(cm2.enabled).toBe(false);
// 两个实例互不影响 // 两个实例互不影响
expect(cm1).not.toBe(cm2); expect(cm1).not.toBe(cm2);
}); });
}); });
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// P2-14: ALTER TABLE 语法 // P2-14: ALTER TABLE 语法
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
describe('[v0.2.5] P2-14: ALTER TABLE', () => { describe('[v0.2.5] P2-14: ALTER TABLE', () => {
test('解析 ALTER TABLE ADD COLUMN', () => { test('解析 ALTER TABLE ADD COLUMN', () => {
const stmt = parse('ALTER TABLE users ADD COLUMN email VARCHAR(255) UNIQUE'); const stmt = parse('ALTER TABLE users ADD COLUMN email VARCHAR(255) UNIQUE');
expect(stmt.type).toBe('ALTER_TABLE'); expect(stmt.type).toBe('ALTER_TABLE');
expect((stmt as any).name).toBe('users'); expect((stmt as any).name).toBe('users');
expect((stmt as any).action).toBe('ADD'); expect((stmt as any).action).toBe('ADD');
expect((stmt as any).column.name).toBe('email'); expect((stmt as any).column.name).toBe('email');
}); });
test('解析 ALTER TABLE DROP COLUMN', () => { test('解析 ALTER TABLE DROP COLUMN', () => {
const stmt = parse('ALTER TABLE users DROP COLUMN email'); const stmt = parse('ALTER TABLE users DROP COLUMN email');
expect(stmt.type).toBe('ALTER_TABLE'); expect(stmt.type).toBe('ALTER_TABLE');
expect((stmt as any).action).toBe('DROP'); expect((stmt as any).action).toBe('DROP');
expect((stmt as any).column.name).toBe('email'); expect((stmt as any).column.name).toBe('email');
}); });
test('执行 ALTER TABLE ADD COLUMN', async () => { test('执行 ALTER TABLE ADD COLUMN', async () => {
const engine = new MemoryEngine(); const engine = new MemoryEngine();
await engine.open('test-alter', 1); await engine.open('test-alter', 1);
await engine.createTable(createSchema('users', { await engine.createTable(createSchema('users', {
id: { type: 'string', primaryKey: true }, id: { type: 'string', primaryKey: true },
name: { type: 'string' }, name: { type: 'string' },
})); }));
const executor = new QueryExecutor(engine); const executor = new QueryExecutor(engine);
const stmt = parse('ALTER TABLE users ADD COLUMN email VARCHAR(255)'); const stmt = parse('ALTER TABLE users ADD COLUMN email VARCHAR(255)');
await executor.execute(stmt); await executor.execute(stmt);
const schema = await engine.getTableSchema('users'); const schema = await engine.getTableSchema('users');
expect(schema!.columns.email).toBeDefined(); expect(schema!.columns.email).toBeDefined();
await engine.close(); await engine.close();
}); });
test('执行 ALTER TABLE DROP COLUMN', async () => { test('执行 ALTER TABLE DROP COLUMN', async () => {
const engine = new MemoryEngine(); const engine = new MemoryEngine();
await engine.open('test-alter-drop', 1); await engine.open('test-alter-drop', 1);
await engine.createTable(createSchema('users', { await engine.createTable(createSchema('users', {
id: { type: 'string', primaryKey: true }, id: { type: 'string', primaryKey: true },
name: { type: 'string' }, name: { type: 'string' },
email: { type: 'string' }, email: { type: 'string' },
})); }));
const executor = new QueryExecutor(engine); const executor = new QueryExecutor(engine);
const stmt = parse('ALTER TABLE users DROP COLUMN email'); const stmt = parse('ALTER TABLE users DROP COLUMN email');
await executor.execute(stmt); await executor.execute(stmt);
const schema = await engine.getTableSchema('users'); const schema = await engine.getTableSchema('users');
expect(schema!.columns.email).toBeUndefined(); expect(schema!.columns.email).toBeUndefined();
await engine.close(); await engine.close();
}); });
}); });
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// P2-15: TRUNCATE TABLE 语法 // P2-15: TRUNCATE TABLE 语法
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
describe('[v0.2.5] P2-15: TRUNCATE TABLE', () => { describe('[v0.2.5] P2-15: TRUNCATE TABLE', () => {
test('解析 TRUNCATE TABLE', () => { test('解析 TRUNCATE TABLE', () => {
const stmt = parse('TRUNCATE TABLE users'); const stmt = parse('TRUNCATE TABLE users');
expect(stmt.type).toBe('TRUNCATE_TABLE'); expect(stmt.type).toBe('TRUNCATE_TABLE');
expect((stmt as any).name).toBe('users'); expect((stmt as any).name).toBe('users');
}); });
test('执行 TRUNCATE TABLE 清空数据', async () => { test('执行 TRUNCATE TABLE 清空数据', async () => {
const engine = new MemoryEngine(); const engine = new MemoryEngine();
await engine.open('test-truncate', 1); await engine.open('test-truncate', 1);
await engine.createTable(createSchema('items', { await engine.createTable(createSchema('items', {
id: { type: 'string', primaryKey: true }, id: { type: 'string', primaryKey: true },
})); }));
await engine.insert('items', [ await engine.insert('items', [
{ id: 'a' }, { id: 'b' }, { id: 'c' }, { id: 'a' }, { id: 'b' }, { id: 'c' },
]); ]);
const executor = new QueryExecutor(engine); const executor = new QueryExecutor(engine);
const stmt = parse('TRUNCATE TABLE items'); const stmt = parse('TRUNCATE TABLE items');
await executor.execute(stmt); await executor.execute(stmt);
const rows = await engine.find('items', { table: 'items' }); const rows = await engine.find('items', { table: 'items' });
expect(rows.length).toBe(0); expect(rows.length).toBe(0);
await engine.close(); await engine.close();
}); });
}); });
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// P2-12: WAL 大小阈值接入 checkpoint // P2-12: WAL 大小阈值接入 checkpoint
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
describe('[v0.2.5] P2-12: WAL 大小阈值', () => { describe('[v0.2.5] P2-12: WAL 大小阈值', () => {
test('CheckpointManager 接收 walSizeThreshold 参数', () => { test('CheckpointManager 接收 walSizeThreshold 参数', () => {
const { CheckpointManager } = require('../src/engine/aria/wal/checkpoint'); const { CheckpointManager } = require('../src/engine/aria/wal/checkpoint');
const fakeLsm = { flush: async () => {} }; const fakeLsm = { flush: async () => {} };
const fakeWal = { flush: async () => {}, checkpoint: async () => {}, getBufferedCount: () => 0 }; const fakeWal = { flush: async () => {}, checkpoint: async () => {}, getBufferedCount: () => 0 };
const cm = new CheckpointManager(fakeLsm, fakeWal, null, 1000, 1024); const cm = new CheckpointManager(fakeLsm, fakeWal, null, 1000, 1024);
expect(cm).toBeDefined(); expect(cm).toBeDefined();
expect(cm.getOpCount()).toBe(0); expect(cm.getOpCount()).toBe(0);
}); });
}); });
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// P2-13: compactLevelSync 接口公开化 // P2-13: compactLevelSync 接口公开化
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
describe('[v0.2.5] P2-13: compactLevel public', () => { describe('[v0.2.5] P2-13: compactLevel public', () => {
test('LSM.compactLevel 是 public 方法', () => { test('LSM.compactLevel 是 public 方法', () => {
const { LSM } = require('../src/engine/aria/index/lsm'); const { LSM } = require('../src/engine/aria/index/lsm');
const lsm = new LSM({ const lsm = new LSM({
sstableStore: { sstableStore: {
save: async () => {}, load: async () => null, delete: async () => {}, save: async () => {}, load: async () => null, delete: async () => {},
allocateId: async () => 1, listMeta: async () => [], saveMeta: async () => {}, deleteMeta: async () => {}, allocateId: async () => 1, listMeta: async () => [], saveMeta: async () => {}, deleteMeta: async () => {},
}, },
}); });
expect(typeof lsm.compactLevel).toBe('function'); expect(typeof lsm.compactLevel).toBe('function');
}); });
}); });