fix: v0.7.1 API 修复与防御统一 — static create / 未 open 防护 / 原型污染 / lint 清零
CI / test (22.x) (push) Successful in 17m24s
CI / e2e (push) Successful in 10m39s
CI / test (18.x) (push) Successful in 19m5s
CI / test (20.x) (push) Successful in 18m4s
CI / test (24.x) (push) Successful in 23m19s

- MetonaSqlark.create 静态工厂(README 示例在 ESM/Node 下此前 TypeError),
  独立 create 函数委托静态实现
- close 未初始化防御(engine undefined 不再崩溃)
- AriaEngine hasTable/getTableNames/getTableSchema 统一 ensureOpen
- 事务回滚失败不掩盖原始错误
- __proto__ 列名防护:Executor 列映射 Object.create(null) + schema 校验拒绝
- lint 清零(移除 5 处未使用导入)

测试 1147 → 1155(73 套件);行覆盖率 89.8%;版本 0.7.1
This commit is contained in:
thzxx
2026-08-13 11:14:42 +08:00
parent 57415975ea
commit cbe407eb49
25 changed files with 1620 additions and 1375 deletions
+32
View File
@@ -2,6 +2,38 @@
All notable changes to MetonaSqlark will be documented in this file. All notable changes to MetonaSqlark will be documented in this file.
## [0.7.1] - 2026-08-13
### API 修复 / 防御统一 / 工程质量
> 深度审计第四阶段:修复 README 示例 API 缺失、未 open 防护不一致、
> 事务错误掩盖、原型污染防护,lint 清零。
### Fixed
- **`MetonaSqlark.create` 静态工厂缺失(API/文档不一致)** — create 此前仅存在于
api 对象与 window 挂载,README/CONTRIBUTING/site 全部示例的
`MetonaSqlark.create({...})` 在 ESM/Node 下是 undefinedTypeError)。
类增加静态 create(与 connect/disconnect 同入口风格),独立 create 函数委托
- **close 未初始化崩溃** — `new MetonaSqlark(cfg).close()`init 失败/未调用)时
`this.engine` undefined → TypeError。close 增加防御
- **AriaEngine 未 open 防护不一致** — hasTable/getTableNames/getTableSchema 未检查
DB_NOT_OPEN(返回空而非报错,与 KVStoreEngine 及自身其余方法不一致)。
统一 ensureOpen
- **事务回滚失败掩盖原始错误** — rollbackTransaction 抛错会替换掉真正导致事务
失败的异常(定位困难)。回滚失败吞掉,保留原始错误
- **`__proto__` 列名原型污染防护** — SQL CREATE TABLE 的列名 `__proto__` 在普通
对象上触发原型 setter 静默丢列。Executor 列映射改用 Object.create(null)
(键可见),schema 校验层显式拒绝该列名(SCHEMA_ERROR
- **lint 清零** — 移除 5 处未使用导入(backend/file_manager/kvstore_engine/parser
### Changed
- 测试 1147 → **1155**73 套件,+8 个 v0.7.1 回归);新增 `tests/v071-hardening.test.ts`
行覆盖率 89.8%lint 0 error 0 warning
---
## [0.7.0] - 2026-08-13 ## [0.7.0] - 2026-08-13
### 参数化查询 / 事务性能 / 语义硬化 ### 参数化查询 / 事务性能 / 语义硬化
+4 -4
View File
@@ -1,10 +1,10 @@
# MetonaSqlark # MetonaSqlark
<p align="center"> <p align="center">
<img src="https://img.shields.io/badge/version-0.7.0-blue?style=flat-square" alt="version"> <img src="https://img.shields.io/badge/version-0.7.1-blue?style=flat-square" alt="version">
<img src="https://img.shields.io/badge/license-MIT-green?style=flat-square" alt="license"> <img src="https://img.shields.io/badge/license-MIT-green?style=flat-square" alt="license">
<img src="https://img.shields.io/badge/coverage-89.8%25-brightgreen?style=flat-square" alt="coverage"> <img src="https://img.shields.io/badge/coverage-89.8%25-brightgreen?style=flat-square" alt="coverage">
<img src="https://img.shields.io/badge/tests-1147%20passed-success?style=flat-square" alt="tests"> <img src="https://img.shields.io/badge/tests-1155%20passed-success?style=flat-square" alt="tests">
</p> </p>
> 基于 TypeScript 的**前端关系型数据库**:完整 SQL + Query Builder 双 API > 基于 TypeScript 的**前端关系型数据库**:完整 SQL + Query Builder 双 API
@@ -401,7 +401,7 @@ const { data, loading, error, refresh } = useSqlarkQuery(db, 'SELECT * FROM user
npm install # 安装依赖 npm install # 安装依赖
npm run dev # 开发模式(localhost:3001 npm run dev # 开发模式(localhost:3001
npm run build # 生产构建(生成 dist/ npm run build # 生产构建(生成 dist/
npm test # 运行测试(1147 用例 · 72 套件) npm test # 运行测试(1155 用例 · 73 套件)
npm run test:e2e # Playwright e2e(真实 Chromium + OPFS + 崩溃注入,需先 build npm run test:e2e # Playwright e2e(真实 Chromium + OPFS + 崩溃注入,需先 build
npm run lint # 代码检查 npm run lint # 代码检查
npm run typecheck # 类型检查 npm run typecheck # 类型检查
@@ -413,7 +413,7 @@ npm run typecheck # 类型检查
| 指标 | 数值 | | 指标 | 数值 |
|------|------| |------|------|
| 测试用例 | 1147+12 Playwright e2e | | 测试用例 | 1155+12 Playwright e2e |
| 测试套件 | 70 | | 测试套件 | 70 |
| 行覆盖率 | 89.8% | | 行覆盖率 | 89.8% |
| SQL 关键字 | 72 | | SQL 关键字 | 72 |
+33 -6
View File
@@ -34,7 +34,7 @@ class DatabaseError extends Error {
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// 版本 // 版本
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
const VERSION = '0.7.0'; const VERSION = '0.7.1';
/** /**
* metona-sqlark Shared WHERE Matcher 统一的条件匹配逻辑 * metona-sqlark Shared WHERE Matcher 统一的条件匹配逻辑
@@ -2456,6 +2456,11 @@ function validateColumns(columns) {
} }
let primaryKeyCount = 0; let primaryKeyCount = 0;
for (const [colName, colDef] of Object.entries(columns)) { for (const [colName, colDef] of Object.entries(columns)) {
// v0.7.1: '__proto__' 作为列名会触发对象原型 setter(列静默丢失);
// 显式拒绝避免原型污染类攻击面
if (colName === '__proto__') {
throw new DatabaseError('Column name "__proto__" is not allowed', 'SCHEMA_ERROR');
}
// 类型校验 // 类型校验
if (!FIELD_TYPES.includes(colDef.type)) { if (!FIELD_TYPES.includes(colDef.type)) {
throw new DatabaseError(`Invalid type "${colDef.type}" for column "${colName}". Valid types: ${FIELD_TYPES.join(', ')}`, 'SCHEMA_ERROR'); throw new DatabaseError(`Invalid type "${colDef.type}" for column "${colName}". Valid types: ${FIELD_TYPES.join(', ')}`, 'SCHEMA_ERROR');
@@ -6699,12 +6704,16 @@ class AriaEngine {
} }
} }
async hasTable(tableName) { async hasTable(tableName) {
// v0.7.1: 未 open 防护统一(此前与 KVStoreEngine 不一致:返回空而非报错)
this.ensureOpen();
return this.schemas.has(tableName); return this.schemas.has(tableName);
} }
async getTableNames() { async getTableNames() {
this.ensureOpen();
return Array.from(this.schemas.keys()); return Array.from(this.schemas.keys());
} }
async getTableSchema(tableName) { async getTableSchema(tableName) {
this.ensureOpen();
return this.schemas.get(tableName) ?? null; return this.schemas.get(tableName) ?? null;
} }
// ======================================================================= // =======================================================================
@@ -10961,7 +10970,9 @@ class QueryExecutor {
if (exists) if (exists)
return; return;
} }
const columns = {}; // v0.7.1: Object.create(null) —— 防止 '__proto__' 列名触发原型 setter 静默丢列
// createSchema 校验会显式拒绝该列名)
const columns = Object.create(null);
for (const col of stmt.columns) for (const col of stmt.columns)
columns[col.name] = astColumnToColumnDef(col); columns[col.name] = astColumnToColumnDef(col);
return this.engine.createTable(createSchema(stmt.name, columns)); return this.engine.createTable(createSchema(stmt.name, columns));
@@ -11739,8 +11750,12 @@ class TransactionManager {
return result; return result;
} }
catch (error) { catch (error) {
// 失败 → 回滚 // 失败 → 回滚。v0.7.1: 回滚自身失败不掩盖原始事务错误
// (此前 rollback 抛错会替换掉真正导致失败的异常,定位困难)
try {
await this.engine.rollbackTransaction(); await this.engine.rollbackTransaction();
}
catch { /* 回滚失败保留原始错误 */ }
if (error instanceof DatabaseError) if (error instanceof DatabaseError)
throw error; throw error;
throw new DatabaseError(`Transaction failed: ${error.message}`, 'TRANSACTION_ERROR', error); throw new DatabaseError(`Transaction failed: ${error.message}`, 'TRANSACTION_ERROR', error);
@@ -11838,6 +11853,16 @@ class PluginManager {
// MetonaSqlark // MetonaSqlark
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
class MetonaSqlark { class MetonaSqlark {
/**
* v0.7.1: 静态工厂 connect/disconnect 同一入口风格
* 此前 create 仅存在于 api 对象 / window 挂载 README/站点示例的
* `MetonaSqlark.create({...})` ESM/Node 下是 undefinedTypeError
*/
static async create(config) {
const db = new MetonaSqlark(config);
await db.init();
return db;
}
/** 获取版本号 */ /** 获取版本号 */
get version() { return this._version; } get version() { return this._version; }
/** 查询结果行数上限 */ /** 查询结果行数上限 */
@@ -12285,7 +12310,10 @@ class MetonaSqlark {
this.channel = null; this.channel = null;
} }
this.pluginManager.destroy(); this.pluginManager.destroy();
// v0.7.1: init 失败/未调用时 close 不应崩溃(此前 this.engine undefined → TypeError
if (this.engine) {
await this.engine.close(); await this.engine.close();
}
this.tableCache.clear(); this.tableCache.clear();
this.ready = false; this.ready = false;
} }
@@ -12479,9 +12507,8 @@ M.getActiveConnections = () => manager.getActiveConnections();
* ``` * ```
*/ */
async function create(config) { async function create(config) {
const db = new MetonaSqlark(config); // v0.7.1: 委托静态 create(类与独立函数行为一致,README 两种写法均可)
await db.init(); return MetonaSqlark.create(config);
return db;
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// 全局 API // 全局 API
+1 -1
View File
File diff suppressed because one or more lines are too long
+7 -1
View File
@@ -164,7 +164,7 @@ interface MetonaPlugin {
/** 销毁 */ /** 销毁 */
destroy(): void; destroy(): void;
} }
declare const VERSION = "0.7.0"; declare const VERSION = "0.7.1";
/** /**
* metona-sqlark Plugin — 插件系统 * metona-sqlark Plugin — 插件系统
@@ -644,6 +644,12 @@ declare class Transaction {
declare class MetonaSqlark { declare class MetonaSqlark {
/** 数据库名称 */ /** 数据库名称 */
readonly name: string; readonly name: string;
/**
* v0.7.1: 静态工厂(与 connect/disconnect 同一入口风格)。
* 此前 create 仅存在于 api 对象 / window 挂载 —— README/站点示例的
* `MetonaSqlark.create({...})` 在 ESM/Node 下是 undefinedTypeError)。
*/
static create(config: DatabaseConfig): Promise<MetonaSqlark>;
/** 存储模式 */ /** 存储模式 */
readonly mode: string; readonly mode: string;
/** 版本号 */ /** 版本号 */
+33 -6
View File
@@ -30,7 +30,7 @@ class DatabaseError extends Error {
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// 版本 // 版本
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
const VERSION = '0.7.0'; const VERSION = '0.7.1';
/** /**
* metona-sqlark Shared WHERE Matcher 统一的条件匹配逻辑 * metona-sqlark Shared WHERE Matcher 统一的条件匹配逻辑
@@ -2452,6 +2452,11 @@ function validateColumns(columns) {
} }
let primaryKeyCount = 0; let primaryKeyCount = 0;
for (const [colName, colDef] of Object.entries(columns)) { for (const [colName, colDef] of Object.entries(columns)) {
// v0.7.1: '__proto__' 作为列名会触发对象原型 setter(列静默丢失);
// 显式拒绝避免原型污染类攻击面
if (colName === '__proto__') {
throw new DatabaseError('Column name "__proto__" is not allowed', 'SCHEMA_ERROR');
}
// 类型校验 // 类型校验
if (!FIELD_TYPES.includes(colDef.type)) { if (!FIELD_TYPES.includes(colDef.type)) {
throw new DatabaseError(`Invalid type "${colDef.type}" for column "${colName}". Valid types: ${FIELD_TYPES.join(', ')}`, 'SCHEMA_ERROR'); throw new DatabaseError(`Invalid type "${colDef.type}" for column "${colName}". Valid types: ${FIELD_TYPES.join(', ')}`, 'SCHEMA_ERROR');
@@ -6695,12 +6700,16 @@ class AriaEngine {
} }
} }
async hasTable(tableName) { async hasTable(tableName) {
// v0.7.1: 未 open 防护统一(此前与 KVStoreEngine 不一致:返回空而非报错)
this.ensureOpen();
return this.schemas.has(tableName); return this.schemas.has(tableName);
} }
async getTableNames() { async getTableNames() {
this.ensureOpen();
return Array.from(this.schemas.keys()); return Array.from(this.schemas.keys());
} }
async getTableSchema(tableName) { async getTableSchema(tableName) {
this.ensureOpen();
return this.schemas.get(tableName) ?? null; return this.schemas.get(tableName) ?? null;
} }
// ======================================================================= // =======================================================================
@@ -10957,7 +10966,9 @@ class QueryExecutor {
if (exists) if (exists)
return; return;
} }
const columns = {}; // v0.7.1: Object.create(null) —— 防止 '__proto__' 列名触发原型 setter 静默丢列
// createSchema 校验会显式拒绝该列名)
const columns = Object.create(null);
for (const col of stmt.columns) for (const col of stmt.columns)
columns[col.name] = astColumnToColumnDef(col); columns[col.name] = astColumnToColumnDef(col);
return this.engine.createTable(createSchema(stmt.name, columns)); return this.engine.createTable(createSchema(stmt.name, columns));
@@ -11735,8 +11746,12 @@ class TransactionManager {
return result; return result;
} }
catch (error) { catch (error) {
// 失败 → 回滚 // 失败 → 回滚。v0.7.1: 回滚自身失败不掩盖原始事务错误
// (此前 rollback 抛错会替换掉真正导致失败的异常,定位困难)
try {
await this.engine.rollbackTransaction(); await this.engine.rollbackTransaction();
}
catch { /* 回滚失败保留原始错误 */ }
if (error instanceof DatabaseError) if (error instanceof DatabaseError)
throw error; throw error;
throw new DatabaseError(`Transaction failed: ${error.message}`, 'TRANSACTION_ERROR', error); throw new DatabaseError(`Transaction failed: ${error.message}`, 'TRANSACTION_ERROR', error);
@@ -11834,6 +11849,16 @@ class PluginManager {
// MetonaSqlark // MetonaSqlark
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
class MetonaSqlark { class MetonaSqlark {
/**
* v0.7.1: 静态工厂 connect/disconnect 同一入口风格
* 此前 create 仅存在于 api 对象 / window 挂载 README/站点示例的
* `MetonaSqlark.create({...})` ESM/Node 下是 undefinedTypeError
*/
static async create(config) {
const db = new MetonaSqlark(config);
await db.init();
return db;
}
/** 获取版本号 */ /** 获取版本号 */
get version() { return this._version; } get version() { return this._version; }
/** 查询结果行数上限 */ /** 查询结果行数上限 */
@@ -12281,7 +12306,10 @@ class MetonaSqlark {
this.channel = null; this.channel = null;
} }
this.pluginManager.destroy(); this.pluginManager.destroy();
// v0.7.1: init 失败/未调用时 close 不应崩溃(此前 this.engine undefined → TypeError
if (this.engine) {
await this.engine.close(); await this.engine.close();
}
this.tableCache.clear(); this.tableCache.clear();
this.ready = false; this.ready = false;
} }
@@ -12475,9 +12503,8 @@ M.getActiveConnections = () => manager.getActiveConnections();
* ``` * ```
*/ */
async function create(config) { async function create(config) {
const db = new MetonaSqlark(config); // v0.7.1: 委托静态 create(类与独立函数行为一致,README 两种写法均可)
await db.init(); return MetonaSqlark.create(config);
return db;
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// 全局 API // 全局 API
+1 -1
View File
File diff suppressed because one or more lines are too long
+33 -6
View File
@@ -36,7 +36,7 @@
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// 版本 // 版本
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
const VERSION = '0.7.0'; const VERSION = '0.7.1';
/** /**
* metona-sqlark Shared WHERE Matcher 统一的条件匹配逻辑 * metona-sqlark Shared WHERE Matcher 统一的条件匹配逻辑
@@ -2458,6 +2458,11 @@
} }
let primaryKeyCount = 0; let primaryKeyCount = 0;
for (const [colName, colDef] of Object.entries(columns)) { for (const [colName, colDef] of Object.entries(columns)) {
// v0.7.1: '__proto__' 作为列名会触发对象原型 setter(列静默丢失);
// 显式拒绝避免原型污染类攻击面
if (colName === '__proto__') {
throw new DatabaseError('Column name "__proto__" is not allowed', 'SCHEMA_ERROR');
}
// 类型校验 // 类型校验
if (!FIELD_TYPES.includes(colDef.type)) { if (!FIELD_TYPES.includes(colDef.type)) {
throw new DatabaseError(`Invalid type "${colDef.type}" for column "${colName}". Valid types: ${FIELD_TYPES.join(', ')}`, 'SCHEMA_ERROR'); throw new DatabaseError(`Invalid type "${colDef.type}" for column "${colName}". Valid types: ${FIELD_TYPES.join(', ')}`, 'SCHEMA_ERROR');
@@ -6701,12 +6706,16 @@
} }
} }
async hasTable(tableName) { async hasTable(tableName) {
// v0.7.1: 未 open 防护统一(此前与 KVStoreEngine 不一致:返回空而非报错)
this.ensureOpen();
return this.schemas.has(tableName); return this.schemas.has(tableName);
} }
async getTableNames() { async getTableNames() {
this.ensureOpen();
return Array.from(this.schemas.keys()); return Array.from(this.schemas.keys());
} }
async getTableSchema(tableName) { async getTableSchema(tableName) {
this.ensureOpen();
return this.schemas.get(tableName) ?? null; return this.schemas.get(tableName) ?? null;
} }
// ======================================================================= // =======================================================================
@@ -10963,7 +10972,9 @@
if (exists) if (exists)
return; return;
} }
const columns = {}; // v0.7.1: Object.create(null) —— 防止 '__proto__' 列名触发原型 setter 静默丢列
// createSchema 校验会显式拒绝该列名)
const columns = Object.create(null);
for (const col of stmt.columns) for (const col of stmt.columns)
columns[col.name] = astColumnToColumnDef(col); columns[col.name] = astColumnToColumnDef(col);
return this.engine.createTable(createSchema(stmt.name, columns)); return this.engine.createTable(createSchema(stmt.name, columns));
@@ -11741,8 +11752,12 @@
return result; return result;
} }
catch (error) { catch (error) {
// 失败 → 回滚 // 失败 → 回滚。v0.7.1: 回滚自身失败不掩盖原始事务错误
// (此前 rollback 抛错会替换掉真正导致失败的异常,定位困难)
try {
await this.engine.rollbackTransaction(); await this.engine.rollbackTransaction();
}
catch { /* 回滚失败保留原始错误 */ }
if (error instanceof DatabaseError) if (error instanceof DatabaseError)
throw error; throw error;
throw new DatabaseError(`Transaction failed: ${error.message}`, 'TRANSACTION_ERROR', error); throw new DatabaseError(`Transaction failed: ${error.message}`, 'TRANSACTION_ERROR', error);
@@ -11840,6 +11855,16 @@
// MetonaSqlark // MetonaSqlark
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
class MetonaSqlark { class MetonaSqlark {
/**
* v0.7.1: 静态工厂 connect/disconnect 同一入口风格
* 此前 create 仅存在于 api 对象 / window 挂载 README/站点示例的
* `MetonaSqlark.create({...})` ESM/Node 下是 undefinedTypeError
*/
static async create(config) {
const db = new MetonaSqlark(config);
await db.init();
return db;
}
/** 获取版本号 */ /** 获取版本号 */
get version() { return this._version; } get version() { return this._version; }
/** 查询结果行数上限 */ /** 查询结果行数上限 */
@@ -12287,7 +12312,10 @@
this.channel = null; this.channel = null;
} }
this.pluginManager.destroy(); this.pluginManager.destroy();
// v0.7.1: init 失败/未调用时 close 不应崩溃(此前 this.engine undefined → TypeError
if (this.engine) {
await this.engine.close(); await this.engine.close();
}
this.tableCache.clear(); this.tableCache.clear();
this.ready = false; this.ready = false;
} }
@@ -12481,9 +12509,8 @@
* ``` * ```
*/ */
async function create(config) { async function create(config) {
const db = new MetonaSqlark(config); // v0.7.1: 委托静态 create(类与独立函数行为一致,README 两种写法均可)
await db.init(); return MetonaSqlark.create(config);
return db;
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// 全局 API // 全局 API
+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
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@metona-team/metona-sqlark", "name": "@metona-team/metona-sqlark",
"version": "0.7.0", "version": "0.7.1",
"description": "Frontend SQL database with in-memory and disk dual-mode storage", "description": "Frontend SQL database with in-memory and disk dual-mode storage",
"type": "module", "type": "module",
"main": "dist/metona-sqlark.cjs", "main": "dist/metona-sqlark.cjs",
+1 -1
View File
@@ -214,4 +214,4 @@ export class DatabaseError extends Error {
// 版本 // 版本
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
export const VERSION = '0.7.0'; export const VERSION = '0.7.1';
+15
View File
@@ -29,6 +29,18 @@ export class MetonaSqlark {
/** 数据库名称 */ /** 数据库名称 */
readonly name: string; readonly name: string;
/**
* v0.7.1: 静态工厂 connect/disconnect
* create api / window README/
* `MetonaSqlark.create({...})` ESM/Node undefinedTypeError
*/
static async create(config: DatabaseConfig): Promise<MetonaSqlark> {
const db = new MetonaSqlark(config);
await db.init();
return db;
}
/** 存储模式 */ /** 存储模式 */
readonly mode: string; readonly mode: string;
@@ -540,7 +552,10 @@ export class MetonaSqlark {
this.channel = null; this.channel = null;
} }
this.pluginManager.destroy(); this.pluginManager.destroy();
// v0.7.1: init 失败/未调用时 close 不应崩溃(此前 this.engine undefined → TypeError
if (this.engine) {
await this.engine.close(); await this.engine.close();
}
this.tableCache.clear(); this.tableCache.clear();
this.ready = false; this.ready = false;
} }
+4
View File
@@ -512,14 +512,18 @@ export class AriaEngine implements IStorageEngine {
} }
async hasTable(tableName: string): Promise<boolean> { async hasTable(tableName: string): Promise<boolean> {
// v0.7.1: 未 open 防护统一(此前与 KVStoreEngine 不一致:返回空而非报错)
this.ensureOpen();
return this.schemas.has(tableName); return this.schemas.has(tableName);
} }
async getTableNames(): Promise<string[]> { async getTableNames(): Promise<string[]> {
this.ensureOpen();
return Array.from(this.schemas.keys()); return Array.from(this.schemas.keys());
} }
async getTableSchema(tableName: string): Promise<TableSchema | null> { async getTableSchema(tableName: string): Promise<TableSchema | null> {
this.ensureOpen();
return this.schemas.get(tableName) ?? null; return this.schemas.get(tableName) ?? null;
} }
-2
View File
@@ -6,8 +6,6 @@
* Buffer Pool PageIO WAL WALStore 使 * Buffer Pool PageIO WAL WALStore 使
*/ */
import { DatabaseError } from '../../../constants';
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// StorageBackend 接口 // StorageBackend 接口
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
+1 -2
View File
@@ -7,8 +7,7 @@
import type { IStorageBackend } from './backend'; import type { IStorageBackend } from './backend';
import type { PageIO } from '../buffer/pool'; import type { PageIO } from '../buffer/pool';
import { PAGE_SIZE, PageType } from '../types'; import { PAGE_SIZE } from '../types';
import { initPageHeader } from '../page/header';
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// FileManager (implements PageIO) // FileManager (implements PageIO)
-1
View File
@@ -22,7 +22,6 @@ import type { QueryPlan, TableSchema } from '../constants';
import { DatabaseError } from '../constants'; import { DatabaseError } from '../constants';
import { MemoryEngine } from './memory'; import { MemoryEngine } from './memory';
import { KVStore } from './kvstore/index'; import { KVStore } from './kvstore/index';
import { SharedMemoryBackend } from './kvstore/shared_memory_medium';
import type { IStorageBackend } from './aria/store/backend'; import type { IStorageBackend } from './aria/store/backend';
const SCHEMA_KEY = '__schema'; const SCHEMA_KEY = '__schema';
+2 -3
View File
@@ -38,9 +38,8 @@ import './connection-manager';
* ``` * ```
*/ */
async function create(config: DatabaseConfig): Promise<MetonaSqlark> { async function create(config: DatabaseConfig): Promise<MetonaSqlark> {
const db = new MetonaSqlark(config); // v0.7.1: 委托静态 create(类与独立函数行为一致,README 两种写法均可)
await db.init(); return MetonaSqlark.create(config);
return db;
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
+3 -1
View File
@@ -712,7 +712,9 @@ export class QueryExecutor {
const exists = await this.engine.hasTable(stmt.name); const exists = await this.engine.hasTable(stmt.name);
if (exists) return; if (exists) return;
} }
const columns: Record<string, any> = {}; // v0.7.1: Object.create(null) —— 防止 '__proto__' 列名触发原型 setter 静默丢列
// createSchema 校验会显式拒绝该列名)
const columns: Record<string, any> = Object.create(null) as Record<string, any>;
for (const col of stmt.columns) columns[col.name] = astColumnToColumnDef(col); for (const col of stmt.columns) columns[col.name] = astColumnToColumnDef(col);
return this.engine.createTable(createSchema(stmt.name, columns)); return this.engine.createTable(createSchema(stmt.name, columns));
} }
-1
View File
@@ -23,7 +23,6 @@ import type {
DropIndexStatement, DropIndexStatement,
BeginTransactionStatement, BeginTransactionStatement,
CommitTransactionStatement, CommitTransactionStatement,
RollbackTransactionStatement,
ASTColumnDef, ASTColumnDef,
} from '../query/ast'; } from '../query/ast';
import type { WhereCondition, OrderBy, SortDirection } from '../constants'; import type { WhereCondition, OrderBy, SortDirection } from '../constants';
+6
View File
@@ -26,6 +26,12 @@ export function validateColumns(columns: Record<string, ColumnDef>): void {
let primaryKeyCount = 0; let primaryKeyCount = 0;
for (const [colName, colDef] of Object.entries(columns)) { for (const [colName, colDef] of Object.entries(columns)) {
// v0.7.1: '__proto__' 作为列名会触发对象原型 setter(列静默丢失);
// 显式拒绝避免原型污染类攻击面
if (colName === '__proto__') {
throw new DatabaseError('Column name "__proto__" is not allowed', 'SCHEMA_ERROR');
}
// 类型校验 // 类型校验
if (!FIELD_TYPES.includes(colDef.type)) { if (!FIELD_TYPES.includes(colDef.type)) {
throw new DatabaseError( throw new DatabaseError(
+4 -1
View File
@@ -64,8 +64,11 @@ export class TransactionManager {
trx._markCompleted(); trx._markCompleted();
return result; return result;
} catch (error) { } catch (error) {
// 失败 → 回滚 // 失败 → 回滚。v0.7.1: 回滚自身失败不掩盖原始事务错误
// (此前 rollback 抛错会替换掉真正导致失败的异常,定位困难)
try {
await this.engine.rollbackTransaction(); await this.engine.rollbackTransaction();
} catch { /* 回滚失败保留原始错误 */ }
if (error instanceof DatabaseError) throw error; if (error instanceof DatabaseError) throw error;
throw new DatabaseError(`Transaction failed: ${(error as Error).message}`, 'TRANSACTION_ERROR', error); throw new DatabaseError(`Transaction failed: ${(error as Error).message}`, 'TRANSACTION_ERROR', error);
} }
+1 -1
View File
@@ -28,7 +28,7 @@ beforeEach(() => { installOPFSMock(new Map()); });
describe('[v0.2.5] P0-1: 版本号统一', () => { describe('[v0.2.5] P0-1: 版本号统一', () => {
test('VERSION 常量为当前版本(0.6.0', () => { test('VERSION 常量为当前版本(0.6.0', () => {
expect(VERSION).toBe('0.7.0'); expect(VERSION).toBe('0.7.1');
}); });
}); });
+1 -1
View File
@@ -403,7 +403,7 @@ describe('[v0.3.3] P1-9: Savepoint + MVCC 一致性', () => {
describe('[v0.3.3] 端到端', () => { describe('[v0.3.3] 端到端', () => {
test('全部修复点可共存于 MetonaSqlark API', async () => { test('全部修复点可共存于 MetonaSqlark API', async () => {
expect(VERSION).toBe('0.7.0'); expect(VERSION).toBe('0.7.1');
const db = new MetonaSqlark({ name: `e2e-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`, mode: 'memory' }); const db = new MetonaSqlark({ name: `e2e-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`, mode: 'memory' });
await db.init(); await db.init();
await db.defineTable('users', { await db.defineTable('users', {
+102
View File
@@ -0,0 +1,102 @@
/**
* v0.7.1
*
*
* - MetonaSqlark.create README API
* - close TypeError
* - AriaEngine open hasTable/getTableNames/getTableSchema
* -
* - '__proto__'
*/
import { MetonaSqlark } from '../src/core';
import { create } from '../src/index';
import { AriaEngine } from '../src/engine/aria/index';
import { MemoryEngine } from '../src/engine/memory';
import { TransactionManager } from '../src/transaction/index';
import { createSchema } from '../src/table/schema';
function uniqueDB(): string {
return `v071-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
}
describe('v0.7.1 — MetonaSqlark.create 静态工厂', () => {
it('类静态 create 可用(与 api create 行为一致)', async () => {
const db = await MetonaSqlark.create({ name: uniqueDB(), mode: 'memory' });
expect(db.isReady()).toBe(true);
await db.defineTable('t', { id: { type: 'string', primaryKey: true } });
await db.query('INSERT INTO t VALUES (\'1\')');
const rows = await db.query('SELECT * FROM t') as Record<string, unknown>[];
expect(rows).toHaveLength(1);
await db.close();
});
it('独立 create 函数仍可用', async () => {
const db = await create({ name: uniqueDB(), mode: 'memory' });
expect(db.isReady()).toBe(true);
await db.close();
});
it('init 前 close 不崩溃', async () => {
const db = new MetonaSqlark({ name: uniqueDB(), mode: 'memory' });
await expect(db.close()).resolves.toBeUndefined();
expect(db.isReady()).toBe(false);
});
});
describe('v0.7.1 — AriaEngine 未 open 防护统一', () => {
it('close 后 getTableNames/hasTable/getTableSchema 抛 DB_NOT_OPEN', async () => {
const eng = new AriaEngine({ storageBackend: 'memory' });
await eng.open(uniqueDB(), 1);
await eng.close();
await expect(eng.getTableNames()).rejects.toMatchObject({ code: 'DB_NOT_OPEN' });
await expect(eng.hasTable('t')).rejects.toMatchObject({ code: 'DB_NOT_OPEN' });
await expect(eng.getTableSchema('t')).rejects.toMatchObject({ code: 'DB_NOT_OPEN' });
});
it('open 后正常返回(行为不变)', async () => {
const eng = new AriaEngine({ storageBackend: 'memory' });
await eng.open(uniqueDB(), 1);
expect(await eng.getTableNames()).toEqual([]);
expect(await eng.hasTable('t')).toBe(false);
expect(await eng.getTableSchema('t')).toBeNull();
await eng.close();
});
});
describe('v0.7.1 — 事务回滚失败不掩盖原始错误', () => {
class RollbackFailsEngine extends MemoryEngine {
async rollbackTransaction(): Promise<void> {
throw new Error('rollback boom');
}
async beginTransaction(): Promise<void> { /* ok */ }
async commitTransaction(): Promise<void> { /* ok */ }
}
it('事务体抛错 + 回滚失败 → 抛原始事务错误', async () => {
const eng = new RollbackFailsEngine();
const mgr = new TransactionManager(eng);
await expect(mgr.execute(async () => {
throw new Error('original tx failure');
})).rejects.toThrow('original tx failure');
});
});
describe('v0.7.1 — __proto__ 列名拒绝', () => {
it('动态构造列映射(null 原型)含 __proto__ 列名被拒绝', () => {
// 对象字面量的 `__proto__:` 在字面量层就触发原型 setter(键不存在);
// 真实攻击面是动态构造的列映射(SQL 路径 / Object.create(null))——键可被
// Object.entries 看到,校验层必须拒绝
const cols: Record<string, unknown> = Object.create(null) as Record<string, unknown>;
cols.id = { type: 'string', primaryKey: true };
cols.__proto__ = { type: 'string' };
expect(() => createSchema('t', cols as never)).toThrow('"__proto__"');
});
it('SQL CREATE TABLE 拒绝 __proto__ 列名(列不静默丢失)', async () => {
const db = await MetonaSqlark.create({ name: uniqueDB(), mode: 'memory' });
await expect(db.query('CREATE TABLE t (id STRING PRIMARY KEY, __proto__ STRING)'))
.rejects.toMatchObject({ code: 'SCHEMA_ERROR' });
await db.close();
});
});