docs(G6): 宣称与实现一致性收口 —— priority 真正生效、MVCC/backup/引擎数/打包 全部对齐

独立核验(12 条宣称逐条对源码验证)发现 5 处**硬伤**与 2 处**数字过期**,
本提交按"能改代码就让宣称成立、改不动就如实描述"的原则全部收口。

让实现符合文档(2 处):
1. **插件 priority 此前不生效** — `register()` 虽按 priority 插入数组,但
   `install()` 在 register 内**立即**执行,因此 install 与钩子顺序 = config 数组
   顺序(实测 priority low=1/high=100/mid=50 时钩子按 low→high→mid 触发,
   只有 `getPlugins()` 是 high,mid,low)。而 README/CONTRIBUTING/constants
   一直宣称"越大越先执行"。
   现在 Core 注册前按 priority **稳定降序**排序(同优先级保持数组顺序),
   install 与钩子都按优先级执行 → 宣称成立。新增
   `tests/v080-plugin-priority.test.ts` 锁定 install 顺序、钩子顺序、稳定性、缺省值。
2. **连接池静态方法不在类型系统里** — `MetonaSqlark.connect/disconnect/
   disconnectAll/getActiveConnections` 由 connection-manager 用
   `as unknown as Record<string, unknown>` 注入,README 的连接池表格在
   TypeScript 下全部 TS2339。现在在类上声明为可选静态成员,注入处去掉断言。

如实描述(3 处):
3. **MVCC 快照隔离**(README 三处 + 实现对照)— `snapshotLsn` / `prevVersion`
   只写不读,事务读走 `txnSnapshot`+LSM,commit 即清理版本链,并发
   `beginTransaction` 抛 `TX_ACTIVE`。改为"快照回滚(事务串行,非 MVCC 隔离)",
   并在 README 架构图与维护语句表里同步措辞。
4. **"存储引擎(5 种)"** — 实际是 4 种模式 + 3 种后端,引擎类只有 4 个
   (Memory / KVStore / Hybrid / Aria),OPFS 是后端而非引擎。标题与条目已改写,
   并写明"`disk`/`hybrid` 恒用 KVStore"。
5. **`diskEngine` 生效范围** — 仅 `mode:'aria'` 生效;`constants.ts` 的注释
   此前写成"仅 mode='disk'|'hybrid' 时生效"(正好写反),已改正;README 配置表、
   快速开始示例与 Aria 示例同步标注。

数字口径统一(可复现):
- 测试 1872(90 套件)+ 14 e2e,另 4 个重型套件在独立 CI job 串行运行;
- 覆盖率 语句 90.43% / 分支 82.21% / 函数 94.27% / 行 93.44%;
- README 明确写出**产出这些数字的完整命令**(与 CI 常规 job 一致),
  并要求改动覆盖范围/阈值时同步更新表格(G5)。
- CHANGELOG 0.8.0 条目与 site 首页/文档页同步。

另修 **CONTRIBUTING 的钩子契约**:明确写出"返回值被忽略(不能取消/改写)、
就地改参数在 Table API 生效、抛异常可取消、SQL 路径的 beforeInsert 收到副本"
—— 此前只写 "allow intercepting",容易被理解为返回值可改变行为。

验证:全量 90 套件 / 1872 测试通过(+4 重型套件);覆盖率四项均高于阈值;
typecheck(src+tests)、lint、build 零错误零告警;e2e 14 项通过;dist 已重建。
This commit is contained in:
thzxx
2026-09-15 08:06:23 +08:00
parent d14663ef80
commit 799560ea05
26 changed files with 35817 additions and 106 deletions
+7 -8
View File
@@ -51,8 +51,8 @@ class ConnectionManager {
this.connections.set(name, db);
this.refCount.set(name, 1);
// 注入 disconnect 方法
(db as MetonaSqlark & { disconnect: () => Promise<void> }).disconnect = async () => {
// 注入 disconnect 方法(类型已在 MetonaSqlark 上声明,无需 any 断言)
db.disconnect = async () => {
await this.release(name);
};
@@ -114,12 +114,11 @@ class ConnectionManager {
const manager = new ConnectionManager();
// 挂载到 MetonaSqlark 静态方法(通过 any 绕过 TS 类型检查
const M = MetonaSqlark as unknown as Record<string, unknown>;
M.connect = (config: DatabaseConfig) => manager.connect(config);
M.disconnect = (dbName: string) => manager.release(dbName);
M.disconnectAll = () => manager.closeAll();
M.getActiveConnections = () => manager.getActiveConnections();
// 挂载到 MetonaSqlark 静态方法(v0.8.0:类型已在类上声明,无需 any 断言
MetonaSqlark.connect = (config: DatabaseConfig) => manager.connect(config);
MetonaSqlark.disconnect = (dbName: string) => manager.release(dbName);
MetonaSqlark.disconnectAll = () => manager.closeAll();
MetonaSqlark.getActiveConnections = () => manager.getActiveConnections();
export { manager as connectionManager };
export default manager;
+16 -2
View File
@@ -73,7 +73,14 @@ export interface DatabaseConfig {
name: string;
/** 存储模式 */
mode?: StorageMode;
/** 磁盘引擎(仅 mode='disk'|'hybrid' 时生效) */
/**
* 磁盘后端选择。
*
* v0.8.0 修正注释:**仅 `mode: 'aria'` 真正生效**(作为 Aria 的存储后端,
* 见 core.ts 的 aria 分支)。`disk` 模式恒用自研 KVStoreEngine
* `hybrid` 的内存+磁盘组合也恒用 KVStoreEngine —— 两者会忽略本项
*(此前注释写成"disk 模式生效",与实现相反)。
*/
diskEngine?: DiskEngine;
/** 版本号 */
version?: number;
@@ -186,7 +193,14 @@ export interface MetonaPlugin {
version: string;
/** 描述 */
description?: string;
/** 优先级,越大越先执行 */
/**
* 优先级:**越大越先执行**(含 `install()` 与钩子触发顺序)。
*
* v0.8.0 起真正生效 —— Core 会先按 priority 降序稳定排序再注册插件
*(同优先级保持 config 数组顺序)。此前 register() 虽按优先级插入数组,
* 但 install() 在 register 内立即执行,实际顺序 = config 数组顺序。
*/
priority?: number;
/** 安装 */
install(db: unknown): void;
+55 -4
View File
@@ -31,6 +31,38 @@ export class MetonaSqlark {
/** 数据库名称 */
readonly name: string;
/**
* v0.8.0:释放一个连接引用(引用计数 -1,归零时自动关闭)。
*
* 由 `MetonaSqlark.connect()` 注入实现 —— 此前该方法是**运行时注入、类型上不存在**:
* README 与示例都在用 `await db.disconnect()`,但 `db` 的声明里没有它,
* TypeScript 使用者会直接编译失败(只能 `as any` 绕过)。
* 普通 `create()` 得到的实例没有这个方法,因此为可选:
* 只有经 `connect()` 取得的实例才有,直接调用会抛错(而不是静默无操作)。
*/
disconnect?: () => Promise<void>;
/**
* v0.8.0:连接池静态 API 的类型声明。
*
* 这些方法由 `src/connection-manager.ts` **运行时注入**`MetonaSqlark.connect = ...`)。
* 此前注入侧用 `as unknown as Record<string, unknown>` 绕过类型检查,
* 于是 README「连接池」一节里的 `MetonaSqlark.connect(...)` /
* `MetonaSqlark.disconnectAll()` 在 TypeScript 下全部报 TS2339
*"属性不存在"),使用者只能 `as any`。
*
* 声明为 `?` 可选是因为它们**只在 import 了 connection-manager 的构建里存在**
* 核心入口不 import 它(避免无谓的模块副作用)。真正常用的路径是
* `MetonaSqlark.create()`。
*/
static connect?: (config: DatabaseConfig) => Promise<MetonaSqlark>;
/** 按库名释放一个连接引用(等价于实例上的 `disconnect()` */
static disconnect?: (dbName: string) => Promise<void>;
/** 关闭全部连接 */
static disconnectAll?: () => Promise<void>;
/** 当前活跃连接名列表 */
static getActiveConnections?: () => string[];
/**
* v0.7.1: 静态工厂(与 connect/disconnect 同一入口风格)。
* 此前 create 仅存在于 api 对象 / window 挂载 —— README/站点示例的
@@ -148,9 +180,22 @@ export class MetonaSqlark {
this.executor = new QueryExecutor(this.engine, this.maxRowsPerQuery);
this.transactionManager = new TransactionManager(this.engine);
// 注册插件
// 注册插件
//
// v0.8.0**先按 priority 降序排序再注册** —— 在这之前 PluginManager.register
// 虽然会把插件插到正确的位置,但 `install()` 是在 register 里**立即**调用的,
// 因此 install 与钩子的实际执行顺序仍等于 config 数组顺序
//(实测:priority 为 low/high/mid 的插件,钩子按 low→high→mid 触发,
// 只有 getPlugins() 才是 high,mid,low)。而 README/CONTRIBUTING 一直宣称
// "priority 越大越先执行" —— 文档与实现不符。
// 这里选择**让实现符合文档**(priority 是用户可见的配置项,静默无效比没有更糟)。
// 用稳定排序:同优先级保持 config 数组中的相对顺序。
if (this.config.plugins) {
for (const plugin of this.config.plugins) {
const ordered = this.config.plugins
.map((plugin, index) => ({ plugin, index }))
.sort((a, b) => (b.plugin.priority ?? 0) - (a.plugin.priority ?? 0) || a.index - b.index)
.map((entry) => entry.plugin);
for (const plugin of ordered) {
this.pluginManager.register(plugin, this);
}
}
@@ -489,8 +534,14 @@ export class MetonaSqlark {
}
/**
* v0.5.1: 在线备份 — 导出全库一致性快照
* Aria 引擎走引擎级 backup()(MVCC 一致性视图);其余引擎回退 exportAll()。
* v0.5.1: 在线备份 — 导出全库数据
*
* v0.8.0 修正表述:此前注释与 README 宣称"全库**一致性**快照",但引擎层
* 并没有跨表快照原语 —— 实现是**逐表读取**(Aria 走引擎级 `backup()`
* 其余引擎回退 `exportAll()`)。备份过程中的并发写入会让不同表来自不同
* 时间点(单表内部仍是一致的)。需要强一致时先 `close()`,或用
* `db.transaction()` 包住调用(事务期间并发写被 `TX_ACTIVE` 拒绝)。
* 真正的跨表快照需要 COW 行所有权改造,列入后续版本。
*/
async backup(): Promise<Record<string, Record<string, unknown>[]>> {
this.ensureReady();
+6
View File
@@ -97,3 +97,9 @@ export { bindParameters } from './sql/params';
// AriaEngine 类型 & 后端
export type { AriaEngineConfig } from './engine/aria/types';
export { OPFSBackend } from './engine/aria/store/opfs_backend';
// v0.8.0: 迁移工具 —— 此前只在 src/migration/index.ts 定义,主入口未导出,
// 于是 package.json 的 "./migration" 子路径**指向的产物里根本没有这个函数**
//`import { migrateFromIndexedDB } from '.../migration'` 会得到 undefined)。
export { migrateFromIndexedDB } from './migration/index';
export type { MigrationOptions, MigrationResult } from './migration/index';
+63
View File
@@ -0,0 +1,63 @@
/**
* metona-sqlark React 集成 —— 公开类型声明(v0.8.0)
*
* 为什么手写而不是从 `react.ts` 生成:该文件带 `// @ts-nocheck`hooks 的
* 泛型推断需要 React 类型,而 React 是 peer dependency,构建时不一定存在),
* 由 rollup-plugin-dts 生成只会得到 `any` —— 对使用者毫无价值。
* 这里给出**手写的精确签名**,并随包发布(package.json 的 `./react` 指向它)。
*
* 注意:`react` 是 peer dependency,本文件不 import 任何 React 类型,
* 因此即使使用者的项目里没有装 React 也能通过类型检查(只要不真的调用)。
*/
import type { MetonaSqlark, DatabaseConfig } from '../constants';
/** `useQuery` 的返回结构 */
export interface UseQueryResult<T = Record<string, unknown>> {
/** 查询结果(初次渲染时为空数组) */
data: T[];
/** 是否正在查询 */
loading: boolean;
/** 查询错误(成功时为 null) */
error: Error | null;
/** 重新执行查询 */
refresh: () => void;
}
/** `useTable` 的返回结构 */
export interface UseTableResult<T = Record<string, unknown>> {
data: T[];
loading: boolean;
refresh: () => void;
}
/** `useDatabase` 的返回结构 */
export interface UseDatabaseResult {
/** 初始化完成的数据库实例;未就绪时为 null */
db: MetonaSqlark | null;
ready: boolean;
error: Error | null;
}
/**
* 执行 SQL 查询并在结果变化时重渲染。
* @param db 已初始化的数据库实例
* @param sql SQL 语句
* @param deps 依赖数组,变化时重新查询(同 React useEffect 语义)
*/
export function useQuery<T = Record<string, unknown>>(
db: MetonaSqlark,
sql: string,
deps?: unknown[],
): UseQueryResult<T>;
/** 查询整张表(`SELECT * FROM <table>`),表名会做标识符校验 */
export function useTable<T = Record<string, unknown>>(
db: MetonaSqlark,
tableName: string,
): UseTableResult<T>;
/**
* 创建并管理数据库实例:组件挂载时 init,卸载时 close。
* `config` 变化(按序列化指纹比较)时重建实例。
*/
export function useDatabase(config: DatabaseConfig): UseDatabaseResult;
+56
View File
@@ -0,0 +1,56 @@
/**
* metona-sqlark Vue 集成 —— 公开类型声明(v0.8.0)
*
* 与 `react.d.ts` 同理:`vue.ts` 带 `// @ts-nocheck`,自动生成的声明只有 `any`,
* 因此这里手写精确签名并随包发布。`vue` 是 peer dependency
* 本文件用最小结构类型(`Ref`)描述响应式引用,不 import Vue 本身。
*/
import type { MetonaSqlark, DatabaseConfig } from '../constants';
/** 最小响应式引用结构(与 Vue 的 `Ref<T>` 兼容,但不依赖 vue 包) */
export interface Ref<T> {
value: T;
}
/** `useSqlarkQuery` 的返回结构 */
export interface UseSqlarkQueryResult<T = Record<string, unknown>> {
data: Ref<T[]>;
loading: Ref<boolean>;
error: Ref<Error | null>;
refresh: () => void;
}
/** `useSqlarkTable` 的返回结构 */
export interface UseSqlarkTableResult<T = Record<string, unknown>> {
data: Ref<T[]>;
loading: Ref<boolean>;
refresh: () => void;
}
/** `useSqlarkDatabase` 的返回结构 */
export interface UseSqlarkDatabaseResult {
db: Ref<MetonaSqlark | null>;
ready: Ref<boolean>;
error: Ref<Error | null>;
}
/**
* 执行 SQL 查询并保持结果为响应式引用。
* @param db 已初始化的数据库实例
* @param sql SQL 语句
* @param deps 依赖的响应式引用数组,变化时重新查询
*/
export function useSqlarkQuery<T = Record<string, unknown>>(
db: MetonaSqlark,
sql: string,
deps?: Ref<unknown>[],
): UseSqlarkQueryResult<T>;
/** 查询整张表(`SELECT * FROM <table>`),表名会做标识符校验 */
export function useSqlarkTable<T = Record<string, unknown>>(
db: MetonaSqlark,
tableName: string,
): UseSqlarkTableResult<T>;
/** 创建并管理数据库实例:`onMounted` 时 init */
export function useSqlarkDatabase(config: DatabaseConfig): UseSqlarkDatabaseResult;