docs: site 站点全量同步 v0.7.4 — 首页徽章/统计(1304测试 76套件 90.1%)+ 流式真惰性说明、API 文档补写语句子查询与 DROP INDEX UNIQUE 语义 + v0.7.4 里程碑、错误码表补 COLUMN_NOT_FOUND/INDEX_NOT_FOUND、演示/基准版本标注、README 已知限制章节、dist 重建(VERSION 0.7.4)
This commit is contained in:
@@ -2,6 +2,72 @@
|
||||
|
||||
All notable changes to MetonaSqlark will be documented in this file.
|
||||
|
||||
## [0.7.4] - 2026-08-15
|
||||
|
||||
### 写语句子查询 / 约束硬化 / 真惰性流式
|
||||
|
||||
> 深度审计第七阶段:修复 UPDATE/DELETE WHERE 子查询静默 0 行(P1,四引擎)、
|
||||
> 主键 NULL 静默入库(P1)、DROP INDEX 解除建表 UNIQUE 约束(P2)等 8 项正确性问题;
|
||||
> Aria findStream 迭代器化落地"真惰性"宣称;REINDEX 单次扫描性能优化。
|
||||
|
||||
### Fixed
|
||||
|
||||
- **UPDATE/DELETE WHERE 子查询静默 0 行(P1,四引擎)** — `executeUpdate/executeDelete`
|
||||
直接 compileStatement 调引擎,`$subquery` 未解析:引擎层 matchWhere 的 `$in/$nin`
|
||||
遇对象恒 false → 所有行不匹配,写语句静默影响 0 行(与 v0.7.3 修的 queryStream
|
||||
同类)。executor 写路径先 `resolveWriteWhere` 解析非关联子查询(IN 列表/标量),
|
||||
EXPLAIN 的 estimatedRows 同步修复;关联引用($col / 关联 EXISTS)显式
|
||||
`NOT_SUPPORTED`(写语句无法逐行绑定外层上下文);QueryBuilder 直通引擎路径
|
||||
由引擎层 `containsUnresolvedSubqueries` 防御(Memory/Aria 双引擎)
|
||||
- **主键 NULL/undefined 静默入库(P1,四引擎)** — 主键列缺失/置 null 时
|
||||
`String(undefined)`→`"undefined"`、`String(null)`→`"null"` 作为主键落库。
|
||||
validateRow 层强制主键非空(SQL 语义 PK 隐含 NOT NULL)抛 `VALIDATION_ERROR`;
|
||||
default 值生效时仍可省略主键(语义不变)
|
||||
- **DROP INDEX 解除建表 UNIQUE 约束(P2)** — `dropIndex` 此前 `colDef.unique = false`
|
||||
静默解除建表约束(aria 侧 persistSchemas 后重启约束永久消失)。现在建表 UNIQUE
|
||||
列 DROP INDEX 抛 `NOT_SUPPORTED`(对齐 SQLite:需重建表解除);仅
|
||||
`CREATE UNIQUE INDEX` 添加的约束可随索引删除(引擎内 `uniqueIndexCols` 跟踪来源,
|
||||
重启后 schema 中的 unique 一律按建表约束保护)
|
||||
- **GROUP BY 把 null 与字符串 'null' 合并(P2)** — 分组键 `String(row[col] ?? 'null')`
|
||||
使两类值合并为一组;DISTINCT/UNION 去重键 `String(v ?? '\0')` 同类吞并。
|
||||
统一类型安全编码(类型前缀),仅同类型同值合并(与 where-matcher 的 === 语义一致)
|
||||
- **UPDATE 未知列静默写入存储行(P2)** — `SET nonexistent = ...` 的脏列残留在行内
|
||||
并随持久化落盘。预检阶段显式 `COLUMN_NOT_FOUND`(SQL + Table API 双路径,
|
||||
Memory/Aria 双引擎)
|
||||
- **queryStream 多语句语义不一致(P2)** — `parseAll(sql)[0]` 静默忽略后续语句:
|
||||
可流式时不执行(如 DELETE),不可流式时回退 query() 却执行全部语句。
|
||||
多语句显式 `PARSE_ERROR`(流式 API 要求单条 SELECT)
|
||||
- **KVStore lastBackgroundError 跨生命周期残留(P3)** — close/open 未清理,
|
||||
重开后首次 checkpoint 抛出上一次生命周期的旧错误。open/close 统一清零
|
||||
- **Hybrid beginTransaction 部分成功泄漏(P3)** — 内存 begin 成功 + 磁盘 begin
|
||||
抛错 → 内存快照泄漏(后续事务永久 TX_ACTIVE)。磁盘失败补偿回滚内存
|
||||
- **MemTable RB-Tree 删除双黑修复边界(P3)** — 两子节点删除时 successor 右子
|
||||
为 null 的占位 parent 传 null → fixDelete 直接跳过修复(树失衡)。
|
||||
修复 parent 传递(直接右子传 successor,间接右子传 successor 原父)
|
||||
- **死代码清理** — 移除 LSM.getAllEntries(无调用者;其 frozen 遍历顺序与 `get()`
|
||||
相反属死代码路径)
|
||||
|
||||
### Changed
|
||||
|
||||
- **Aria findStream 真惰性(宣称落地)** — 此前 `rangeScanLazy` 内部
|
||||
`mergeIter.drain()` 全量物化,与"流式不物化"宣称不符。重写为真惰性:
|
||||
MemTable 红黑树显式栈生成器 + SSTableReader 块级生成器 + GeneratorEntrySource
|
||||
接入 MergeIterator 逐条拉取;callback 返回 false 提前终止(limit 达成时
|
||||
未消费块/子树不再解析),大表流式内存 O(1)
|
||||
- **REINDEX 单次全表扫描(性能)** — `reindexTableInternal` 此前每个索引列一次
|
||||
getAllRows(N 列 × M 行 + 每列一次 drainChain),改为单次扫描重建全部索引列
|
||||
- VERSION 0.7.3 → **0.7.4**
|
||||
|
||||
### Changed(测试)
|
||||
|
||||
- 测试 1256 → **1304**(76 套件,+48 个 v0.7.4 回归);新增 `tests/v074-fixes.test.ts`
|
||||
(写语句子查询 ×10 / 关联引用 ×3 / EXPLAIN ×1 / 主键非空 ×8 / DROP INDEX unique ×5 /
|
||||
分组键编码 ×4 / 未知列 ×4 / queryStream ×2 / KVStore 生命周期 ×1 / Hybrid 补偿 ×1 /
|
||||
真惰性流式 ×2 / REINDEX ×2);VERSION 断言同步
|
||||
- 行覆盖率 90.0% → **90.1%**
|
||||
|
||||
---
|
||||
|
||||
## [0.7.3] - 2026-08-14
|
||||
|
||||
### INSERT 语句级原子性补全 / 索引一致性 / 流式查询回退安全
|
||||
|
||||
+1
-1
@@ -75,7 +75,7 @@ src/
|
||||
├── plugin/ # Plugin system (14 lifecycle hooks)
|
||||
└── integrations/ # React & Vue hooks
|
||||
|
||||
tests/ # Test suite (1256 test cases, 75 suites + 12 e2e)
|
||||
tests/ # Test suite (1304 test cases, 76 suites + 12 e2e)
|
||||
tests/helpers/ # 共享测试工具(OPFS mock 等)
|
||||
tests/e2e/ # Playwright e2e(真实 Chromium + OPFS)
|
||||
site/ # Documentation site (index / docs / demo)
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
# MetonaSqlark
|
||||
|
||||
<p align="center">
|
||||
<img src="https://img.shields.io/badge/version-0.7.3-blue?style=flat-square" alt="version">
|
||||
<img src="https://img.shields.io/badge/version-0.7.4-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/coverage-90.0%25-brightgreen?style=flat-square" alt="coverage">
|
||||
<img src="https://img.shields.io/badge/tests-1256%20passed-success?style=flat-square" alt="tests">
|
||||
<img src="https://img.shields.io/badge/coverage-90.1%25-brightgreen?style=flat-square" alt="coverage">
|
||||
<img src="https://img.shields.io/badge/tests-1304%20passed-success?style=flat-square" alt="tests">
|
||||
</p>
|
||||
|
||||
> 基于 TypeScript 的**前端关系型数据库**:完整 SQL + Query Builder 双 API,
|
||||
@@ -32,9 +32,9 @@
|
||||
|
||||
**数据库能力**
|
||||
|
||||
- **完整 SQL** — SELECT(JOIN / 子查询 / 派生表 / UNION / GROUP BY / HAVING / DISTINCT / CASE WHEN / EXISTS / BETWEEN / NULLS 排序)、INSERT...SELECT、ALTER TABLE、TRUNCATE TABLE、CREATE INDEX、事务语句(BEGIN / COMMIT / ROLLBACK / SAVEPOINT)、维护语句(EXPLAIN / ANALYZE / REINDEX / VACUUM)
|
||||
- **完整 SQL** — SELECT(JOIN / 子查询 / 派生表 / UNION / GROUP BY / HAVING / DISTINCT / CASE WHEN / EXISTS / BETWEEN / NULLS 排序)、INSERT...SELECT、UPDATE/DELETE 子查询、ALTER TABLE、TRUNCATE TABLE、CREATE INDEX、事务语句(BEGIN / COMMIT / ROLLBACK / SAVEPOINT)、维护语句(EXPLAIN / ANALYZE / REINDEX / VACUUM)
|
||||
- **Query Builder** — 链式 `.select().where().innerJoin().orderBy().limit().execute()`,类型安全
|
||||
- **流式查询** — `queryStream` / `table().stream()` 逐行回调,Aria 引擎惰性扫描不物化结果集
|
||||
- **流式查询** — `queryStream` / `table().stream()` 逐行回调,Aria 引擎真惰性扫描(limit 提前终止,不物化结果集)
|
||||
- **事务** — 四引擎事务原子性 + 自动回滚;Aria 用 MVCC 快照隔离,事务读写不互斥
|
||||
- **外键级联** — `ON DELETE` / `ON UPDATE` 支持 `CASCADE` / `SET NULL` / `RESTRICT`(含主键变更级联、级联环路保护)
|
||||
- **数据迁移** — 版本化迁移(持久化到库内,重启不重跑)、导入导出、在线一致性备份
|
||||
@@ -401,7 +401,7 @@ const { data, loading, error, refresh } = useSqlarkQuery(db, 'SELECT * FROM user
|
||||
npm install # 安装依赖
|
||||
npm run dev # 开发模式(localhost:3001)
|
||||
npm run build # 生产构建(生成 dist/)
|
||||
npm test # 运行测试(1256 用例 · 75 套件)
|
||||
npm test # 运行测试(1304 用例 · 76 套件)
|
||||
npm run test:e2e # Playwright e2e(真实 Chromium + OPFS + 崩溃注入,需先 build)
|
||||
npm run lint # 代码检查
|
||||
npm run typecheck # 类型检查
|
||||
@@ -413,13 +413,21 @@ npm run typecheck # 类型检查
|
||||
|
||||
| 指标 | 数值 |
|
||||
|------|------|
|
||||
| 测试用例 | 1256(+12 Playwright e2e) |
|
||||
| 测试套件 | 75 |
|
||||
| 行覆盖率 | 89.8% |
|
||||
| 测试用例 | 1304(+12 Playwright e2e) |
|
||||
| 测试套件 | 76 |
|
||||
| 行覆盖率 | 90.1% |
|
||||
| SQL 关键字 | 72 |
|
||||
| 存储引擎 | 5(Memory / KVStore / OPFS / Hybrid / Aria) |
|
||||
| 运行时依赖 | 0 |
|
||||
|
||||
### 已知限制(v0.7.4)
|
||||
|
||||
- **单列主键** — 复合主键暂不支持(建表时显式 `SCHEMA_ERROR`),列入 v0.8 路线图
|
||||
- **写语句关联引用** — UPDATE/DELETE 的 WHERE 支持非关联子查询(`IN (SELECT)` / 标量子查询),关联引用(`$col` / 关联 EXISTS)显式抛 `NOT_SUPPORTED`(不静默)
|
||||
- **建表 UNIQUE 约束** — 不可经 `DROP INDEX` 解除(对齐 SQLite,需重建表);仅 `CREATE UNIQUE INDEX` 添加的约束可随索引删除
|
||||
- **唯一值交换更新** — 同一语句内两行互换唯一列值(A:x→y, B:y→x)保守拒绝(最终状态合法但报 `UNIQUE_VIOLATION`)
|
||||
- **主键非空** — 主键列强制非空(SQL 语义 PK 隐含 NOT NULL),`INSERT`/`UPDATE` 置 null/undefined 抛 `VALIDATION_ERROR`
|
||||
|
||||
### 浏览器兼容性
|
||||
|
||||
| 浏览器 | 最低版本 | Memory | Disk (KVStore) | Aria (OPFS) |
|
||||
|
||||
Vendored
+323
-58
@@ -34,7 +34,7 @@ class DatabaseError extends Error {
|
||||
// ---------------------------------------------------------------------------
|
||||
// 版本
|
||||
// ---------------------------------------------------------------------------
|
||||
const VERSION = '0.7.3';
|
||||
const VERSION = '0.7.4';
|
||||
|
||||
/**
|
||||
* metona-sqlark Shared WHERE Matcher — 统一的条件匹配逻辑
|
||||
@@ -62,6 +62,40 @@ function compileLikeRegex(pattern) {
|
||||
// ---------------------------------------------------------------------------
|
||||
// WHERE 匹配(顶层入口)
|
||||
// ---------------------------------------------------------------------------
|
||||
/**
|
||||
* v0.7.4: 检测 WHERE 中未解析的子查询/列引用标记($subquery / $col / $exists)。
|
||||
* QueryBuilder 等直通引擎的写路径(update/delete)不经 Executor 解析子查询,
|
||||
* 引擎层 matchWhere 对未解析标记恒 false → 静默影响 0 行。
|
||||
* 写路径预检阶段显式拒绝;SELECT 路径由 Executor 解析(不调用本函数)。
|
||||
*/
|
||||
function containsUnresolvedSubqueries(where) {
|
||||
if (!where)
|
||||
return false;
|
||||
for (const [k, v] of Object.entries(where)) {
|
||||
if (k === '$and' || k === '$or') {
|
||||
if (v.some((sub) => containsUnresolvedSubqueries(sub)))
|
||||
return true;
|
||||
continue;
|
||||
}
|
||||
if (k === '$not') {
|
||||
if (containsUnresolvedSubqueries(v))
|
||||
return true;
|
||||
continue;
|
||||
}
|
||||
if (k === '$exists')
|
||||
return true;
|
||||
if (typeof v === 'object' && v !== null && !Array.isArray(v)) {
|
||||
for (const [, operand] of Object.entries(v)) {
|
||||
if (typeof operand === 'object' && operand !== null) {
|
||||
const ops = operand;
|
||||
if ('$subquery' in ops || '$col' in ops)
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
/**
|
||||
* 匹配完整 WHERE 条件
|
||||
* @param row 当前数据行
|
||||
@@ -352,6 +386,12 @@ class MemoryEngine {
|
||||
this.opened = false;
|
||||
/** v0.4.2-fix: 库内元数据(迁移版本持久化用) */
|
||||
this.metaStore = new Map();
|
||||
/**
|
||||
* v0.7.4: 由 CREATE UNIQUE INDEX 添加的 unique 列(table:col)。
|
||||
* 与建表 UNIQUE 约束区分:DROP INDEX 只允许解除索引来源的 unique,
|
||||
* 建表约束需重建表(对齐 SQLite 语义,此前静默解除且不可恢复)。
|
||||
*/
|
||||
this.uniqueIndexCols = new Set();
|
||||
// ---- 事务快照 ----
|
||||
this.snapshot = null;
|
||||
}
|
||||
@@ -412,6 +452,12 @@ class MemoryEngine {
|
||||
this.schemas.delete(tableName);
|
||||
this.tables.delete(tableName);
|
||||
this.indexes.delete(tableName);
|
||||
// v0.7.4: 清理该表的 unique 索引来源标记(重建同名表不残留)
|
||||
const prefix = `${tableName}:`;
|
||||
for (const key of this.uniqueIndexCols) {
|
||||
if (key.startsWith(prefix))
|
||||
this.uniqueIndexCols.delete(key);
|
||||
}
|
||||
}
|
||||
async hasTable(tableName) { return this.schemas.has(tableName); }
|
||||
async getTableNames() { return Array.from(this.schemas.keys()); }
|
||||
@@ -546,6 +592,18 @@ class MemoryEngine {
|
||||
const pkCol = this.getPrimaryKey(schema);
|
||||
// v0.7.2: undefined 值视为"不更新该列"(保留旧值),null 显式置空
|
||||
const cleanUpdates = stripUndefinedUpdates(updates);
|
||||
// v0.7.4: 防御 —— QueryBuilder 直通引擎不经 Executor 子查询解析,
|
||||
// 未解析的 $subquery/$col/$exists 在 matchWhere 中恒 false → 静默 0 行
|
||||
if (containsUnresolvedSubqueries(query.where)) {
|
||||
throw new DatabaseError('Unresolved subqueries/column references in UPDATE WHERE (use db.query() to execute subqueries)', 'NOT_SUPPORTED');
|
||||
}
|
||||
// v0.7.4: 未知列显式报错 —— 此前 SET nonexistent = ... 被静默写入存储行
|
||||
// (validateRow 只遍历 schema 列,脏列残留在行内并随持久化落盘)
|
||||
for (const col of Object.keys(cleanUpdates)) {
|
||||
if (!schema.columns[col]) {
|
||||
throw new DatabaseError(`Column "${col}" does not exist in table "${tableName}"`, 'COLUMN_NOT_FOUND');
|
||||
}
|
||||
}
|
||||
// v0.7.2: 语句级原子性 — 两阶段(先全量预检,后执行)。
|
||||
// 此前逐行"校验+写入":第 N 行唯一冲突/校验失败抛错时,前 N-1 行已写入
|
||||
// → 无事务下语句级部分提交(数据半更新且调用方已收到错误)。
|
||||
@@ -718,6 +776,11 @@ class MemoryEngine {
|
||||
}
|
||||
async delete(tableName, query) {
|
||||
this.ensureTable(tableName);
|
||||
// v0.7.4: 防御 —— QueryBuilder 直通引擎不经 Executor 子查询解析,
|
||||
// 未解析的 $subquery/$col/$exists 在 matchWhere 中恒 false → 静默 0 行
|
||||
if (containsUnresolvedSubqueries(query.where)) {
|
||||
throw new DatabaseError('Unresolved subqueries/column references in DELETE WHERE (use db.query() to execute subqueries)', 'NOT_SUPPORTED');
|
||||
}
|
||||
const table = this.tables.get(tableName);
|
||||
const toDelete = [];
|
||||
for (const [pk, row] of table) {
|
||||
@@ -847,8 +910,11 @@ class MemoryEngine {
|
||||
throw error;
|
||||
}
|
||||
colDef.index = true;
|
||||
if (unique)
|
||||
if (unique) {
|
||||
colDef.unique = true;
|
||||
// v0.7.4: 记录唯一约束来源(DROP INDEX 时可解除;建表约束不可)
|
||||
this.uniqueIndexCols.add(`${tableName}:${column}`);
|
||||
}
|
||||
}
|
||||
async dropIndex(tableName, column, _indexName) {
|
||||
// v0.7.2: 同 createIndex —— 列级标志修改无法通过事务快照回滚,显式拒绝
|
||||
@@ -864,8 +930,17 @@ class MemoryEngine {
|
||||
if (!colDef.index && !colDef.unique) {
|
||||
throw new DatabaseError(`Index on column "${column}" does not exist in table "${tableName}"`, 'INDEX_NOT_FOUND');
|
||||
}
|
||||
// v0.7.4: 建表 UNIQUE 约束不可通过 DROP INDEX 解除 —— 此前 colDef.unique = false
|
||||
// 静默解除约束(后续唯一性检查失效、重复数据入库)。对齐 SQLite 语义:
|
||||
// 约束随建表存在,解除需重建表;仅 CREATE UNIQUE INDEX 添加的约束可随索引删除。
|
||||
const uniqueKey = `${tableName}:${column}`;
|
||||
if (colDef.unique && !this.uniqueIndexCols.has(uniqueKey)) {
|
||||
throw new DatabaseError(`Cannot drop index on column "${column}" in table "${tableName}": ` +
|
||||
'UNIQUE constraint defined at table creation must be removed by recreating the table', 'NOT_SUPPORTED');
|
||||
}
|
||||
colDef.index = false;
|
||||
colDef.unique = false;
|
||||
this.uniqueIndexCols.delete(uniqueKey);
|
||||
const tableIndexes = this.indexes.get(tableName);
|
||||
if (tableIndexes)
|
||||
tableIndexes.delete(column);
|
||||
@@ -939,6 +1014,11 @@ class MemoryEngine {
|
||||
if (colDef.required && (value === undefined || value === null)) {
|
||||
throw new DatabaseError(`Column "${colName}" is required in table "${schema.name}"`, 'VALIDATION_ERROR');
|
||||
}
|
||||
// v0.7.4: 主键列强制非空(SQL 语义 PK 隐含 NOT NULL)——
|
||||
// 此前 null/undefined 主键被 String() 化为 "null"/"undefined" 静默入库
|
||||
if (colDef.primaryKey && (value === undefined || value === null)) {
|
||||
throw new DatabaseError(`Primary key column "${colName}" in table "${schema.name}" cannot be null or undefined`, 'VALIDATION_ERROR');
|
||||
}
|
||||
if (value !== undefined && value !== null)
|
||||
this.checkType(colName, colDef.type, value);
|
||||
if (value !== undefined)
|
||||
@@ -1771,6 +1851,8 @@ class KVStore {
|
||||
if (this.opened)
|
||||
return;
|
||||
this.dbName = dbName;
|
||||
// v0.7.4: 打开时同样清理后台错误状态(防 close/reopen 残留)
|
||||
this.lastBackgroundError = null;
|
||||
await this.medium.open(dbName);
|
||||
this.index = new Map();
|
||||
this.seq = 0;
|
||||
@@ -1853,6 +1935,9 @@ class KVStore {
|
||||
this.index.clear();
|
||||
this.seq = 0;
|
||||
this.logBytes = 0;
|
||||
// v0.7.4: 清理后台错误状态 —— 此前跨 close/reopen 残留,
|
||||
// 重开后首次 checkpoint 会抛出上一次生命周期的旧错误
|
||||
this.lastBackgroundError = null;
|
||||
this.opened = false;
|
||||
}
|
||||
// =======================================================================
|
||||
@@ -2927,6 +3012,37 @@ class RedBlackTree {
|
||||
rangeScan(startKey, endKey, callback) {
|
||||
this._rangeScan(this.root, startKey, endKey, callback);
|
||||
}
|
||||
/**
|
||||
* v0.7.4: 惰性范围遍历(显式栈中序迭代 + 边界剪枝)。
|
||||
* 真流式扫描:生成器按需产出,提前终止(limit 达成)时剩余子树不再遍历。
|
||||
*/
|
||||
*scanLazy(startKey, endKey) {
|
||||
const stack = [];
|
||||
// 定位到 >= startKey 的最左节点(沿路入栈)
|
||||
let cur = this.root;
|
||||
while (cur) {
|
||||
if (cur.key >= startKey) {
|
||||
stack.push(cur);
|
||||
cur = cur.left;
|
||||
}
|
||||
else {
|
||||
cur = cur.right;
|
||||
}
|
||||
}
|
||||
while (stack.length > 0) {
|
||||
const node = stack.pop();
|
||||
// 中序递增:越过 endKey 后所有剩余节点均越界
|
||||
if (node.key > endKey)
|
||||
break;
|
||||
if (node.key >= startKey)
|
||||
yield [node.key, node.value];
|
||||
cur = node.right;
|
||||
while (cur) {
|
||||
stack.push(cur);
|
||||
cur = cur.left;
|
||||
}
|
||||
}
|
||||
}
|
||||
/** 获取所有条目 */
|
||||
getAllEntries() {
|
||||
const entries = [];
|
||||
@@ -2974,8 +3090,14 @@ class RedBlackTree {
|
||||
}
|
||||
else {
|
||||
const successor = this.minimum(node.right);
|
||||
if (successor.parent !== node) {
|
||||
this.transplant(successor, successor.right);
|
||||
// v0.7.4: 在 transplant 重连前捕获 successor 原右子与父 ——
|
||||
// x(双黑修复起点)= successor 原右子(占位在 successor 原位置)。
|
||||
// 此前 `successor.right?.parent ?? null` 在重赋值后取值:x 指向 node 右子树,
|
||||
// 且 null 时 parent 为 null → fixDelete 直接跳过修复(删除黑色节点后失衡)。
|
||||
const successorRight = successor.right;
|
||||
const successorParent = successor.parent;
|
||||
if (successorParent !== node) {
|
||||
this.transplant(successor, successorRight);
|
||||
successor.right = node.right;
|
||||
successor.right.parent = successor;
|
||||
}
|
||||
@@ -2984,8 +3106,13 @@ class RedBlackTree {
|
||||
successor.left.parent = successor;
|
||||
const origColor = successor.color;
|
||||
successor.color = node.color;
|
||||
if (origColor === Color.BLACK)
|
||||
this.fixDelete(successor.right, successor.right?.parent ?? null);
|
||||
if (origColor === Color.BLACK) {
|
||||
const x = successorRight;
|
||||
// x 为 null 占位:直接右子时其父为 successor(已移到 node 位置),
|
||||
// 间接右子时其父为 successor 原父(transplant 已把 x 接到其下)
|
||||
const xParent = x ? x.parent : (successorParent === node ? successor : successorParent);
|
||||
this.fixDelete(x, xParent);
|
||||
}
|
||||
}
|
||||
}
|
||||
transplant(u, v) {
|
||||
@@ -3250,6 +3377,10 @@ class MemTable {
|
||||
this.tree.rangeScan(startKey, endKey, (k, v) => entries.push([k, v]));
|
||||
return entries;
|
||||
}
|
||||
/** v0.7.4: 惰性范围扫描(真流式,逐条产出) */
|
||||
scanLazy(startKey, endKey) {
|
||||
return this.tree.scanLazy(startKey, endKey);
|
||||
}
|
||||
/** 条目数 */
|
||||
getEntryCount() {
|
||||
return this.tree.size;
|
||||
@@ -3680,6 +3811,16 @@ class SSTableReader {
|
||||
}
|
||||
/** 范围扫描 */
|
||||
rangeScan(startKey, endKey, callback) {
|
||||
// v0.7.4: 包装惰性生成器(行为一致,消除双份解析循环)
|
||||
for (const [key, value] of this.scanLazy(startKey, endKey)) {
|
||||
callback(key, value);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* v0.7.4: 惰性范围扫描 —— 生成器逐块逐条产出(真流式)。
|
||||
* 提前终止时未消费的块不再解析,大表流式内存 O(1)。
|
||||
*/
|
||||
*scanLazy(startKey, endKey) {
|
||||
if (this.indexEntries.length === 0)
|
||||
return;
|
||||
const startBlockIdx = Math.max(0, this.locateBlockGE(startKey));
|
||||
@@ -3694,7 +3835,7 @@ class SSTableReader {
|
||||
for (let bi = startBlockIdx; bi <= endBlockIdx && bi >= 0; bi++) {
|
||||
const entry = this.indexEntries[bi];
|
||||
const blockData = this.getBlockData(entry);
|
||||
// v0.4.1-fix: 残缺块跳过(rangeScan 继续后续块,不抛异常)
|
||||
// v0.4.1-fix: 残缺块跳过(继续后续块,不抛异常)
|
||||
if (!blockData)
|
||||
continue;
|
||||
const blockView = new DataView(blockData.buffer, blockData.byteOffset, blockData.byteLength);
|
||||
@@ -3718,7 +3859,7 @@ class SSTableReader {
|
||||
if (key >= startKey && key <= endKey) {
|
||||
try {
|
||||
const value = JSON.parse(new TextDecoder().decode(valBytes));
|
||||
callback(key, value);
|
||||
yield [key, value];
|
||||
}
|
||||
catch {
|
||||
// skip corrupted entry
|
||||
@@ -3907,6 +4048,23 @@ class ArrayEntrySource {
|
||||
this.index = 0;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* v0.7.4: 生成器数据源 —— 惰性迭代(真流式扫描)。
|
||||
* MergeIterator 的 next() 逐条拉取,生成器按需产出(findStream 提前终止时
|
||||
* 未消费部分不再物化,大表流式内存 O(1))。
|
||||
*/
|
||||
class GeneratorEntrySource {
|
||||
constructor(iter) {
|
||||
this.iter = iter;
|
||||
}
|
||||
next() {
|
||||
const r = this.iter.next();
|
||||
return r.done ? null : r.value;
|
||||
}
|
||||
reset() {
|
||||
// 生成器不可重置;MergeIterator 无 reset 消费方,接口保留
|
||||
}
|
||||
}
|
||||
/** 最小堆 */
|
||||
class MinHeap {
|
||||
constructor() {
|
||||
@@ -4373,16 +4531,21 @@ class LSM {
|
||||
}
|
||||
rangeScan(startKey, endKey) {
|
||||
const result = [];
|
||||
this.rangeScanLazy(startKey, endKey, (k, v) => result.push([k, v]));
|
||||
this.rangeScanLazy(startKey, endKey, (k, v) => { result.push([k, v]); });
|
||||
return result;
|
||||
}
|
||||
/** 惰性范围扫描:通过回调逐条返回,不一次性物化所有源 */
|
||||
/**
|
||||
* 惰性范围扫描:通过回调逐条返回,不一次性物化。
|
||||
* v0.7.4: 真惰性 —— 各源(MemTable/frozen/SSTable)以生成器接入 MergeIterator,
|
||||
* 逐条拉取;回调返回 false 时提前终止(未消费部分不再解析/物化)。
|
||||
* 此前实现内部 mergeIter.drain() 全量物化,与"流式不物化"宣称不符。
|
||||
*/
|
||||
rangeScanLazy(startKey, endKey, callback) {
|
||||
const mergeIter = new MergeIterator();
|
||||
mergeIter.addSource(new ArrayEntrySource(this.memtable.rangeScan(startKey, endKey)));
|
||||
mergeIter.addSource(new GeneratorEntrySource(this.memtable.scanLazy(startKey, endKey)));
|
||||
// pending frozen memtables(从新到旧,新数据 sourceIndex 更小)
|
||||
for (let i = this.frozenMemtables.length - 1; i >= 0; i--) {
|
||||
mergeIter.addSource(new ArrayEntrySource(this.frozenMemtables[i].rangeScan(startKey, endKey)));
|
||||
mergeIter.addSource(new GeneratorEntrySource(this.frozenMemtables[i].scanLazy(startKey, endKey)));
|
||||
}
|
||||
for (let level = 0; level < MAX_LSM_LEVELS; level++) {
|
||||
for (const meta of this.levels[level]) {
|
||||
@@ -4391,42 +4554,21 @@ class LSM {
|
||||
const reader = this.loadSSTableReader(meta);
|
||||
if (!reader)
|
||||
continue;
|
||||
reader.rangeScan(startKey, endKey, (k, v) => {
|
||||
mergeIter.addSource(new ArrayEntrySource([[k, v]]));
|
||||
});
|
||||
mergeIter.addSource(new GeneratorEntrySource(reader.scanLazy(startKey, endKey)));
|
||||
}
|
||||
}
|
||||
const merged = mergeIter.drain();
|
||||
for (const [k, v] of merged) {
|
||||
let entry = mergeIter.next();
|
||||
while (entry) {
|
||||
const [k, v] = entry;
|
||||
if (!v.__tombstone) {
|
||||
callback(k, v);
|
||||
const cont = callback(k, v);
|
||||
// v0.7.4: 提前终止(流式 limit 达成)
|
||||
if (cont === false)
|
||||
return;
|
||||
}
|
||||
entry = mergeIter.next();
|
||||
}
|
||||
}
|
||||
getAllEntries() {
|
||||
const result = new Map();
|
||||
// 从最旧层级开始聚合;同层级内从最旧到最新遍历,
|
||||
// 保证 result.set 覆盖时最终保留最新值
|
||||
for (let level = MAX_LSM_LEVELS - 1; level >= 0; level--) {
|
||||
for (let i = this.levels[level].length - 1; i >= 0; i--) {
|
||||
const meta = this.levels[level][i];
|
||||
const reader = this.loadSSTableReader(meta);
|
||||
if (!reader)
|
||||
continue;
|
||||
reader.scanAll((k, v) => result.set(k, v));
|
||||
}
|
||||
}
|
||||
// MemTable 覆盖(最新)
|
||||
for (const [k, v] of this.memtable.getAllEntries()) {
|
||||
result.set(k, v);
|
||||
}
|
||||
for (let i = this.frozenMemtables.length - 1; i >= 0; i--) {
|
||||
for (const [k, v] of this.frozenMemtables[i].getAllEntries()) {
|
||||
result.set(k, v);
|
||||
}
|
||||
}
|
||||
return Array.from(result.entries()).filter(([, v]) => !v.__tombstone);
|
||||
}
|
||||
// =======================================================================
|
||||
// Compaction
|
||||
// =======================================================================
|
||||
@@ -6488,6 +6630,13 @@ class AriaEngine {
|
||||
this.opCounter = 0;
|
||||
// 二级索引:table.colKey → LSM
|
||||
this.secondaryIndexes = new Map();
|
||||
/**
|
||||
* v0.7.4: 由 CREATE UNIQUE INDEX 添加的 unique 列(table:col)。
|
||||
* 与建表 UNIQUE 约束区分:DROP INDEX 只允许解除索引来源的 unique,
|
||||
* 建表约束需重建表(对齐 SQLite 语义,此前静默解除且重启后永久消失)。
|
||||
* 注:重启后无法区分历史来源,schema 中的 unique 一律按建表约束保护(保守)。
|
||||
*/
|
||||
this.uniqueIndexCols = new Set();
|
||||
// MVCC 事务
|
||||
this.mvcc = new MVCCManager();
|
||||
this.currentTxnId = null;
|
||||
@@ -6700,6 +6849,7 @@ class AriaEngine {
|
||||
this.schemas.clear();
|
||||
this.tablePKs.clear();
|
||||
this.secondaryIndexes.clear();
|
||||
this.uniqueIndexCols.clear();
|
||||
this.mvcc = new MVCCManager();
|
||||
this.currentTxnId = null;
|
||||
this.txnSnapshot = null;
|
||||
@@ -6796,6 +6946,7 @@ class AriaEngine {
|
||||
this.schemas.clear();
|
||||
this.tablePKs.clear();
|
||||
this.secondaryIndexes.clear();
|
||||
this.uniqueIndexCols.clear();
|
||||
this.lsm.clear();
|
||||
this.mvcc = new MVCCManager();
|
||||
this.currentTxnId = null;
|
||||
@@ -6870,6 +7021,12 @@ class AriaEngine {
|
||||
// v0.4.2-fix: 清理该表的全部二级索引 LSM 与持久化文件 —
|
||||
// 此前残留孤儿索引,重建同名表后旧索引数据污染新表(索引查询返回错误行)
|
||||
await this.cleanupTableIndexes(tableName);
|
||||
// v0.7.4: 清理该表的 unique 索引来源标记
|
||||
const uPrefix = `${tableName}:`;
|
||||
for (const k of this.uniqueIndexCols) {
|
||||
if (k.startsWith(uPrefix))
|
||||
this.uniqueIndexCols.delete(k);
|
||||
}
|
||||
this.schemas.delete(tableName);
|
||||
this.tablePKs.delete(tableName);
|
||||
await this.persistSchemas();
|
||||
@@ -7054,6 +7211,11 @@ class AriaEngine {
|
||||
async update(tableName, query, updates) {
|
||||
this.ensureOpen();
|
||||
this.ensureTable(tableName);
|
||||
// v0.7.4: 防御 —— QueryBuilder 直通引擎不经 Executor 子查询解析,
|
||||
// 未解析的 $subquery/$col/$exists 在 matchWhere 中恒 false → 静默 0 行
|
||||
if (containsUnresolvedSubqueries(query.where)) {
|
||||
throw new DatabaseError('Unresolved subqueries/column references in UPDATE WHERE (use db.query() to execute subqueries)', 'NOT_SUPPORTED');
|
||||
}
|
||||
const schema = this.schemas.get(tableName);
|
||||
const rows = await this.getAllRows(tableName);
|
||||
let count = 0;
|
||||
@@ -7063,6 +7225,13 @@ class AriaEngine {
|
||||
const visited = new Set();
|
||||
// v0.7.2: undefined 值视为"不更新该列"(保留旧值),null 显式置空
|
||||
const cleanUpdates = stripUndefinedUpdates(updates);
|
||||
// v0.7.4: 未知列显式报错 —— 此前 SET nonexistent = ... 被静默写入存储行
|
||||
// (validateRow 只遍历 schema 列,脏列残留在行内并随 SSTable 持久化)
|
||||
for (const col of Object.keys(cleanUpdates)) {
|
||||
if (!schema.columns[col]) {
|
||||
throw new DatabaseError(`Column "${col}" does not exist in table "${tableName}"`, 'COLUMN_NOT_FOUND');
|
||||
}
|
||||
}
|
||||
// v0.6.2: 唯一约束 — 批量预加载本批更新涉及的唯一列索引范围(一次 drainChain)
|
||||
const uniqueCols = this.uniqueColumns(tableName, schema);
|
||||
for (const colName of uniqueCols) {
|
||||
@@ -7271,6 +7440,11 @@ class AriaEngine {
|
||||
async delete(tableName, query) {
|
||||
this.ensureOpen();
|
||||
this.ensureTable(tableName);
|
||||
// v0.7.4: 防御 —— QueryBuilder 直通引擎不经 Executor 子查询解析,
|
||||
// 未解析的 $subquery/$col/$exists 在 matchWhere 中恒 false → 静默 0 行
|
||||
if (containsUnresolvedSubqueries(query.where)) {
|
||||
throw new DatabaseError('Unresolved subqueries/column references in DELETE WHERE (use db.query() to execute subqueries)', 'NOT_SUPPORTED');
|
||||
}
|
||||
const rows = await this.getAllRows(tableName);
|
||||
let count = 0;
|
||||
// v0.3.1: 批量 WAL 写入(组提交)
|
||||
@@ -7483,14 +7657,15 @@ class AriaEngine {
|
||||
}
|
||||
return count;
|
||||
}
|
||||
// 全表惰性扫描(含 WHERE 过滤,不物化)
|
||||
// 全表惰性扫描(含 WHERE 过滤,不物化;v0.7.4: callback 返回 false 提前终止,
|
||||
// 未消费的 SSTable 块 / 子树不再解析 —— 真流式,大表 limit 内存 O(1))
|
||||
await this.lsm.prefetchRange(prefix, `${prefix}\uffff`);
|
||||
this.lsm.rangeScanLazy(prefix, `${prefix}\uffff`, (key, value) => {
|
||||
if (count >= limit)
|
||||
return;
|
||||
return false;
|
||||
const row = { ...value };
|
||||
row[pkCol] = key.slice(prefix.length);
|
||||
emit(row);
|
||||
return emit(row);
|
||||
});
|
||||
return count;
|
||||
}
|
||||
@@ -7652,8 +7827,11 @@ class AriaEngine {
|
||||
throw error;
|
||||
}
|
||||
colDef.index = true;
|
||||
if (unique)
|
||||
if (unique) {
|
||||
colDef.unique = true;
|
||||
// v0.7.4: 记录唯一约束来源(DROP INDEX 时可解除;建表约束不可)
|
||||
this.uniqueIndexCols.add(`${tableName}:${column}`);
|
||||
}
|
||||
await this.persistSchemas();
|
||||
}
|
||||
async dropIndex(tableName, column, _indexName) {
|
||||
@@ -7672,8 +7850,17 @@ class AriaEngine {
|
||||
if (!colDef.index && !colDef.unique && !this.secondaryIndexes.has(`${tableName}:idx:${column}`)) {
|
||||
throw new DatabaseError(`Index on column "${column}" does not exist in table "${tableName}"`, 'INDEX_NOT_FOUND');
|
||||
}
|
||||
// v0.7.4: 建表 UNIQUE 约束不可通过 DROP INDEX 解除 —— 此前 colDef.unique = false
|
||||
// 静默解除约束(重启后 persistSchemas 使约束永久消失)。对齐 SQLite 语义:
|
||||
// 约束随建表存在,解除需重建表;仅 CREATE UNIQUE INDEX 添加的约束可随索引删除。
|
||||
const uniqueKey = `${tableName}:${column}`;
|
||||
if (colDef.unique && !this.uniqueIndexCols.has(uniqueKey)) {
|
||||
throw new DatabaseError(`Cannot drop index on column "${column}" in table "${tableName}": ` +
|
||||
'UNIQUE constraint defined at table creation must be removed by recreating the table', 'NOT_SUPPORTED');
|
||||
}
|
||||
colDef.index = false;
|
||||
colDef.unique = false;
|
||||
this.uniqueIndexCols.delete(uniqueKey);
|
||||
const idxKey = `${tableName}:idx:${column}`;
|
||||
const idxLsm = this.secondaryIndexes.get(idxKey);
|
||||
if (idxLsm) {
|
||||
@@ -7892,6 +8079,11 @@ class AriaEngine {
|
||||
if (colDef.required && (value === undefined || value === null)) {
|
||||
throw new DatabaseError(`Column "${colName}" is required in table "${schema.name}"`, 'VALIDATION_ERROR');
|
||||
}
|
||||
// v0.7.4: 主键列强制非空(SQL 语义 PK 隐含 NOT NULL)——
|
||||
// 此前 null/undefined 主键被 String() 化为 "null"/"undefined" 静默入库
|
||||
if (colDef.primaryKey && (value === undefined || value === null)) {
|
||||
throw new DatabaseError(`Primary key column "${colName}" in table "${schema.name}" cannot be null or undefined`, 'VALIDATION_ERROR');
|
||||
}
|
||||
if (value !== undefined && value !== null) {
|
||||
this.checkType(colName, colDef.type, value, colDef);
|
||||
}
|
||||
@@ -8420,10 +8612,15 @@ class AriaEngine {
|
||||
if (!schema)
|
||||
return 0;
|
||||
let rebuiltCount = 0;
|
||||
for (const [colName, colDef] of Object.entries(schema.columns)) {
|
||||
// v0.3.3: 主键列不建冗余二级索引(主 LSM 即 PK 索引)
|
||||
if (!colDef.index && !colDef.unique)
|
||||
continue;
|
||||
// v0.7.4-perf: 单次全表扫描重建全部索引列 —— 此前每个索引列各做一次
|
||||
// getAllRows(N 列 × M 行全表扫描 + 每次 prefetchRange drainChain),
|
||||
// 多索引大表 REINDEX/崩溃恢复按索引列数线性放大
|
||||
const pkCol = this.tablePKs.get(tableName);
|
||||
const idxCols = Object.entries(schema.columns).filter(([, colDef]) => colDef.index || colDef.unique);
|
||||
if (idxCols.length === 0)
|
||||
return 0;
|
||||
const rows = await this.getAllRows(tableName);
|
||||
for (const [colName] of idxCols) {
|
||||
const idxKey = `${tableName}:idx:${colName}`;
|
||||
const idxLsm = this.secondaryIndexes.get(idxKey);
|
||||
if (!idxLsm)
|
||||
@@ -8432,11 +8629,10 @@ class AriaEngine {
|
||||
await idxLsm.clear();
|
||||
rebuiltCount++;
|
||||
// 从主 LSM 重建索引
|
||||
const rows = await this.getAllRows(tableName);
|
||||
for (const row of rows) {
|
||||
const val = row[colName];
|
||||
if (val !== undefined && val !== null) {
|
||||
idxLsm.put(`${String(val)}:${row[this.tablePKs.get(tableName)]}`, { pk: row[this.tablePKs.get(tableName)] });
|
||||
idxLsm.put(`${String(val)}:${row[pkCol]}`, { pk: row[pkCol] });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8733,7 +8929,15 @@ class HybridEngine {
|
||||
// ---- 事务 ----
|
||||
async beginTransaction() {
|
||||
await this.memoryEngine.beginTransaction();
|
||||
await this.diskEngine.beginTransaction();
|
||||
// v0.7.4: 磁盘 begin 失败时补偿回滚内存快照 —— 此前内存已 begin、
|
||||
// 磁盘抛错 → 内存快照泄漏(后续所有事务报 TX_ACTIVE)
|
||||
try {
|
||||
await this.diskEngine.beginTransaction();
|
||||
}
|
||||
catch (error) {
|
||||
await this.memoryEngine.rollbackTransaction();
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
async commitTransaction() {
|
||||
// 先写磁盘,保证持久化优先;磁盘失败则回滚内存
|
||||
@@ -10694,6 +10898,30 @@ function parseWhereCondition(sql) {
|
||||
*
|
||||
* JOIN / GROUP BY / DISTINCT 逻辑在此层处理。
|
||||
*/
|
||||
// ---------------------------------------------------------------------------
|
||||
// 分组 / 去重键编码(v0.7.4)
|
||||
// ---------------------------------------------------------------------------
|
||||
/**
|
||||
* v0.7.4: 分组/去重键的类型安全编码 —— 此前 `String(v ?? 'null')` 使
|
||||
* null 与字符串 'null' 合并为一组(GROUP BY 静默少组),`String(v ?? '\0')`
|
||||
* 使 null/undefined/'\0' 在 DISTINCT/UNION 中互相吞并。类型前缀编码后
|
||||
* 各类型独立,仅同类型同值合并(与 where-matcher 的 === 语义一致)。
|
||||
*/
|
||||
function encodeGroupKey(v) {
|
||||
if (v === null)
|
||||
return 'n';
|
||||
if (v === undefined)
|
||||
return 'u';
|
||||
if (typeof v === 'string')
|
||||
return `s${v}`;
|
||||
if (typeof v === 'number')
|
||||
return `d${v}`;
|
||||
if (typeof v === 'boolean')
|
||||
return `b${v}`;
|
||||
if (typeof v === 'object')
|
||||
return `o${JSON.stringify(v)}`;
|
||||
return `x${String(v)}`;
|
||||
}
|
||||
/** 解析 "CASE WHEN c1 THEN v1 WHEN c2 THEN v2 ELSE v3 END [AS alias]" */
|
||||
function parseCaseExpression(expr) {
|
||||
const m = expr.match(/^\s*CASE\s+([\s\S]*?)\s+END\s*(?:AS\s+(\w+))?\s*$/i);
|
||||
@@ -10801,7 +11029,7 @@ class QueryExecutor {
|
||||
const seen = new Set();
|
||||
const result = [];
|
||||
for (const row of normalized) {
|
||||
const key = Object.values(row).map((v) => String(v ?? '\0')).join('\x1f');
|
||||
const key = Object.values(row).map(encodeGroupKey).join('\x1f');
|
||||
if (!seen.has(key)) {
|
||||
seen.add(key);
|
||||
result.push(row);
|
||||
@@ -10809,7 +11037,7 @@ class QueryExecutor {
|
||||
}
|
||||
for (const row of rightRows) {
|
||||
const projected = this.projectUnionRow(row, leftCols);
|
||||
const key = Object.values(projected).map((v) => String(v ?? '\0')).join('\x1f');
|
||||
const key = Object.values(projected).map(encodeGroupKey).join('\x1f');
|
||||
if (!seen.has(key)) {
|
||||
seen.add(key);
|
||||
result.push(projected);
|
||||
@@ -10848,7 +11076,10 @@ class QueryExecutor {
|
||||
}
|
||||
else if (stmt.query.type === 'UPDATE' || stmt.query.type === 'DELETE') {
|
||||
try {
|
||||
// v0.7.4: 子查询解析后估算 —— 此前 $subquery 未解析使 count 恒 0
|
||||
await this.resolveWriteWhere(stmt.query);
|
||||
const plan = compileStatement(stmt.query);
|
||||
plan.where = stmt.query.where;
|
||||
rows = await this.engine.count(plan.table, plan);
|
||||
}
|
||||
catch {
|
||||
@@ -11254,7 +11485,9 @@ class QueryExecutor {
|
||||
executeGroupBy(rows, stmt) {
|
||||
const groups = new Map();
|
||||
for (const row of rows) {
|
||||
const key = stmt.groupBy.map((col) => String(row[col] ?? 'null')).join('|');
|
||||
// v0.7.4: 类型安全键编码 —— 此前 String(row[col] ?? 'null') 使
|
||||
// null 与字符串 'null' 合并为一组(GROUP BY 静默少组)
|
||||
const key = stmt.groupBy.map((col) => encodeGroupKey(row[col])).join('\x1f');
|
||||
if (!groups.has(key))
|
||||
groups.set(key, []);
|
||||
groups.get(key).push(row);
|
||||
@@ -11325,7 +11558,8 @@ class QueryExecutor {
|
||||
executeDistinct(rows) {
|
||||
const seen = new Set();
|
||||
return rows.filter((row) => {
|
||||
const key = Object.values(row).map((v) => String(v ?? '\0')).join('\x1f');
|
||||
// v0.7.4: 类型安全键编码(null 与 'null' 字符串、'\0' 分离)
|
||||
const key = Object.values(row).map(encodeGroupKey).join('\x1f');
|
||||
if (seen.has(key))
|
||||
return false;
|
||||
seen.add(key);
|
||||
@@ -11381,13 +11615,37 @@ class QueryExecutor {
|
||||
return this.engine.insert(stmt.into, rows);
|
||||
}
|
||||
async executeUpdate(stmt) {
|
||||
// v0.7.4: 先解析 WHERE 子查询 —— 此前直接 compileStatement 调引擎:
|
||||
// 引擎层 matchWhere 的 $in/$nin 遇未解析的 $subquery 对象恒 false →
|
||||
// 所有行不匹配,UPDATE 静默影响 0 行(与 queryStream v0.7.3 修复同类)。
|
||||
await this.resolveWriteWhere(stmt);
|
||||
const plan = compileStatement(stmt);
|
||||
plan.where = stmt.where;
|
||||
return this.engine.update(plan.table, plan, stmt.sets);
|
||||
}
|
||||
async executeDelete(stmt) {
|
||||
// v0.7.4: 同 executeUpdate —— DELETE 子查询 WHERE 此前静默删除 0 行
|
||||
await this.resolveWriteWhere(stmt);
|
||||
const plan = compileStatement(stmt);
|
||||
plan.where = stmt.where;
|
||||
return this.engine.delete(plan.table, plan);
|
||||
}
|
||||
/**
|
||||
* v0.7.4: 写语句(UPDATE/DELETE)WHERE 的子查询解析。
|
||||
* 非关联子查询($subquery)解析为具体值列表/标量;
|
||||
* 关联引用($col / 关联 EXISTS)在写语句中无法逐行绑定外层上下文
|
||||
* (引擎层 matchWhere 无 $col 绑定选项)→ 显式 NOT_SUPPORTED 而非静默 0 行。
|
||||
*/
|
||||
async resolveWriteWhere(stmt) {
|
||||
const where = stmt.where;
|
||||
if (!where || Object.keys(where).length === 0)
|
||||
return where ?? {};
|
||||
if (this.hasCorrelatedRefs(where)) {
|
||||
throw new DatabaseError('Correlated subqueries and column references are not supported in UPDATE/DELETE WHERE clauses', 'NOT_SUPPORTED');
|
||||
}
|
||||
stmt.where = await this.resolveSubqueries(where);
|
||||
return stmt.where;
|
||||
}
|
||||
async executeCreateTable(stmt) {
|
||||
// IF NOT EXISTS: 表已存在时静默返回
|
||||
if (stmt.ifNotExists) {
|
||||
@@ -12502,7 +12760,14 @@ class MetonaSqlark {
|
||||
*/
|
||||
async queryStream(sql, onRow) {
|
||||
this.ensureReady();
|
||||
const stmt = parseAll(sql)[0];
|
||||
// v0.7.4: 多语句显式拒绝 —— 此前 parseAll(sql)[0] 静默忽略后续语句:
|
||||
// 可流式时后续语句(如 DELETE)不执行,不可流式时回退 query() 却会执行
|
||||
// 全部语句 → 同一条 SQL 两种语义。流式 API 要求单条 SELECT。
|
||||
const statements = parseAll(sql);
|
||||
if (statements.length !== 1) {
|
||||
throw new DatabaseError('queryStream requires exactly one SELECT statement', 'PARSE_ERROR');
|
||||
}
|
||||
const stmt = statements[0];
|
||||
if (!stmt || stmt.type !== 'SELECT') {
|
||||
throw new DatabaseError('queryStream only supports SELECT statements', 'NOT_SUPPORTED');
|
||||
}
|
||||
|
||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+21
-1
@@ -164,7 +164,7 @@ interface MetonaPlugin {
|
||||
/** 销毁 */
|
||||
destroy(): void;
|
||||
}
|
||||
declare const VERSION = "0.7.3";
|
||||
declare const VERSION = "0.7.4";
|
||||
|
||||
/**
|
||||
* metona-sqlark Plugin — 插件系统
|
||||
@@ -462,6 +462,13 @@ declare class QueryExecutor {
|
||||
private executeInsert;
|
||||
private executeUpdate;
|
||||
private executeDelete;
|
||||
/**
|
||||
* v0.7.4: 写语句(UPDATE/DELETE)WHERE 的子查询解析。
|
||||
* 非关联子查询($subquery)解析为具体值列表/标量;
|
||||
* 关联引用($col / 关联 EXISTS)在写语句中无法逐行绑定外层上下文
|
||||
* (引擎层 matchWhere 无 $col 绑定选项)→ 显式 NOT_SUPPORTED 而非静默 0 行。
|
||||
*/
|
||||
private resolveWriteWhere;
|
||||
private executeCreateTable;
|
||||
private executeDropTable;
|
||||
private executeAlterTable;
|
||||
@@ -780,6 +787,12 @@ declare class MemoryEngine implements IStorageEngine {
|
||||
private opened;
|
||||
/** v0.4.2-fix: 库内元数据(迁移版本持久化用) */
|
||||
private metaStore;
|
||||
/**
|
||||
* v0.7.4: 由 CREATE UNIQUE INDEX 添加的 unique 列(table:col)。
|
||||
* 与建表 UNIQUE 约束区分:DROP INDEX 只允许解除索引来源的 unique,
|
||||
* 建表约束需重建表(对齐 SQLite 语义,此前静默解除且不可恢复)。
|
||||
*/
|
||||
private uniqueIndexCols;
|
||||
private snapshot;
|
||||
open(_dbName: string, _version: number): Promise<void>;
|
||||
close(): Promise<void>;
|
||||
@@ -1004,6 +1017,13 @@ declare class AriaEngine implements IStorageEngine {
|
||||
private tablePKs;
|
||||
private opCounter;
|
||||
private secondaryIndexes;
|
||||
/**
|
||||
* v0.7.4: 由 CREATE UNIQUE INDEX 添加的 unique 列(table:col)。
|
||||
* 与建表 UNIQUE 约束区分:DROP INDEX 只允许解除索引来源的 unique,
|
||||
* 建表约束需重建表(对齐 SQLite 语义,此前静默解除且重启后永久消失)。
|
||||
* 注:重启后无法区分历史来源,schema 中的 unique 一律按建表约束保护(保守)。
|
||||
*/
|
||||
private uniqueIndexCols;
|
||||
private mvcc;
|
||||
private currentTxnId;
|
||||
private txnSnapshot;
|
||||
|
||||
Vendored
+323
-58
@@ -30,7 +30,7 @@ class DatabaseError extends Error {
|
||||
// ---------------------------------------------------------------------------
|
||||
// 版本
|
||||
// ---------------------------------------------------------------------------
|
||||
const VERSION = '0.7.3';
|
||||
const VERSION = '0.7.4';
|
||||
|
||||
/**
|
||||
* metona-sqlark Shared WHERE Matcher — 统一的条件匹配逻辑
|
||||
@@ -58,6 +58,40 @@ function compileLikeRegex(pattern) {
|
||||
// ---------------------------------------------------------------------------
|
||||
// WHERE 匹配(顶层入口)
|
||||
// ---------------------------------------------------------------------------
|
||||
/**
|
||||
* v0.7.4: 检测 WHERE 中未解析的子查询/列引用标记($subquery / $col / $exists)。
|
||||
* QueryBuilder 等直通引擎的写路径(update/delete)不经 Executor 解析子查询,
|
||||
* 引擎层 matchWhere 对未解析标记恒 false → 静默影响 0 行。
|
||||
* 写路径预检阶段显式拒绝;SELECT 路径由 Executor 解析(不调用本函数)。
|
||||
*/
|
||||
function containsUnresolvedSubqueries(where) {
|
||||
if (!where)
|
||||
return false;
|
||||
for (const [k, v] of Object.entries(where)) {
|
||||
if (k === '$and' || k === '$or') {
|
||||
if (v.some((sub) => containsUnresolvedSubqueries(sub)))
|
||||
return true;
|
||||
continue;
|
||||
}
|
||||
if (k === '$not') {
|
||||
if (containsUnresolvedSubqueries(v))
|
||||
return true;
|
||||
continue;
|
||||
}
|
||||
if (k === '$exists')
|
||||
return true;
|
||||
if (typeof v === 'object' && v !== null && !Array.isArray(v)) {
|
||||
for (const [, operand] of Object.entries(v)) {
|
||||
if (typeof operand === 'object' && operand !== null) {
|
||||
const ops = operand;
|
||||
if ('$subquery' in ops || '$col' in ops)
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
/**
|
||||
* 匹配完整 WHERE 条件
|
||||
* @param row 当前数据行
|
||||
@@ -348,6 +382,12 @@ class MemoryEngine {
|
||||
this.opened = false;
|
||||
/** v0.4.2-fix: 库内元数据(迁移版本持久化用) */
|
||||
this.metaStore = new Map();
|
||||
/**
|
||||
* v0.7.4: 由 CREATE UNIQUE INDEX 添加的 unique 列(table:col)。
|
||||
* 与建表 UNIQUE 约束区分:DROP INDEX 只允许解除索引来源的 unique,
|
||||
* 建表约束需重建表(对齐 SQLite 语义,此前静默解除且不可恢复)。
|
||||
*/
|
||||
this.uniqueIndexCols = new Set();
|
||||
// ---- 事务快照 ----
|
||||
this.snapshot = null;
|
||||
}
|
||||
@@ -408,6 +448,12 @@ class MemoryEngine {
|
||||
this.schemas.delete(tableName);
|
||||
this.tables.delete(tableName);
|
||||
this.indexes.delete(tableName);
|
||||
// v0.7.4: 清理该表的 unique 索引来源标记(重建同名表不残留)
|
||||
const prefix = `${tableName}:`;
|
||||
for (const key of this.uniqueIndexCols) {
|
||||
if (key.startsWith(prefix))
|
||||
this.uniqueIndexCols.delete(key);
|
||||
}
|
||||
}
|
||||
async hasTable(tableName) { return this.schemas.has(tableName); }
|
||||
async getTableNames() { return Array.from(this.schemas.keys()); }
|
||||
@@ -542,6 +588,18 @@ class MemoryEngine {
|
||||
const pkCol = this.getPrimaryKey(schema);
|
||||
// v0.7.2: undefined 值视为"不更新该列"(保留旧值),null 显式置空
|
||||
const cleanUpdates = stripUndefinedUpdates(updates);
|
||||
// v0.7.4: 防御 —— QueryBuilder 直通引擎不经 Executor 子查询解析,
|
||||
// 未解析的 $subquery/$col/$exists 在 matchWhere 中恒 false → 静默 0 行
|
||||
if (containsUnresolvedSubqueries(query.where)) {
|
||||
throw new DatabaseError('Unresolved subqueries/column references in UPDATE WHERE (use db.query() to execute subqueries)', 'NOT_SUPPORTED');
|
||||
}
|
||||
// v0.7.4: 未知列显式报错 —— 此前 SET nonexistent = ... 被静默写入存储行
|
||||
// (validateRow 只遍历 schema 列,脏列残留在行内并随持久化落盘)
|
||||
for (const col of Object.keys(cleanUpdates)) {
|
||||
if (!schema.columns[col]) {
|
||||
throw new DatabaseError(`Column "${col}" does not exist in table "${tableName}"`, 'COLUMN_NOT_FOUND');
|
||||
}
|
||||
}
|
||||
// v0.7.2: 语句级原子性 — 两阶段(先全量预检,后执行)。
|
||||
// 此前逐行"校验+写入":第 N 行唯一冲突/校验失败抛错时,前 N-1 行已写入
|
||||
// → 无事务下语句级部分提交(数据半更新且调用方已收到错误)。
|
||||
@@ -714,6 +772,11 @@ class MemoryEngine {
|
||||
}
|
||||
async delete(tableName, query) {
|
||||
this.ensureTable(tableName);
|
||||
// v0.7.4: 防御 —— QueryBuilder 直通引擎不经 Executor 子查询解析,
|
||||
// 未解析的 $subquery/$col/$exists 在 matchWhere 中恒 false → 静默 0 行
|
||||
if (containsUnresolvedSubqueries(query.where)) {
|
||||
throw new DatabaseError('Unresolved subqueries/column references in DELETE WHERE (use db.query() to execute subqueries)', 'NOT_SUPPORTED');
|
||||
}
|
||||
const table = this.tables.get(tableName);
|
||||
const toDelete = [];
|
||||
for (const [pk, row] of table) {
|
||||
@@ -843,8 +906,11 @@ class MemoryEngine {
|
||||
throw error;
|
||||
}
|
||||
colDef.index = true;
|
||||
if (unique)
|
||||
if (unique) {
|
||||
colDef.unique = true;
|
||||
// v0.7.4: 记录唯一约束来源(DROP INDEX 时可解除;建表约束不可)
|
||||
this.uniqueIndexCols.add(`${tableName}:${column}`);
|
||||
}
|
||||
}
|
||||
async dropIndex(tableName, column, _indexName) {
|
||||
// v0.7.2: 同 createIndex —— 列级标志修改无法通过事务快照回滚,显式拒绝
|
||||
@@ -860,8 +926,17 @@ class MemoryEngine {
|
||||
if (!colDef.index && !colDef.unique) {
|
||||
throw new DatabaseError(`Index on column "${column}" does not exist in table "${tableName}"`, 'INDEX_NOT_FOUND');
|
||||
}
|
||||
// v0.7.4: 建表 UNIQUE 约束不可通过 DROP INDEX 解除 —— 此前 colDef.unique = false
|
||||
// 静默解除约束(后续唯一性检查失效、重复数据入库)。对齐 SQLite 语义:
|
||||
// 约束随建表存在,解除需重建表;仅 CREATE UNIQUE INDEX 添加的约束可随索引删除。
|
||||
const uniqueKey = `${tableName}:${column}`;
|
||||
if (colDef.unique && !this.uniqueIndexCols.has(uniqueKey)) {
|
||||
throw new DatabaseError(`Cannot drop index on column "${column}" in table "${tableName}": ` +
|
||||
'UNIQUE constraint defined at table creation must be removed by recreating the table', 'NOT_SUPPORTED');
|
||||
}
|
||||
colDef.index = false;
|
||||
colDef.unique = false;
|
||||
this.uniqueIndexCols.delete(uniqueKey);
|
||||
const tableIndexes = this.indexes.get(tableName);
|
||||
if (tableIndexes)
|
||||
tableIndexes.delete(column);
|
||||
@@ -935,6 +1010,11 @@ class MemoryEngine {
|
||||
if (colDef.required && (value === undefined || value === null)) {
|
||||
throw new DatabaseError(`Column "${colName}" is required in table "${schema.name}"`, 'VALIDATION_ERROR');
|
||||
}
|
||||
// v0.7.4: 主键列强制非空(SQL 语义 PK 隐含 NOT NULL)——
|
||||
// 此前 null/undefined 主键被 String() 化为 "null"/"undefined" 静默入库
|
||||
if (colDef.primaryKey && (value === undefined || value === null)) {
|
||||
throw new DatabaseError(`Primary key column "${colName}" in table "${schema.name}" cannot be null or undefined`, 'VALIDATION_ERROR');
|
||||
}
|
||||
if (value !== undefined && value !== null)
|
||||
this.checkType(colName, colDef.type, value);
|
||||
if (value !== undefined)
|
||||
@@ -1767,6 +1847,8 @@ class KVStore {
|
||||
if (this.opened)
|
||||
return;
|
||||
this.dbName = dbName;
|
||||
// v0.7.4: 打开时同样清理后台错误状态(防 close/reopen 残留)
|
||||
this.lastBackgroundError = null;
|
||||
await this.medium.open(dbName);
|
||||
this.index = new Map();
|
||||
this.seq = 0;
|
||||
@@ -1849,6 +1931,9 @@ class KVStore {
|
||||
this.index.clear();
|
||||
this.seq = 0;
|
||||
this.logBytes = 0;
|
||||
// v0.7.4: 清理后台错误状态 —— 此前跨 close/reopen 残留,
|
||||
// 重开后首次 checkpoint 会抛出上一次生命周期的旧错误
|
||||
this.lastBackgroundError = null;
|
||||
this.opened = false;
|
||||
}
|
||||
// =======================================================================
|
||||
@@ -2923,6 +3008,37 @@ class RedBlackTree {
|
||||
rangeScan(startKey, endKey, callback) {
|
||||
this._rangeScan(this.root, startKey, endKey, callback);
|
||||
}
|
||||
/**
|
||||
* v0.7.4: 惰性范围遍历(显式栈中序迭代 + 边界剪枝)。
|
||||
* 真流式扫描:生成器按需产出,提前终止(limit 达成)时剩余子树不再遍历。
|
||||
*/
|
||||
*scanLazy(startKey, endKey) {
|
||||
const stack = [];
|
||||
// 定位到 >= startKey 的最左节点(沿路入栈)
|
||||
let cur = this.root;
|
||||
while (cur) {
|
||||
if (cur.key >= startKey) {
|
||||
stack.push(cur);
|
||||
cur = cur.left;
|
||||
}
|
||||
else {
|
||||
cur = cur.right;
|
||||
}
|
||||
}
|
||||
while (stack.length > 0) {
|
||||
const node = stack.pop();
|
||||
// 中序递增:越过 endKey 后所有剩余节点均越界
|
||||
if (node.key > endKey)
|
||||
break;
|
||||
if (node.key >= startKey)
|
||||
yield [node.key, node.value];
|
||||
cur = node.right;
|
||||
while (cur) {
|
||||
stack.push(cur);
|
||||
cur = cur.left;
|
||||
}
|
||||
}
|
||||
}
|
||||
/** 获取所有条目 */
|
||||
getAllEntries() {
|
||||
const entries = [];
|
||||
@@ -2970,8 +3086,14 @@ class RedBlackTree {
|
||||
}
|
||||
else {
|
||||
const successor = this.minimum(node.right);
|
||||
if (successor.parent !== node) {
|
||||
this.transplant(successor, successor.right);
|
||||
// v0.7.4: 在 transplant 重连前捕获 successor 原右子与父 ——
|
||||
// x(双黑修复起点)= successor 原右子(占位在 successor 原位置)。
|
||||
// 此前 `successor.right?.parent ?? null` 在重赋值后取值:x 指向 node 右子树,
|
||||
// 且 null 时 parent 为 null → fixDelete 直接跳过修复(删除黑色节点后失衡)。
|
||||
const successorRight = successor.right;
|
||||
const successorParent = successor.parent;
|
||||
if (successorParent !== node) {
|
||||
this.transplant(successor, successorRight);
|
||||
successor.right = node.right;
|
||||
successor.right.parent = successor;
|
||||
}
|
||||
@@ -2980,8 +3102,13 @@ class RedBlackTree {
|
||||
successor.left.parent = successor;
|
||||
const origColor = successor.color;
|
||||
successor.color = node.color;
|
||||
if (origColor === Color.BLACK)
|
||||
this.fixDelete(successor.right, successor.right?.parent ?? null);
|
||||
if (origColor === Color.BLACK) {
|
||||
const x = successorRight;
|
||||
// x 为 null 占位:直接右子时其父为 successor(已移到 node 位置),
|
||||
// 间接右子时其父为 successor 原父(transplant 已把 x 接到其下)
|
||||
const xParent = x ? x.parent : (successorParent === node ? successor : successorParent);
|
||||
this.fixDelete(x, xParent);
|
||||
}
|
||||
}
|
||||
}
|
||||
transplant(u, v) {
|
||||
@@ -3246,6 +3373,10 @@ class MemTable {
|
||||
this.tree.rangeScan(startKey, endKey, (k, v) => entries.push([k, v]));
|
||||
return entries;
|
||||
}
|
||||
/** v0.7.4: 惰性范围扫描(真流式,逐条产出) */
|
||||
scanLazy(startKey, endKey) {
|
||||
return this.tree.scanLazy(startKey, endKey);
|
||||
}
|
||||
/** 条目数 */
|
||||
getEntryCount() {
|
||||
return this.tree.size;
|
||||
@@ -3676,6 +3807,16 @@ class SSTableReader {
|
||||
}
|
||||
/** 范围扫描 */
|
||||
rangeScan(startKey, endKey, callback) {
|
||||
// v0.7.4: 包装惰性生成器(行为一致,消除双份解析循环)
|
||||
for (const [key, value] of this.scanLazy(startKey, endKey)) {
|
||||
callback(key, value);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* v0.7.4: 惰性范围扫描 —— 生成器逐块逐条产出(真流式)。
|
||||
* 提前终止时未消费的块不再解析,大表流式内存 O(1)。
|
||||
*/
|
||||
*scanLazy(startKey, endKey) {
|
||||
if (this.indexEntries.length === 0)
|
||||
return;
|
||||
const startBlockIdx = Math.max(0, this.locateBlockGE(startKey));
|
||||
@@ -3690,7 +3831,7 @@ class SSTableReader {
|
||||
for (let bi = startBlockIdx; bi <= endBlockIdx && bi >= 0; bi++) {
|
||||
const entry = this.indexEntries[bi];
|
||||
const blockData = this.getBlockData(entry);
|
||||
// v0.4.1-fix: 残缺块跳过(rangeScan 继续后续块,不抛异常)
|
||||
// v0.4.1-fix: 残缺块跳过(继续后续块,不抛异常)
|
||||
if (!blockData)
|
||||
continue;
|
||||
const blockView = new DataView(blockData.buffer, blockData.byteOffset, blockData.byteLength);
|
||||
@@ -3714,7 +3855,7 @@ class SSTableReader {
|
||||
if (key >= startKey && key <= endKey) {
|
||||
try {
|
||||
const value = JSON.parse(new TextDecoder().decode(valBytes));
|
||||
callback(key, value);
|
||||
yield [key, value];
|
||||
}
|
||||
catch {
|
||||
// skip corrupted entry
|
||||
@@ -3903,6 +4044,23 @@ class ArrayEntrySource {
|
||||
this.index = 0;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* v0.7.4: 生成器数据源 —— 惰性迭代(真流式扫描)。
|
||||
* MergeIterator 的 next() 逐条拉取,生成器按需产出(findStream 提前终止时
|
||||
* 未消费部分不再物化,大表流式内存 O(1))。
|
||||
*/
|
||||
class GeneratorEntrySource {
|
||||
constructor(iter) {
|
||||
this.iter = iter;
|
||||
}
|
||||
next() {
|
||||
const r = this.iter.next();
|
||||
return r.done ? null : r.value;
|
||||
}
|
||||
reset() {
|
||||
// 生成器不可重置;MergeIterator 无 reset 消费方,接口保留
|
||||
}
|
||||
}
|
||||
/** 最小堆 */
|
||||
class MinHeap {
|
||||
constructor() {
|
||||
@@ -4369,16 +4527,21 @@ class LSM {
|
||||
}
|
||||
rangeScan(startKey, endKey) {
|
||||
const result = [];
|
||||
this.rangeScanLazy(startKey, endKey, (k, v) => result.push([k, v]));
|
||||
this.rangeScanLazy(startKey, endKey, (k, v) => { result.push([k, v]); });
|
||||
return result;
|
||||
}
|
||||
/** 惰性范围扫描:通过回调逐条返回,不一次性物化所有源 */
|
||||
/**
|
||||
* 惰性范围扫描:通过回调逐条返回,不一次性物化。
|
||||
* v0.7.4: 真惰性 —— 各源(MemTable/frozen/SSTable)以生成器接入 MergeIterator,
|
||||
* 逐条拉取;回调返回 false 时提前终止(未消费部分不再解析/物化)。
|
||||
* 此前实现内部 mergeIter.drain() 全量物化,与"流式不物化"宣称不符。
|
||||
*/
|
||||
rangeScanLazy(startKey, endKey, callback) {
|
||||
const mergeIter = new MergeIterator();
|
||||
mergeIter.addSource(new ArrayEntrySource(this.memtable.rangeScan(startKey, endKey)));
|
||||
mergeIter.addSource(new GeneratorEntrySource(this.memtable.scanLazy(startKey, endKey)));
|
||||
// pending frozen memtables(从新到旧,新数据 sourceIndex 更小)
|
||||
for (let i = this.frozenMemtables.length - 1; i >= 0; i--) {
|
||||
mergeIter.addSource(new ArrayEntrySource(this.frozenMemtables[i].rangeScan(startKey, endKey)));
|
||||
mergeIter.addSource(new GeneratorEntrySource(this.frozenMemtables[i].scanLazy(startKey, endKey)));
|
||||
}
|
||||
for (let level = 0; level < MAX_LSM_LEVELS; level++) {
|
||||
for (const meta of this.levels[level]) {
|
||||
@@ -4387,42 +4550,21 @@ class LSM {
|
||||
const reader = this.loadSSTableReader(meta);
|
||||
if (!reader)
|
||||
continue;
|
||||
reader.rangeScan(startKey, endKey, (k, v) => {
|
||||
mergeIter.addSource(new ArrayEntrySource([[k, v]]));
|
||||
});
|
||||
mergeIter.addSource(new GeneratorEntrySource(reader.scanLazy(startKey, endKey)));
|
||||
}
|
||||
}
|
||||
const merged = mergeIter.drain();
|
||||
for (const [k, v] of merged) {
|
||||
let entry = mergeIter.next();
|
||||
while (entry) {
|
||||
const [k, v] = entry;
|
||||
if (!v.__tombstone) {
|
||||
callback(k, v);
|
||||
const cont = callback(k, v);
|
||||
// v0.7.4: 提前终止(流式 limit 达成)
|
||||
if (cont === false)
|
||||
return;
|
||||
}
|
||||
entry = mergeIter.next();
|
||||
}
|
||||
}
|
||||
getAllEntries() {
|
||||
const result = new Map();
|
||||
// 从最旧层级开始聚合;同层级内从最旧到最新遍历,
|
||||
// 保证 result.set 覆盖时最终保留最新值
|
||||
for (let level = MAX_LSM_LEVELS - 1; level >= 0; level--) {
|
||||
for (let i = this.levels[level].length - 1; i >= 0; i--) {
|
||||
const meta = this.levels[level][i];
|
||||
const reader = this.loadSSTableReader(meta);
|
||||
if (!reader)
|
||||
continue;
|
||||
reader.scanAll((k, v) => result.set(k, v));
|
||||
}
|
||||
}
|
||||
// MemTable 覆盖(最新)
|
||||
for (const [k, v] of this.memtable.getAllEntries()) {
|
||||
result.set(k, v);
|
||||
}
|
||||
for (let i = this.frozenMemtables.length - 1; i >= 0; i--) {
|
||||
for (const [k, v] of this.frozenMemtables[i].getAllEntries()) {
|
||||
result.set(k, v);
|
||||
}
|
||||
}
|
||||
return Array.from(result.entries()).filter(([, v]) => !v.__tombstone);
|
||||
}
|
||||
// =======================================================================
|
||||
// Compaction
|
||||
// =======================================================================
|
||||
@@ -6484,6 +6626,13 @@ class AriaEngine {
|
||||
this.opCounter = 0;
|
||||
// 二级索引:table.colKey → LSM
|
||||
this.secondaryIndexes = new Map();
|
||||
/**
|
||||
* v0.7.4: 由 CREATE UNIQUE INDEX 添加的 unique 列(table:col)。
|
||||
* 与建表 UNIQUE 约束区分:DROP INDEX 只允许解除索引来源的 unique,
|
||||
* 建表约束需重建表(对齐 SQLite 语义,此前静默解除且重启后永久消失)。
|
||||
* 注:重启后无法区分历史来源,schema 中的 unique 一律按建表约束保护(保守)。
|
||||
*/
|
||||
this.uniqueIndexCols = new Set();
|
||||
// MVCC 事务
|
||||
this.mvcc = new MVCCManager();
|
||||
this.currentTxnId = null;
|
||||
@@ -6696,6 +6845,7 @@ class AriaEngine {
|
||||
this.schemas.clear();
|
||||
this.tablePKs.clear();
|
||||
this.secondaryIndexes.clear();
|
||||
this.uniqueIndexCols.clear();
|
||||
this.mvcc = new MVCCManager();
|
||||
this.currentTxnId = null;
|
||||
this.txnSnapshot = null;
|
||||
@@ -6792,6 +6942,7 @@ class AriaEngine {
|
||||
this.schemas.clear();
|
||||
this.tablePKs.clear();
|
||||
this.secondaryIndexes.clear();
|
||||
this.uniqueIndexCols.clear();
|
||||
this.lsm.clear();
|
||||
this.mvcc = new MVCCManager();
|
||||
this.currentTxnId = null;
|
||||
@@ -6866,6 +7017,12 @@ class AriaEngine {
|
||||
// v0.4.2-fix: 清理该表的全部二级索引 LSM 与持久化文件 —
|
||||
// 此前残留孤儿索引,重建同名表后旧索引数据污染新表(索引查询返回错误行)
|
||||
await this.cleanupTableIndexes(tableName);
|
||||
// v0.7.4: 清理该表的 unique 索引来源标记
|
||||
const uPrefix = `${tableName}:`;
|
||||
for (const k of this.uniqueIndexCols) {
|
||||
if (k.startsWith(uPrefix))
|
||||
this.uniqueIndexCols.delete(k);
|
||||
}
|
||||
this.schemas.delete(tableName);
|
||||
this.tablePKs.delete(tableName);
|
||||
await this.persistSchemas();
|
||||
@@ -7050,6 +7207,11 @@ class AriaEngine {
|
||||
async update(tableName, query, updates) {
|
||||
this.ensureOpen();
|
||||
this.ensureTable(tableName);
|
||||
// v0.7.4: 防御 —— QueryBuilder 直通引擎不经 Executor 子查询解析,
|
||||
// 未解析的 $subquery/$col/$exists 在 matchWhere 中恒 false → 静默 0 行
|
||||
if (containsUnresolvedSubqueries(query.where)) {
|
||||
throw new DatabaseError('Unresolved subqueries/column references in UPDATE WHERE (use db.query() to execute subqueries)', 'NOT_SUPPORTED');
|
||||
}
|
||||
const schema = this.schemas.get(tableName);
|
||||
const rows = await this.getAllRows(tableName);
|
||||
let count = 0;
|
||||
@@ -7059,6 +7221,13 @@ class AriaEngine {
|
||||
const visited = new Set();
|
||||
// v0.7.2: undefined 值视为"不更新该列"(保留旧值),null 显式置空
|
||||
const cleanUpdates = stripUndefinedUpdates(updates);
|
||||
// v0.7.4: 未知列显式报错 —— 此前 SET nonexistent = ... 被静默写入存储行
|
||||
// (validateRow 只遍历 schema 列,脏列残留在行内并随 SSTable 持久化)
|
||||
for (const col of Object.keys(cleanUpdates)) {
|
||||
if (!schema.columns[col]) {
|
||||
throw new DatabaseError(`Column "${col}" does not exist in table "${tableName}"`, 'COLUMN_NOT_FOUND');
|
||||
}
|
||||
}
|
||||
// v0.6.2: 唯一约束 — 批量预加载本批更新涉及的唯一列索引范围(一次 drainChain)
|
||||
const uniqueCols = this.uniqueColumns(tableName, schema);
|
||||
for (const colName of uniqueCols) {
|
||||
@@ -7267,6 +7436,11 @@ class AriaEngine {
|
||||
async delete(tableName, query) {
|
||||
this.ensureOpen();
|
||||
this.ensureTable(tableName);
|
||||
// v0.7.4: 防御 —— QueryBuilder 直通引擎不经 Executor 子查询解析,
|
||||
// 未解析的 $subquery/$col/$exists 在 matchWhere 中恒 false → 静默 0 行
|
||||
if (containsUnresolvedSubqueries(query.where)) {
|
||||
throw new DatabaseError('Unresolved subqueries/column references in DELETE WHERE (use db.query() to execute subqueries)', 'NOT_SUPPORTED');
|
||||
}
|
||||
const rows = await this.getAllRows(tableName);
|
||||
let count = 0;
|
||||
// v0.3.1: 批量 WAL 写入(组提交)
|
||||
@@ -7479,14 +7653,15 @@ class AriaEngine {
|
||||
}
|
||||
return count;
|
||||
}
|
||||
// 全表惰性扫描(含 WHERE 过滤,不物化)
|
||||
// 全表惰性扫描(含 WHERE 过滤,不物化;v0.7.4: callback 返回 false 提前终止,
|
||||
// 未消费的 SSTable 块 / 子树不再解析 —— 真流式,大表 limit 内存 O(1))
|
||||
await this.lsm.prefetchRange(prefix, `${prefix}\uffff`);
|
||||
this.lsm.rangeScanLazy(prefix, `${prefix}\uffff`, (key, value) => {
|
||||
if (count >= limit)
|
||||
return;
|
||||
return false;
|
||||
const row = { ...value };
|
||||
row[pkCol] = key.slice(prefix.length);
|
||||
emit(row);
|
||||
return emit(row);
|
||||
});
|
||||
return count;
|
||||
}
|
||||
@@ -7648,8 +7823,11 @@ class AriaEngine {
|
||||
throw error;
|
||||
}
|
||||
colDef.index = true;
|
||||
if (unique)
|
||||
if (unique) {
|
||||
colDef.unique = true;
|
||||
// v0.7.4: 记录唯一约束来源(DROP INDEX 时可解除;建表约束不可)
|
||||
this.uniqueIndexCols.add(`${tableName}:${column}`);
|
||||
}
|
||||
await this.persistSchemas();
|
||||
}
|
||||
async dropIndex(tableName, column, _indexName) {
|
||||
@@ -7668,8 +7846,17 @@ class AriaEngine {
|
||||
if (!colDef.index && !colDef.unique && !this.secondaryIndexes.has(`${tableName}:idx:${column}`)) {
|
||||
throw new DatabaseError(`Index on column "${column}" does not exist in table "${tableName}"`, 'INDEX_NOT_FOUND');
|
||||
}
|
||||
// v0.7.4: 建表 UNIQUE 约束不可通过 DROP INDEX 解除 —— 此前 colDef.unique = false
|
||||
// 静默解除约束(重启后 persistSchemas 使约束永久消失)。对齐 SQLite 语义:
|
||||
// 约束随建表存在,解除需重建表;仅 CREATE UNIQUE INDEX 添加的约束可随索引删除。
|
||||
const uniqueKey = `${tableName}:${column}`;
|
||||
if (colDef.unique && !this.uniqueIndexCols.has(uniqueKey)) {
|
||||
throw new DatabaseError(`Cannot drop index on column "${column}" in table "${tableName}": ` +
|
||||
'UNIQUE constraint defined at table creation must be removed by recreating the table', 'NOT_SUPPORTED');
|
||||
}
|
||||
colDef.index = false;
|
||||
colDef.unique = false;
|
||||
this.uniqueIndexCols.delete(uniqueKey);
|
||||
const idxKey = `${tableName}:idx:${column}`;
|
||||
const idxLsm = this.secondaryIndexes.get(idxKey);
|
||||
if (idxLsm) {
|
||||
@@ -7888,6 +8075,11 @@ class AriaEngine {
|
||||
if (colDef.required && (value === undefined || value === null)) {
|
||||
throw new DatabaseError(`Column "${colName}" is required in table "${schema.name}"`, 'VALIDATION_ERROR');
|
||||
}
|
||||
// v0.7.4: 主键列强制非空(SQL 语义 PK 隐含 NOT NULL)——
|
||||
// 此前 null/undefined 主键被 String() 化为 "null"/"undefined" 静默入库
|
||||
if (colDef.primaryKey && (value === undefined || value === null)) {
|
||||
throw new DatabaseError(`Primary key column "${colName}" in table "${schema.name}" cannot be null or undefined`, 'VALIDATION_ERROR');
|
||||
}
|
||||
if (value !== undefined && value !== null) {
|
||||
this.checkType(colName, colDef.type, value, colDef);
|
||||
}
|
||||
@@ -8416,10 +8608,15 @@ class AriaEngine {
|
||||
if (!schema)
|
||||
return 0;
|
||||
let rebuiltCount = 0;
|
||||
for (const [colName, colDef] of Object.entries(schema.columns)) {
|
||||
// v0.3.3: 主键列不建冗余二级索引(主 LSM 即 PK 索引)
|
||||
if (!colDef.index && !colDef.unique)
|
||||
continue;
|
||||
// v0.7.4-perf: 单次全表扫描重建全部索引列 —— 此前每个索引列各做一次
|
||||
// getAllRows(N 列 × M 行全表扫描 + 每次 prefetchRange drainChain),
|
||||
// 多索引大表 REINDEX/崩溃恢复按索引列数线性放大
|
||||
const pkCol = this.tablePKs.get(tableName);
|
||||
const idxCols = Object.entries(schema.columns).filter(([, colDef]) => colDef.index || colDef.unique);
|
||||
if (idxCols.length === 0)
|
||||
return 0;
|
||||
const rows = await this.getAllRows(tableName);
|
||||
for (const [colName] of idxCols) {
|
||||
const idxKey = `${tableName}:idx:${colName}`;
|
||||
const idxLsm = this.secondaryIndexes.get(idxKey);
|
||||
if (!idxLsm)
|
||||
@@ -8428,11 +8625,10 @@ class AriaEngine {
|
||||
await idxLsm.clear();
|
||||
rebuiltCount++;
|
||||
// 从主 LSM 重建索引
|
||||
const rows = await this.getAllRows(tableName);
|
||||
for (const row of rows) {
|
||||
const val = row[colName];
|
||||
if (val !== undefined && val !== null) {
|
||||
idxLsm.put(`${String(val)}:${row[this.tablePKs.get(tableName)]}`, { pk: row[this.tablePKs.get(tableName)] });
|
||||
idxLsm.put(`${String(val)}:${row[pkCol]}`, { pk: row[pkCol] });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8729,7 +8925,15 @@ class HybridEngine {
|
||||
// ---- 事务 ----
|
||||
async beginTransaction() {
|
||||
await this.memoryEngine.beginTransaction();
|
||||
await this.diskEngine.beginTransaction();
|
||||
// v0.7.4: 磁盘 begin 失败时补偿回滚内存快照 —— 此前内存已 begin、
|
||||
// 磁盘抛错 → 内存快照泄漏(后续所有事务报 TX_ACTIVE)
|
||||
try {
|
||||
await this.diskEngine.beginTransaction();
|
||||
}
|
||||
catch (error) {
|
||||
await this.memoryEngine.rollbackTransaction();
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
async commitTransaction() {
|
||||
// 先写磁盘,保证持久化优先;磁盘失败则回滚内存
|
||||
@@ -10690,6 +10894,30 @@ function parseWhereCondition(sql) {
|
||||
*
|
||||
* JOIN / GROUP BY / DISTINCT 逻辑在此层处理。
|
||||
*/
|
||||
// ---------------------------------------------------------------------------
|
||||
// 分组 / 去重键编码(v0.7.4)
|
||||
// ---------------------------------------------------------------------------
|
||||
/**
|
||||
* v0.7.4: 分组/去重键的类型安全编码 —— 此前 `String(v ?? 'null')` 使
|
||||
* null 与字符串 'null' 合并为一组(GROUP BY 静默少组),`String(v ?? '\0')`
|
||||
* 使 null/undefined/'\0' 在 DISTINCT/UNION 中互相吞并。类型前缀编码后
|
||||
* 各类型独立,仅同类型同值合并(与 where-matcher 的 === 语义一致)。
|
||||
*/
|
||||
function encodeGroupKey(v) {
|
||||
if (v === null)
|
||||
return 'n';
|
||||
if (v === undefined)
|
||||
return 'u';
|
||||
if (typeof v === 'string')
|
||||
return `s${v}`;
|
||||
if (typeof v === 'number')
|
||||
return `d${v}`;
|
||||
if (typeof v === 'boolean')
|
||||
return `b${v}`;
|
||||
if (typeof v === 'object')
|
||||
return `o${JSON.stringify(v)}`;
|
||||
return `x${String(v)}`;
|
||||
}
|
||||
/** 解析 "CASE WHEN c1 THEN v1 WHEN c2 THEN v2 ELSE v3 END [AS alias]" */
|
||||
function parseCaseExpression(expr) {
|
||||
const m = expr.match(/^\s*CASE\s+([\s\S]*?)\s+END\s*(?:AS\s+(\w+))?\s*$/i);
|
||||
@@ -10797,7 +11025,7 @@ class QueryExecutor {
|
||||
const seen = new Set();
|
||||
const result = [];
|
||||
for (const row of normalized) {
|
||||
const key = Object.values(row).map((v) => String(v ?? '\0')).join('\x1f');
|
||||
const key = Object.values(row).map(encodeGroupKey).join('\x1f');
|
||||
if (!seen.has(key)) {
|
||||
seen.add(key);
|
||||
result.push(row);
|
||||
@@ -10805,7 +11033,7 @@ class QueryExecutor {
|
||||
}
|
||||
for (const row of rightRows) {
|
||||
const projected = this.projectUnionRow(row, leftCols);
|
||||
const key = Object.values(projected).map((v) => String(v ?? '\0')).join('\x1f');
|
||||
const key = Object.values(projected).map(encodeGroupKey).join('\x1f');
|
||||
if (!seen.has(key)) {
|
||||
seen.add(key);
|
||||
result.push(projected);
|
||||
@@ -10844,7 +11072,10 @@ class QueryExecutor {
|
||||
}
|
||||
else if (stmt.query.type === 'UPDATE' || stmt.query.type === 'DELETE') {
|
||||
try {
|
||||
// v0.7.4: 子查询解析后估算 —— 此前 $subquery 未解析使 count 恒 0
|
||||
await this.resolveWriteWhere(stmt.query);
|
||||
const plan = compileStatement(stmt.query);
|
||||
plan.where = stmt.query.where;
|
||||
rows = await this.engine.count(plan.table, plan);
|
||||
}
|
||||
catch {
|
||||
@@ -11250,7 +11481,9 @@ class QueryExecutor {
|
||||
executeGroupBy(rows, stmt) {
|
||||
const groups = new Map();
|
||||
for (const row of rows) {
|
||||
const key = stmt.groupBy.map((col) => String(row[col] ?? 'null')).join('|');
|
||||
// v0.7.4: 类型安全键编码 —— 此前 String(row[col] ?? 'null') 使
|
||||
// null 与字符串 'null' 合并为一组(GROUP BY 静默少组)
|
||||
const key = stmt.groupBy.map((col) => encodeGroupKey(row[col])).join('\x1f');
|
||||
if (!groups.has(key))
|
||||
groups.set(key, []);
|
||||
groups.get(key).push(row);
|
||||
@@ -11321,7 +11554,8 @@ class QueryExecutor {
|
||||
executeDistinct(rows) {
|
||||
const seen = new Set();
|
||||
return rows.filter((row) => {
|
||||
const key = Object.values(row).map((v) => String(v ?? '\0')).join('\x1f');
|
||||
// v0.7.4: 类型安全键编码(null 与 'null' 字符串、'\0' 分离)
|
||||
const key = Object.values(row).map(encodeGroupKey).join('\x1f');
|
||||
if (seen.has(key))
|
||||
return false;
|
||||
seen.add(key);
|
||||
@@ -11377,13 +11611,37 @@ class QueryExecutor {
|
||||
return this.engine.insert(stmt.into, rows);
|
||||
}
|
||||
async executeUpdate(stmt) {
|
||||
// v0.7.4: 先解析 WHERE 子查询 —— 此前直接 compileStatement 调引擎:
|
||||
// 引擎层 matchWhere 的 $in/$nin 遇未解析的 $subquery 对象恒 false →
|
||||
// 所有行不匹配,UPDATE 静默影响 0 行(与 queryStream v0.7.3 修复同类)。
|
||||
await this.resolveWriteWhere(stmt);
|
||||
const plan = compileStatement(stmt);
|
||||
plan.where = stmt.where;
|
||||
return this.engine.update(plan.table, plan, stmt.sets);
|
||||
}
|
||||
async executeDelete(stmt) {
|
||||
// v0.7.4: 同 executeUpdate —— DELETE 子查询 WHERE 此前静默删除 0 行
|
||||
await this.resolveWriteWhere(stmt);
|
||||
const plan = compileStatement(stmt);
|
||||
plan.where = stmt.where;
|
||||
return this.engine.delete(plan.table, plan);
|
||||
}
|
||||
/**
|
||||
* v0.7.4: 写语句(UPDATE/DELETE)WHERE 的子查询解析。
|
||||
* 非关联子查询($subquery)解析为具体值列表/标量;
|
||||
* 关联引用($col / 关联 EXISTS)在写语句中无法逐行绑定外层上下文
|
||||
* (引擎层 matchWhere 无 $col 绑定选项)→ 显式 NOT_SUPPORTED 而非静默 0 行。
|
||||
*/
|
||||
async resolveWriteWhere(stmt) {
|
||||
const where = stmt.where;
|
||||
if (!where || Object.keys(where).length === 0)
|
||||
return where ?? {};
|
||||
if (this.hasCorrelatedRefs(where)) {
|
||||
throw new DatabaseError('Correlated subqueries and column references are not supported in UPDATE/DELETE WHERE clauses', 'NOT_SUPPORTED');
|
||||
}
|
||||
stmt.where = await this.resolveSubqueries(where);
|
||||
return stmt.where;
|
||||
}
|
||||
async executeCreateTable(stmt) {
|
||||
// IF NOT EXISTS: 表已存在时静默返回
|
||||
if (stmt.ifNotExists) {
|
||||
@@ -12498,7 +12756,14 @@ class MetonaSqlark {
|
||||
*/
|
||||
async queryStream(sql, onRow) {
|
||||
this.ensureReady();
|
||||
const stmt = parseAll(sql)[0];
|
||||
// v0.7.4: 多语句显式拒绝 —— 此前 parseAll(sql)[0] 静默忽略后续语句:
|
||||
// 可流式时后续语句(如 DELETE)不执行,不可流式时回退 query() 却会执行
|
||||
// 全部语句 → 同一条 SQL 两种语义。流式 API 要求单条 SELECT。
|
||||
const statements = parseAll(sql);
|
||||
if (statements.length !== 1) {
|
||||
throw new DatabaseError('queryStream requires exactly one SELECT statement', 'PARSE_ERROR');
|
||||
}
|
||||
const stmt = statements[0];
|
||||
if (!stmt || stmt.type !== 'SELECT') {
|
||||
throw new DatabaseError('queryStream only supports SELECT statements', 'NOT_SUPPORTED');
|
||||
}
|
||||
|
||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+323
-58
@@ -36,7 +36,7 @@
|
||||
// ---------------------------------------------------------------------------
|
||||
// 版本
|
||||
// ---------------------------------------------------------------------------
|
||||
const VERSION = '0.7.3';
|
||||
const VERSION = '0.7.4';
|
||||
|
||||
/**
|
||||
* metona-sqlark Shared WHERE Matcher — 统一的条件匹配逻辑
|
||||
@@ -64,6 +64,40 @@
|
||||
// ---------------------------------------------------------------------------
|
||||
// WHERE 匹配(顶层入口)
|
||||
// ---------------------------------------------------------------------------
|
||||
/**
|
||||
* v0.7.4: 检测 WHERE 中未解析的子查询/列引用标记($subquery / $col / $exists)。
|
||||
* QueryBuilder 等直通引擎的写路径(update/delete)不经 Executor 解析子查询,
|
||||
* 引擎层 matchWhere 对未解析标记恒 false → 静默影响 0 行。
|
||||
* 写路径预检阶段显式拒绝;SELECT 路径由 Executor 解析(不调用本函数)。
|
||||
*/
|
||||
function containsUnresolvedSubqueries(where) {
|
||||
if (!where)
|
||||
return false;
|
||||
for (const [k, v] of Object.entries(where)) {
|
||||
if (k === '$and' || k === '$or') {
|
||||
if (v.some((sub) => containsUnresolvedSubqueries(sub)))
|
||||
return true;
|
||||
continue;
|
||||
}
|
||||
if (k === '$not') {
|
||||
if (containsUnresolvedSubqueries(v))
|
||||
return true;
|
||||
continue;
|
||||
}
|
||||
if (k === '$exists')
|
||||
return true;
|
||||
if (typeof v === 'object' && v !== null && !Array.isArray(v)) {
|
||||
for (const [, operand] of Object.entries(v)) {
|
||||
if (typeof operand === 'object' && operand !== null) {
|
||||
const ops = operand;
|
||||
if ('$subquery' in ops || '$col' in ops)
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
/**
|
||||
* 匹配完整 WHERE 条件
|
||||
* @param row 当前数据行
|
||||
@@ -354,6 +388,12 @@
|
||||
this.opened = false;
|
||||
/** v0.4.2-fix: 库内元数据(迁移版本持久化用) */
|
||||
this.metaStore = new Map();
|
||||
/**
|
||||
* v0.7.4: 由 CREATE UNIQUE INDEX 添加的 unique 列(table:col)。
|
||||
* 与建表 UNIQUE 约束区分:DROP INDEX 只允许解除索引来源的 unique,
|
||||
* 建表约束需重建表(对齐 SQLite 语义,此前静默解除且不可恢复)。
|
||||
*/
|
||||
this.uniqueIndexCols = new Set();
|
||||
// ---- 事务快照 ----
|
||||
this.snapshot = null;
|
||||
}
|
||||
@@ -414,6 +454,12 @@
|
||||
this.schemas.delete(tableName);
|
||||
this.tables.delete(tableName);
|
||||
this.indexes.delete(tableName);
|
||||
// v0.7.4: 清理该表的 unique 索引来源标记(重建同名表不残留)
|
||||
const prefix = `${tableName}:`;
|
||||
for (const key of this.uniqueIndexCols) {
|
||||
if (key.startsWith(prefix))
|
||||
this.uniqueIndexCols.delete(key);
|
||||
}
|
||||
}
|
||||
async hasTable(tableName) { return this.schemas.has(tableName); }
|
||||
async getTableNames() { return Array.from(this.schemas.keys()); }
|
||||
@@ -548,6 +594,18 @@
|
||||
const pkCol = this.getPrimaryKey(schema);
|
||||
// v0.7.2: undefined 值视为"不更新该列"(保留旧值),null 显式置空
|
||||
const cleanUpdates = stripUndefinedUpdates(updates);
|
||||
// v0.7.4: 防御 —— QueryBuilder 直通引擎不经 Executor 子查询解析,
|
||||
// 未解析的 $subquery/$col/$exists 在 matchWhere 中恒 false → 静默 0 行
|
||||
if (containsUnresolvedSubqueries(query.where)) {
|
||||
throw new DatabaseError('Unresolved subqueries/column references in UPDATE WHERE (use db.query() to execute subqueries)', 'NOT_SUPPORTED');
|
||||
}
|
||||
// v0.7.4: 未知列显式报错 —— 此前 SET nonexistent = ... 被静默写入存储行
|
||||
// (validateRow 只遍历 schema 列,脏列残留在行内并随持久化落盘)
|
||||
for (const col of Object.keys(cleanUpdates)) {
|
||||
if (!schema.columns[col]) {
|
||||
throw new DatabaseError(`Column "${col}" does not exist in table "${tableName}"`, 'COLUMN_NOT_FOUND');
|
||||
}
|
||||
}
|
||||
// v0.7.2: 语句级原子性 — 两阶段(先全量预检,后执行)。
|
||||
// 此前逐行"校验+写入":第 N 行唯一冲突/校验失败抛错时,前 N-1 行已写入
|
||||
// → 无事务下语句级部分提交(数据半更新且调用方已收到错误)。
|
||||
@@ -720,6 +778,11 @@
|
||||
}
|
||||
async delete(tableName, query) {
|
||||
this.ensureTable(tableName);
|
||||
// v0.7.4: 防御 —— QueryBuilder 直通引擎不经 Executor 子查询解析,
|
||||
// 未解析的 $subquery/$col/$exists 在 matchWhere 中恒 false → 静默 0 行
|
||||
if (containsUnresolvedSubqueries(query.where)) {
|
||||
throw new DatabaseError('Unresolved subqueries/column references in DELETE WHERE (use db.query() to execute subqueries)', 'NOT_SUPPORTED');
|
||||
}
|
||||
const table = this.tables.get(tableName);
|
||||
const toDelete = [];
|
||||
for (const [pk, row] of table) {
|
||||
@@ -849,8 +912,11 @@
|
||||
throw error;
|
||||
}
|
||||
colDef.index = true;
|
||||
if (unique)
|
||||
if (unique) {
|
||||
colDef.unique = true;
|
||||
// v0.7.4: 记录唯一约束来源(DROP INDEX 时可解除;建表约束不可)
|
||||
this.uniqueIndexCols.add(`${tableName}:${column}`);
|
||||
}
|
||||
}
|
||||
async dropIndex(tableName, column, _indexName) {
|
||||
// v0.7.2: 同 createIndex —— 列级标志修改无法通过事务快照回滚,显式拒绝
|
||||
@@ -866,8 +932,17 @@
|
||||
if (!colDef.index && !colDef.unique) {
|
||||
throw new DatabaseError(`Index on column "${column}" does not exist in table "${tableName}"`, 'INDEX_NOT_FOUND');
|
||||
}
|
||||
// v0.7.4: 建表 UNIQUE 约束不可通过 DROP INDEX 解除 —— 此前 colDef.unique = false
|
||||
// 静默解除约束(后续唯一性检查失效、重复数据入库)。对齐 SQLite 语义:
|
||||
// 约束随建表存在,解除需重建表;仅 CREATE UNIQUE INDEX 添加的约束可随索引删除。
|
||||
const uniqueKey = `${tableName}:${column}`;
|
||||
if (colDef.unique && !this.uniqueIndexCols.has(uniqueKey)) {
|
||||
throw new DatabaseError(`Cannot drop index on column "${column}" in table "${tableName}": ` +
|
||||
'UNIQUE constraint defined at table creation must be removed by recreating the table', 'NOT_SUPPORTED');
|
||||
}
|
||||
colDef.index = false;
|
||||
colDef.unique = false;
|
||||
this.uniqueIndexCols.delete(uniqueKey);
|
||||
const tableIndexes = this.indexes.get(tableName);
|
||||
if (tableIndexes)
|
||||
tableIndexes.delete(column);
|
||||
@@ -941,6 +1016,11 @@
|
||||
if (colDef.required && (value === undefined || value === null)) {
|
||||
throw new DatabaseError(`Column "${colName}" is required in table "${schema.name}"`, 'VALIDATION_ERROR');
|
||||
}
|
||||
// v0.7.4: 主键列强制非空(SQL 语义 PK 隐含 NOT NULL)——
|
||||
// 此前 null/undefined 主键被 String() 化为 "null"/"undefined" 静默入库
|
||||
if (colDef.primaryKey && (value === undefined || value === null)) {
|
||||
throw new DatabaseError(`Primary key column "${colName}" in table "${schema.name}" cannot be null or undefined`, 'VALIDATION_ERROR');
|
||||
}
|
||||
if (value !== undefined && value !== null)
|
||||
this.checkType(colName, colDef.type, value);
|
||||
if (value !== undefined)
|
||||
@@ -1773,6 +1853,8 @@
|
||||
if (this.opened)
|
||||
return;
|
||||
this.dbName = dbName;
|
||||
// v0.7.4: 打开时同样清理后台错误状态(防 close/reopen 残留)
|
||||
this.lastBackgroundError = null;
|
||||
await this.medium.open(dbName);
|
||||
this.index = new Map();
|
||||
this.seq = 0;
|
||||
@@ -1855,6 +1937,9 @@
|
||||
this.index.clear();
|
||||
this.seq = 0;
|
||||
this.logBytes = 0;
|
||||
// v0.7.4: 清理后台错误状态 —— 此前跨 close/reopen 残留,
|
||||
// 重开后首次 checkpoint 会抛出上一次生命周期的旧错误
|
||||
this.lastBackgroundError = null;
|
||||
this.opened = false;
|
||||
}
|
||||
// =======================================================================
|
||||
@@ -2929,6 +3014,37 @@
|
||||
rangeScan(startKey, endKey, callback) {
|
||||
this._rangeScan(this.root, startKey, endKey, callback);
|
||||
}
|
||||
/**
|
||||
* v0.7.4: 惰性范围遍历(显式栈中序迭代 + 边界剪枝)。
|
||||
* 真流式扫描:生成器按需产出,提前终止(limit 达成)时剩余子树不再遍历。
|
||||
*/
|
||||
*scanLazy(startKey, endKey) {
|
||||
const stack = [];
|
||||
// 定位到 >= startKey 的最左节点(沿路入栈)
|
||||
let cur = this.root;
|
||||
while (cur) {
|
||||
if (cur.key >= startKey) {
|
||||
stack.push(cur);
|
||||
cur = cur.left;
|
||||
}
|
||||
else {
|
||||
cur = cur.right;
|
||||
}
|
||||
}
|
||||
while (stack.length > 0) {
|
||||
const node = stack.pop();
|
||||
// 中序递增:越过 endKey 后所有剩余节点均越界
|
||||
if (node.key > endKey)
|
||||
break;
|
||||
if (node.key >= startKey)
|
||||
yield [node.key, node.value];
|
||||
cur = node.right;
|
||||
while (cur) {
|
||||
stack.push(cur);
|
||||
cur = cur.left;
|
||||
}
|
||||
}
|
||||
}
|
||||
/** 获取所有条目 */
|
||||
getAllEntries() {
|
||||
const entries = [];
|
||||
@@ -2976,8 +3092,14 @@
|
||||
}
|
||||
else {
|
||||
const successor = this.minimum(node.right);
|
||||
if (successor.parent !== node) {
|
||||
this.transplant(successor, successor.right);
|
||||
// v0.7.4: 在 transplant 重连前捕获 successor 原右子与父 ——
|
||||
// x(双黑修复起点)= successor 原右子(占位在 successor 原位置)。
|
||||
// 此前 `successor.right?.parent ?? null` 在重赋值后取值:x 指向 node 右子树,
|
||||
// 且 null 时 parent 为 null → fixDelete 直接跳过修复(删除黑色节点后失衡)。
|
||||
const successorRight = successor.right;
|
||||
const successorParent = successor.parent;
|
||||
if (successorParent !== node) {
|
||||
this.transplant(successor, successorRight);
|
||||
successor.right = node.right;
|
||||
successor.right.parent = successor;
|
||||
}
|
||||
@@ -2986,8 +3108,13 @@
|
||||
successor.left.parent = successor;
|
||||
const origColor = successor.color;
|
||||
successor.color = node.color;
|
||||
if (origColor === Color.BLACK)
|
||||
this.fixDelete(successor.right, successor.right?.parent ?? null);
|
||||
if (origColor === Color.BLACK) {
|
||||
const x = successorRight;
|
||||
// x 为 null 占位:直接右子时其父为 successor(已移到 node 位置),
|
||||
// 间接右子时其父为 successor 原父(transplant 已把 x 接到其下)
|
||||
const xParent = x ? x.parent : (successorParent === node ? successor : successorParent);
|
||||
this.fixDelete(x, xParent);
|
||||
}
|
||||
}
|
||||
}
|
||||
transplant(u, v) {
|
||||
@@ -3252,6 +3379,10 @@
|
||||
this.tree.rangeScan(startKey, endKey, (k, v) => entries.push([k, v]));
|
||||
return entries;
|
||||
}
|
||||
/** v0.7.4: 惰性范围扫描(真流式,逐条产出) */
|
||||
scanLazy(startKey, endKey) {
|
||||
return this.tree.scanLazy(startKey, endKey);
|
||||
}
|
||||
/** 条目数 */
|
||||
getEntryCount() {
|
||||
return this.tree.size;
|
||||
@@ -3682,6 +3813,16 @@
|
||||
}
|
||||
/** 范围扫描 */
|
||||
rangeScan(startKey, endKey, callback) {
|
||||
// v0.7.4: 包装惰性生成器(行为一致,消除双份解析循环)
|
||||
for (const [key, value] of this.scanLazy(startKey, endKey)) {
|
||||
callback(key, value);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* v0.7.4: 惰性范围扫描 —— 生成器逐块逐条产出(真流式)。
|
||||
* 提前终止时未消费的块不再解析,大表流式内存 O(1)。
|
||||
*/
|
||||
*scanLazy(startKey, endKey) {
|
||||
if (this.indexEntries.length === 0)
|
||||
return;
|
||||
const startBlockIdx = Math.max(0, this.locateBlockGE(startKey));
|
||||
@@ -3696,7 +3837,7 @@
|
||||
for (let bi = startBlockIdx; bi <= endBlockIdx && bi >= 0; bi++) {
|
||||
const entry = this.indexEntries[bi];
|
||||
const blockData = this.getBlockData(entry);
|
||||
// v0.4.1-fix: 残缺块跳过(rangeScan 继续后续块,不抛异常)
|
||||
// v0.4.1-fix: 残缺块跳过(继续后续块,不抛异常)
|
||||
if (!blockData)
|
||||
continue;
|
||||
const blockView = new DataView(blockData.buffer, blockData.byteOffset, blockData.byteLength);
|
||||
@@ -3720,7 +3861,7 @@
|
||||
if (key >= startKey && key <= endKey) {
|
||||
try {
|
||||
const value = JSON.parse(new TextDecoder().decode(valBytes));
|
||||
callback(key, value);
|
||||
yield [key, value];
|
||||
}
|
||||
catch {
|
||||
// skip corrupted entry
|
||||
@@ -3909,6 +4050,23 @@
|
||||
this.index = 0;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* v0.7.4: 生成器数据源 —— 惰性迭代(真流式扫描)。
|
||||
* MergeIterator 的 next() 逐条拉取,生成器按需产出(findStream 提前终止时
|
||||
* 未消费部分不再物化,大表流式内存 O(1))。
|
||||
*/
|
||||
class GeneratorEntrySource {
|
||||
constructor(iter) {
|
||||
this.iter = iter;
|
||||
}
|
||||
next() {
|
||||
const r = this.iter.next();
|
||||
return r.done ? null : r.value;
|
||||
}
|
||||
reset() {
|
||||
// 生成器不可重置;MergeIterator 无 reset 消费方,接口保留
|
||||
}
|
||||
}
|
||||
/** 最小堆 */
|
||||
class MinHeap {
|
||||
constructor() {
|
||||
@@ -4375,16 +4533,21 @@
|
||||
}
|
||||
rangeScan(startKey, endKey) {
|
||||
const result = [];
|
||||
this.rangeScanLazy(startKey, endKey, (k, v) => result.push([k, v]));
|
||||
this.rangeScanLazy(startKey, endKey, (k, v) => { result.push([k, v]); });
|
||||
return result;
|
||||
}
|
||||
/** 惰性范围扫描:通过回调逐条返回,不一次性物化所有源 */
|
||||
/**
|
||||
* 惰性范围扫描:通过回调逐条返回,不一次性物化。
|
||||
* v0.7.4: 真惰性 —— 各源(MemTable/frozen/SSTable)以生成器接入 MergeIterator,
|
||||
* 逐条拉取;回调返回 false 时提前终止(未消费部分不再解析/物化)。
|
||||
* 此前实现内部 mergeIter.drain() 全量物化,与"流式不物化"宣称不符。
|
||||
*/
|
||||
rangeScanLazy(startKey, endKey, callback) {
|
||||
const mergeIter = new MergeIterator();
|
||||
mergeIter.addSource(new ArrayEntrySource(this.memtable.rangeScan(startKey, endKey)));
|
||||
mergeIter.addSource(new GeneratorEntrySource(this.memtable.scanLazy(startKey, endKey)));
|
||||
// pending frozen memtables(从新到旧,新数据 sourceIndex 更小)
|
||||
for (let i = this.frozenMemtables.length - 1; i >= 0; i--) {
|
||||
mergeIter.addSource(new ArrayEntrySource(this.frozenMemtables[i].rangeScan(startKey, endKey)));
|
||||
mergeIter.addSource(new GeneratorEntrySource(this.frozenMemtables[i].scanLazy(startKey, endKey)));
|
||||
}
|
||||
for (let level = 0; level < MAX_LSM_LEVELS; level++) {
|
||||
for (const meta of this.levels[level]) {
|
||||
@@ -4393,42 +4556,21 @@
|
||||
const reader = this.loadSSTableReader(meta);
|
||||
if (!reader)
|
||||
continue;
|
||||
reader.rangeScan(startKey, endKey, (k, v) => {
|
||||
mergeIter.addSource(new ArrayEntrySource([[k, v]]));
|
||||
});
|
||||
mergeIter.addSource(new GeneratorEntrySource(reader.scanLazy(startKey, endKey)));
|
||||
}
|
||||
}
|
||||
const merged = mergeIter.drain();
|
||||
for (const [k, v] of merged) {
|
||||
let entry = mergeIter.next();
|
||||
while (entry) {
|
||||
const [k, v] = entry;
|
||||
if (!v.__tombstone) {
|
||||
callback(k, v);
|
||||
const cont = callback(k, v);
|
||||
// v0.7.4: 提前终止(流式 limit 达成)
|
||||
if (cont === false)
|
||||
return;
|
||||
}
|
||||
entry = mergeIter.next();
|
||||
}
|
||||
}
|
||||
getAllEntries() {
|
||||
const result = new Map();
|
||||
// 从最旧层级开始聚合;同层级内从最旧到最新遍历,
|
||||
// 保证 result.set 覆盖时最终保留最新值
|
||||
for (let level = MAX_LSM_LEVELS - 1; level >= 0; level--) {
|
||||
for (let i = this.levels[level].length - 1; i >= 0; i--) {
|
||||
const meta = this.levels[level][i];
|
||||
const reader = this.loadSSTableReader(meta);
|
||||
if (!reader)
|
||||
continue;
|
||||
reader.scanAll((k, v) => result.set(k, v));
|
||||
}
|
||||
}
|
||||
// MemTable 覆盖(最新)
|
||||
for (const [k, v] of this.memtable.getAllEntries()) {
|
||||
result.set(k, v);
|
||||
}
|
||||
for (let i = this.frozenMemtables.length - 1; i >= 0; i--) {
|
||||
for (const [k, v] of this.frozenMemtables[i].getAllEntries()) {
|
||||
result.set(k, v);
|
||||
}
|
||||
}
|
||||
return Array.from(result.entries()).filter(([, v]) => !v.__tombstone);
|
||||
}
|
||||
// =======================================================================
|
||||
// Compaction
|
||||
// =======================================================================
|
||||
@@ -6490,6 +6632,13 @@
|
||||
this.opCounter = 0;
|
||||
// 二级索引:table.colKey → LSM
|
||||
this.secondaryIndexes = new Map();
|
||||
/**
|
||||
* v0.7.4: 由 CREATE UNIQUE INDEX 添加的 unique 列(table:col)。
|
||||
* 与建表 UNIQUE 约束区分:DROP INDEX 只允许解除索引来源的 unique,
|
||||
* 建表约束需重建表(对齐 SQLite 语义,此前静默解除且重启后永久消失)。
|
||||
* 注:重启后无法区分历史来源,schema 中的 unique 一律按建表约束保护(保守)。
|
||||
*/
|
||||
this.uniqueIndexCols = new Set();
|
||||
// MVCC 事务
|
||||
this.mvcc = new MVCCManager();
|
||||
this.currentTxnId = null;
|
||||
@@ -6702,6 +6851,7 @@
|
||||
this.schemas.clear();
|
||||
this.tablePKs.clear();
|
||||
this.secondaryIndexes.clear();
|
||||
this.uniqueIndexCols.clear();
|
||||
this.mvcc = new MVCCManager();
|
||||
this.currentTxnId = null;
|
||||
this.txnSnapshot = null;
|
||||
@@ -6798,6 +6948,7 @@
|
||||
this.schemas.clear();
|
||||
this.tablePKs.clear();
|
||||
this.secondaryIndexes.clear();
|
||||
this.uniqueIndexCols.clear();
|
||||
this.lsm.clear();
|
||||
this.mvcc = new MVCCManager();
|
||||
this.currentTxnId = null;
|
||||
@@ -6872,6 +7023,12 @@
|
||||
// v0.4.2-fix: 清理该表的全部二级索引 LSM 与持久化文件 —
|
||||
// 此前残留孤儿索引,重建同名表后旧索引数据污染新表(索引查询返回错误行)
|
||||
await this.cleanupTableIndexes(tableName);
|
||||
// v0.7.4: 清理该表的 unique 索引来源标记
|
||||
const uPrefix = `${tableName}:`;
|
||||
for (const k of this.uniqueIndexCols) {
|
||||
if (k.startsWith(uPrefix))
|
||||
this.uniqueIndexCols.delete(k);
|
||||
}
|
||||
this.schemas.delete(tableName);
|
||||
this.tablePKs.delete(tableName);
|
||||
await this.persistSchemas();
|
||||
@@ -7056,6 +7213,11 @@
|
||||
async update(tableName, query, updates) {
|
||||
this.ensureOpen();
|
||||
this.ensureTable(tableName);
|
||||
// v0.7.4: 防御 —— QueryBuilder 直通引擎不经 Executor 子查询解析,
|
||||
// 未解析的 $subquery/$col/$exists 在 matchWhere 中恒 false → 静默 0 行
|
||||
if (containsUnresolvedSubqueries(query.where)) {
|
||||
throw new DatabaseError('Unresolved subqueries/column references in UPDATE WHERE (use db.query() to execute subqueries)', 'NOT_SUPPORTED');
|
||||
}
|
||||
const schema = this.schemas.get(tableName);
|
||||
const rows = await this.getAllRows(tableName);
|
||||
let count = 0;
|
||||
@@ -7065,6 +7227,13 @@
|
||||
const visited = new Set();
|
||||
// v0.7.2: undefined 值视为"不更新该列"(保留旧值),null 显式置空
|
||||
const cleanUpdates = stripUndefinedUpdates(updates);
|
||||
// v0.7.4: 未知列显式报错 —— 此前 SET nonexistent = ... 被静默写入存储行
|
||||
// (validateRow 只遍历 schema 列,脏列残留在行内并随 SSTable 持久化)
|
||||
for (const col of Object.keys(cleanUpdates)) {
|
||||
if (!schema.columns[col]) {
|
||||
throw new DatabaseError(`Column "${col}" does not exist in table "${tableName}"`, 'COLUMN_NOT_FOUND');
|
||||
}
|
||||
}
|
||||
// v0.6.2: 唯一约束 — 批量预加载本批更新涉及的唯一列索引范围(一次 drainChain)
|
||||
const uniqueCols = this.uniqueColumns(tableName, schema);
|
||||
for (const colName of uniqueCols) {
|
||||
@@ -7273,6 +7442,11 @@
|
||||
async delete(tableName, query) {
|
||||
this.ensureOpen();
|
||||
this.ensureTable(tableName);
|
||||
// v0.7.4: 防御 —— QueryBuilder 直通引擎不经 Executor 子查询解析,
|
||||
// 未解析的 $subquery/$col/$exists 在 matchWhere 中恒 false → 静默 0 行
|
||||
if (containsUnresolvedSubqueries(query.where)) {
|
||||
throw new DatabaseError('Unresolved subqueries/column references in DELETE WHERE (use db.query() to execute subqueries)', 'NOT_SUPPORTED');
|
||||
}
|
||||
const rows = await this.getAllRows(tableName);
|
||||
let count = 0;
|
||||
// v0.3.1: 批量 WAL 写入(组提交)
|
||||
@@ -7485,14 +7659,15 @@
|
||||
}
|
||||
return count;
|
||||
}
|
||||
// 全表惰性扫描(含 WHERE 过滤,不物化)
|
||||
// 全表惰性扫描(含 WHERE 过滤,不物化;v0.7.4: callback 返回 false 提前终止,
|
||||
// 未消费的 SSTable 块 / 子树不再解析 —— 真流式,大表 limit 内存 O(1))
|
||||
await this.lsm.prefetchRange(prefix, `${prefix}\uffff`);
|
||||
this.lsm.rangeScanLazy(prefix, `${prefix}\uffff`, (key, value) => {
|
||||
if (count >= limit)
|
||||
return;
|
||||
return false;
|
||||
const row = { ...value };
|
||||
row[pkCol] = key.slice(prefix.length);
|
||||
emit(row);
|
||||
return emit(row);
|
||||
});
|
||||
return count;
|
||||
}
|
||||
@@ -7654,8 +7829,11 @@
|
||||
throw error;
|
||||
}
|
||||
colDef.index = true;
|
||||
if (unique)
|
||||
if (unique) {
|
||||
colDef.unique = true;
|
||||
// v0.7.4: 记录唯一约束来源(DROP INDEX 时可解除;建表约束不可)
|
||||
this.uniqueIndexCols.add(`${tableName}:${column}`);
|
||||
}
|
||||
await this.persistSchemas();
|
||||
}
|
||||
async dropIndex(tableName, column, _indexName) {
|
||||
@@ -7674,8 +7852,17 @@
|
||||
if (!colDef.index && !colDef.unique && !this.secondaryIndexes.has(`${tableName}:idx:${column}`)) {
|
||||
throw new DatabaseError(`Index on column "${column}" does not exist in table "${tableName}"`, 'INDEX_NOT_FOUND');
|
||||
}
|
||||
// v0.7.4: 建表 UNIQUE 约束不可通过 DROP INDEX 解除 —— 此前 colDef.unique = false
|
||||
// 静默解除约束(重启后 persistSchemas 使约束永久消失)。对齐 SQLite 语义:
|
||||
// 约束随建表存在,解除需重建表;仅 CREATE UNIQUE INDEX 添加的约束可随索引删除。
|
||||
const uniqueKey = `${tableName}:${column}`;
|
||||
if (colDef.unique && !this.uniqueIndexCols.has(uniqueKey)) {
|
||||
throw new DatabaseError(`Cannot drop index on column "${column}" in table "${tableName}": ` +
|
||||
'UNIQUE constraint defined at table creation must be removed by recreating the table', 'NOT_SUPPORTED');
|
||||
}
|
||||
colDef.index = false;
|
||||
colDef.unique = false;
|
||||
this.uniqueIndexCols.delete(uniqueKey);
|
||||
const idxKey = `${tableName}:idx:${column}`;
|
||||
const idxLsm = this.secondaryIndexes.get(idxKey);
|
||||
if (idxLsm) {
|
||||
@@ -7894,6 +8081,11 @@
|
||||
if (colDef.required && (value === undefined || value === null)) {
|
||||
throw new DatabaseError(`Column "${colName}" is required in table "${schema.name}"`, 'VALIDATION_ERROR');
|
||||
}
|
||||
// v0.7.4: 主键列强制非空(SQL 语义 PK 隐含 NOT NULL)——
|
||||
// 此前 null/undefined 主键被 String() 化为 "null"/"undefined" 静默入库
|
||||
if (colDef.primaryKey && (value === undefined || value === null)) {
|
||||
throw new DatabaseError(`Primary key column "${colName}" in table "${schema.name}" cannot be null or undefined`, 'VALIDATION_ERROR');
|
||||
}
|
||||
if (value !== undefined && value !== null) {
|
||||
this.checkType(colName, colDef.type, value, colDef);
|
||||
}
|
||||
@@ -8422,10 +8614,15 @@
|
||||
if (!schema)
|
||||
return 0;
|
||||
let rebuiltCount = 0;
|
||||
for (const [colName, colDef] of Object.entries(schema.columns)) {
|
||||
// v0.3.3: 主键列不建冗余二级索引(主 LSM 即 PK 索引)
|
||||
if (!colDef.index && !colDef.unique)
|
||||
continue;
|
||||
// v0.7.4-perf: 单次全表扫描重建全部索引列 —— 此前每个索引列各做一次
|
||||
// getAllRows(N 列 × M 行全表扫描 + 每次 prefetchRange drainChain),
|
||||
// 多索引大表 REINDEX/崩溃恢复按索引列数线性放大
|
||||
const pkCol = this.tablePKs.get(tableName);
|
||||
const idxCols = Object.entries(schema.columns).filter(([, colDef]) => colDef.index || colDef.unique);
|
||||
if (idxCols.length === 0)
|
||||
return 0;
|
||||
const rows = await this.getAllRows(tableName);
|
||||
for (const [colName] of idxCols) {
|
||||
const idxKey = `${tableName}:idx:${colName}`;
|
||||
const idxLsm = this.secondaryIndexes.get(idxKey);
|
||||
if (!idxLsm)
|
||||
@@ -8434,11 +8631,10 @@
|
||||
await idxLsm.clear();
|
||||
rebuiltCount++;
|
||||
// 从主 LSM 重建索引
|
||||
const rows = await this.getAllRows(tableName);
|
||||
for (const row of rows) {
|
||||
const val = row[colName];
|
||||
if (val !== undefined && val !== null) {
|
||||
idxLsm.put(`${String(val)}:${row[this.tablePKs.get(tableName)]}`, { pk: row[this.tablePKs.get(tableName)] });
|
||||
idxLsm.put(`${String(val)}:${row[pkCol]}`, { pk: row[pkCol] });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8735,7 +8931,15 @@
|
||||
// ---- 事务 ----
|
||||
async beginTransaction() {
|
||||
await this.memoryEngine.beginTransaction();
|
||||
await this.diskEngine.beginTransaction();
|
||||
// v0.7.4: 磁盘 begin 失败时补偿回滚内存快照 —— 此前内存已 begin、
|
||||
// 磁盘抛错 → 内存快照泄漏(后续所有事务报 TX_ACTIVE)
|
||||
try {
|
||||
await this.diskEngine.beginTransaction();
|
||||
}
|
||||
catch (error) {
|
||||
await this.memoryEngine.rollbackTransaction();
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
async commitTransaction() {
|
||||
// 先写磁盘,保证持久化优先;磁盘失败则回滚内存
|
||||
@@ -10696,6 +10900,30 @@
|
||||
*
|
||||
* JOIN / GROUP BY / DISTINCT 逻辑在此层处理。
|
||||
*/
|
||||
// ---------------------------------------------------------------------------
|
||||
// 分组 / 去重键编码(v0.7.4)
|
||||
// ---------------------------------------------------------------------------
|
||||
/**
|
||||
* v0.7.4: 分组/去重键的类型安全编码 —— 此前 `String(v ?? 'null')` 使
|
||||
* null 与字符串 'null' 合并为一组(GROUP BY 静默少组),`String(v ?? '\0')`
|
||||
* 使 null/undefined/'\0' 在 DISTINCT/UNION 中互相吞并。类型前缀编码后
|
||||
* 各类型独立,仅同类型同值合并(与 where-matcher 的 === 语义一致)。
|
||||
*/
|
||||
function encodeGroupKey(v) {
|
||||
if (v === null)
|
||||
return 'n';
|
||||
if (v === undefined)
|
||||
return 'u';
|
||||
if (typeof v === 'string')
|
||||
return `s${v}`;
|
||||
if (typeof v === 'number')
|
||||
return `d${v}`;
|
||||
if (typeof v === 'boolean')
|
||||
return `b${v}`;
|
||||
if (typeof v === 'object')
|
||||
return `o${JSON.stringify(v)}`;
|
||||
return `x${String(v)}`;
|
||||
}
|
||||
/** 解析 "CASE WHEN c1 THEN v1 WHEN c2 THEN v2 ELSE v3 END [AS alias]" */
|
||||
function parseCaseExpression(expr) {
|
||||
const m = expr.match(/^\s*CASE\s+([\s\S]*?)\s+END\s*(?:AS\s+(\w+))?\s*$/i);
|
||||
@@ -10803,7 +11031,7 @@
|
||||
const seen = new Set();
|
||||
const result = [];
|
||||
for (const row of normalized) {
|
||||
const key = Object.values(row).map((v) => String(v ?? '\0')).join('\x1f');
|
||||
const key = Object.values(row).map(encodeGroupKey).join('\x1f');
|
||||
if (!seen.has(key)) {
|
||||
seen.add(key);
|
||||
result.push(row);
|
||||
@@ -10811,7 +11039,7 @@
|
||||
}
|
||||
for (const row of rightRows) {
|
||||
const projected = this.projectUnionRow(row, leftCols);
|
||||
const key = Object.values(projected).map((v) => String(v ?? '\0')).join('\x1f');
|
||||
const key = Object.values(projected).map(encodeGroupKey).join('\x1f');
|
||||
if (!seen.has(key)) {
|
||||
seen.add(key);
|
||||
result.push(projected);
|
||||
@@ -10850,7 +11078,10 @@
|
||||
}
|
||||
else if (stmt.query.type === 'UPDATE' || stmt.query.type === 'DELETE') {
|
||||
try {
|
||||
// v0.7.4: 子查询解析后估算 —— 此前 $subquery 未解析使 count 恒 0
|
||||
await this.resolveWriteWhere(stmt.query);
|
||||
const plan = compileStatement(stmt.query);
|
||||
plan.where = stmt.query.where;
|
||||
rows = await this.engine.count(plan.table, plan);
|
||||
}
|
||||
catch {
|
||||
@@ -11256,7 +11487,9 @@
|
||||
executeGroupBy(rows, stmt) {
|
||||
const groups = new Map();
|
||||
for (const row of rows) {
|
||||
const key = stmt.groupBy.map((col) => String(row[col] ?? 'null')).join('|');
|
||||
// v0.7.4: 类型安全键编码 —— 此前 String(row[col] ?? 'null') 使
|
||||
// null 与字符串 'null' 合并为一组(GROUP BY 静默少组)
|
||||
const key = stmt.groupBy.map((col) => encodeGroupKey(row[col])).join('\x1f');
|
||||
if (!groups.has(key))
|
||||
groups.set(key, []);
|
||||
groups.get(key).push(row);
|
||||
@@ -11327,7 +11560,8 @@
|
||||
executeDistinct(rows) {
|
||||
const seen = new Set();
|
||||
return rows.filter((row) => {
|
||||
const key = Object.values(row).map((v) => String(v ?? '\0')).join('\x1f');
|
||||
// v0.7.4: 类型安全键编码(null 与 'null' 字符串、'\0' 分离)
|
||||
const key = Object.values(row).map(encodeGroupKey).join('\x1f');
|
||||
if (seen.has(key))
|
||||
return false;
|
||||
seen.add(key);
|
||||
@@ -11383,13 +11617,37 @@
|
||||
return this.engine.insert(stmt.into, rows);
|
||||
}
|
||||
async executeUpdate(stmt) {
|
||||
// v0.7.4: 先解析 WHERE 子查询 —— 此前直接 compileStatement 调引擎:
|
||||
// 引擎层 matchWhere 的 $in/$nin 遇未解析的 $subquery 对象恒 false →
|
||||
// 所有行不匹配,UPDATE 静默影响 0 行(与 queryStream v0.7.3 修复同类)。
|
||||
await this.resolveWriteWhere(stmt);
|
||||
const plan = compileStatement(stmt);
|
||||
plan.where = stmt.where;
|
||||
return this.engine.update(plan.table, plan, stmt.sets);
|
||||
}
|
||||
async executeDelete(stmt) {
|
||||
// v0.7.4: 同 executeUpdate —— DELETE 子查询 WHERE 此前静默删除 0 行
|
||||
await this.resolveWriteWhere(stmt);
|
||||
const plan = compileStatement(stmt);
|
||||
plan.where = stmt.where;
|
||||
return this.engine.delete(plan.table, plan);
|
||||
}
|
||||
/**
|
||||
* v0.7.4: 写语句(UPDATE/DELETE)WHERE 的子查询解析。
|
||||
* 非关联子查询($subquery)解析为具体值列表/标量;
|
||||
* 关联引用($col / 关联 EXISTS)在写语句中无法逐行绑定外层上下文
|
||||
* (引擎层 matchWhere 无 $col 绑定选项)→ 显式 NOT_SUPPORTED 而非静默 0 行。
|
||||
*/
|
||||
async resolveWriteWhere(stmt) {
|
||||
const where = stmt.where;
|
||||
if (!where || Object.keys(where).length === 0)
|
||||
return where ?? {};
|
||||
if (this.hasCorrelatedRefs(where)) {
|
||||
throw new DatabaseError('Correlated subqueries and column references are not supported in UPDATE/DELETE WHERE clauses', 'NOT_SUPPORTED');
|
||||
}
|
||||
stmt.where = await this.resolveSubqueries(where);
|
||||
return stmt.where;
|
||||
}
|
||||
async executeCreateTable(stmt) {
|
||||
// IF NOT EXISTS: 表已存在时静默返回
|
||||
if (stmt.ifNotExists) {
|
||||
@@ -12504,7 +12762,14 @@
|
||||
*/
|
||||
async queryStream(sql, onRow) {
|
||||
this.ensureReady();
|
||||
const stmt = parseAll(sql)[0];
|
||||
// v0.7.4: 多语句显式拒绝 —— 此前 parseAll(sql)[0] 静默忽略后续语句:
|
||||
// 可流式时后续语句(如 DELETE)不执行,不可流式时回退 query() 却会执行
|
||||
// 全部语句 → 同一条 SQL 两种语义。流式 API 要求单条 SELECT。
|
||||
const statements = parseAll(sql);
|
||||
if (statements.length !== 1) {
|
||||
throw new DatabaseError('queryStream requires exactly one SELECT statement', 'PARSE_ERROR');
|
||||
}
|
||||
const stmt = statements[0];
|
||||
if (!stmt || stmt.type !== 'SELECT') {
|
||||
throw new DatabaseError('queryStream only supports SELECT statements', 'NOT_SUPPORTED');
|
||||
}
|
||||
|
||||
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
+1
-1
@@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>⚡ 性能基准 — MetonaSqlark v0.7.3</title>
|
||||
<title>⚡ 性能基准 — MetonaSqlark v0.7.4</title>
|
||||
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'><rect width='32' height='32' rx='8' fill='%236366f1'/><text x='16' y='22' text-anchor='middle' font-size='20' fill='white'>◈</text></svg>">
|
||||
<style>
|
||||
:root {
|
||||
|
||||
+4
-4
@@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>🧪 在线演示 — MetonaSqlark v0.7.3</title>
|
||||
<title>🧪 在线演示 — MetonaSqlark v0.7.4</title>
|
||||
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'><rect width='32' height='32' rx='8' fill='%236366f1'/><text x='16' y='22' text-anchor='middle' font-size='20' fill='white'>◈</text></svg>">
|
||||
<style>
|
||||
:root {
|
||||
@@ -84,7 +84,7 @@
|
||||
<a href="demo.html" class="nav-active">演示</a>
|
||||
<a href="benchmark.html">基准</a>
|
||||
</nav>
|
||||
<div class="status"><span class="dot" id="engine-dot"></span> <span id="engine-status">Memory</span> 模式 — v0.7.3</div>
|
||||
<div class="status"><span class="dot" id="engine-dot"></span> <span id="engine-status">Memory</span> 模式 — v0.7.4</div>
|
||||
<button class="btn btn-preset" onclick="switchEngine('memory')" id="btn-memory" style="margin:6px 4px 6px 0;padding:6px 12px;">⚡ Memory</button>
|
||||
<button class="btn btn-preset" onclick="switchEngine('aria')" id="btn-aria" style="margin:6px 0;padding:6px 12px;color:#ec4899;border-color:#ec4899;">🌲 Aria</button>
|
||||
</header>
|
||||
@@ -92,7 +92,7 @@
|
||||
<div class="main">
|
||||
<div class="editor-panel">
|
||||
<div class="editor-area">
|
||||
<textarea id="sql-input" placeholder="输入 SQL 语句... SELECT * FROM users; INSERT INTO users VALUES ('4', 'Diana', 'diana@test.com', 28); SELECT u.name, o.amount FROM users u INNER JOIN orders o ON u.id = o.user_id;">-- 🚀 MetonaSqlark v0.7.3 在线演示
|
||||
<textarea id="sql-input" placeholder="输入 SQL 语句... SELECT * FROM users; INSERT INTO users VALUES ('4', 'Diana', 'diana@test.com', 28); SELECT u.name, o.amount FROM users u INNER JOIN orders o ON u.id = o.user_id;">-- 🚀 MetonaSqlark v0.7.4 在线演示
|
||||
-- 已预置 users / orders / products 表数据
|
||||
-- 新特性: ALTER TABLE · TRUNCATE TABLE · WAL同步 · MVCC · SQL注入防护
|
||||
|
||||
@@ -555,7 +555,7 @@ SELECT COUNT(*) as total FROM temp_logs;
|
||||
|
||||
-- 清理
|
||||
DROP TABLE temp_logs;`,
|
||||
aria: `-- 🌲 AriaEngine 演示 (v0.7.3)
|
||||
aria: `-- 🌲 AriaEngine 演示 (v0.7.4)
|
||||
-- 点击右上角「🌲 Aria」按钮切换数据库引擎到 AriaEngine
|
||||
-- 当前数据库即运行在 Aria 引擎上(LSM-Tree · WAL 崩溃恢复 · MVCC · BloomFilter)
|
||||
-- 基础 CRUD 与 Memory 引擎完全兼容
|
||||
|
||||
+21
-4
@@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>📖 API 文档 — MetonaSqlark v0.7.3</title>
|
||||
<title>📖 API 文档 — MetonaSqlark v0.7.4</title>
|
||||
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'><rect width='32' height='32' rx='8' fill='%236366f1'/><text x='16' y='22' text-anchor='middle' font-size='20' fill='white'>◈</text></svg>">
|
||||
<style>
|
||||
:root {
|
||||
@@ -291,7 +291,12 @@ db.<span class="f">isReady</span>(); <span class="c">// true</span>
|
||||
|
||||
<span class="c">// CREATE / DROP INDEX — 动态二级索引</span>
|
||||
<span class="k">await</span> db.<span class="f">query</span>(<span class="s">'CREATE INDEX idx_users_city ON users (city)'</span>);
|
||||
<span class="k">await</span> db.<span class="f">query</span>(<span class="s">'DROP INDEX idx_users_city ON users (city)'</span>);</pre>
|
||||
<span class="k">await</span> db.<span class="f">query</span>(<span class="s">'DROP INDEX idx_users_city ON users (city)'</span>);
|
||||
|
||||
<span class="c">// v0.7.4: 建表 UNIQUE 约束不可经 DROP INDEX 解除(NOT_SUPPORTED,重建表解除);
|
||||
// 仅 CREATE UNIQUE INDEX 添加的唯一约束可随 DROP INDEX 一并删除(对齐 SQLite)</span>
|
||||
<span class="k">await</span> db.<span class="f">query</span>(<span class="s">'CREATE UNIQUE INDEX idx_u ON users (email)'</span>);
|
||||
<span class="k">await</span> db.<span class="f">query</span>(<span class="s">'DROP INDEX idx_u ON users (email)'</span>); <span class="c">// unique 随之解除</span></pre>
|
||||
|
||||
<h3>维护语句 (v0.5.1 SQL 入口)</h3>
|
||||
<pre><span class="c">// EXPLAIN — 查询计划(真实索引命中信息 v0.7.0)</span>
|
||||
@@ -466,7 +471,16 @@ db.<span class="f">isReady</span>(); <span class="c">// true</span>
|
||||
|
||||
<span class="c">// NOT IN 子查询</span>
|
||||
<span class="k">await</span> db.<span class="f">query</span>(<span class="s">`SELECT * FROM users
|
||||
WHERE id NOT IN (SELECT user_id FROM orders)`</span>);</pre>
|
||||
WHERE id NOT IN (SELECT user_id FROM orders)`</span>);
|
||||
|
||||
<span class="c">// v0.7.4: UPDATE / DELETE 的 WHERE 同样支持子查询(四引擎)
|
||||
// 此前未解析的 $subquery 在引擎层恒 false → 静默影响 0 行</span>
|
||||
<span class="k">await</span> db.<span class="f">query</span>(<span class="s">`UPDATE users SET role = 'vip'
|
||||
WHERE id IN (SELECT user_id FROM orders WHERE amount > 100)`</span>);
|
||||
<span class="k">await</span> db.<span class="f">query</span>(<span class="s">`DELETE FROM logs
|
||||
WHERE ts < (SELECT MIN(ts) FROM keep_logs)`</span>);
|
||||
|
||||
<span class="c">// 写语句中关联引用($col / EXISTS 引用外层行)显式 NOT_SUPPORTED(不静默)</span></pre>
|
||||
|
||||
<h2 id="alter-table">🏗 ALTER TABLE (🆕 v0.2.5)</h2>
|
||||
<p>v0.2.5 新增 ALTER TABLE 语法,支持动态添加和删除列。</p>
|
||||
@@ -799,7 +813,8 @@ db.<span class="f">broadcastChange</span>(<span class="s">'users'</span>);</pre>
|
||||
<strong>v0.7.0 参数化查询 / 事务性能 / 语义硬化</strong> — <strong>参数化查询</strong>(<code>db.query(sql, params)</code> 位置参数 `?`,词法层绑定 + SQL 字面量安全编码,注入防护从根上成立)· EXPLAIN 真实索引命中信息(pk / index:col)· KVStoreEngine 事务行级增量 flush(大表事务改 1 行 commit 仅 1 条记录)· 移除每次 commit 强制全量 checkpoint · 复合主键显式拒绝(SCHEMA_ERROR,v0.8 路线图)。<br>
|
||||
<strong>v0.7.1 API 修复与防御统一</strong> — <code>MetonaSqlark.create</code> 静态工厂(README 示例在 ESM/Node 下可用)· close 未初始化崩溃防御 · AriaEngine 未 open 防护统一 · 事务回滚失败不掩盖原始错误 · <code>__proto__</code> 列名原型污染防护 · lint 清零。<br>
|
||||
<strong>v0.7.2 语句级原子性 / 事务 DDL / 约束硬化</strong> — UPDATE 语句级两阶段原子(多行匹配第 N 行失败整句不执行 + 批内唯一互查)· 事务内 DDL 显式拒绝(四引擎对齐)· SET NULL 级联绕过 required 约束整体拒绝 · 参数绑定注释感知(注释中 `?`/引号不参与绑定)· 未闭合字符串显式 PARSE_ERROR · 未知 where 操作符抛 QUERY_ERROR(此前静默全匹配)· update undefined 语义化(保留旧值)· Hybrid write-through 失败补偿(磁盘失败自动重载内存对齐)。<br>
|
||||
<strong>v0.7.3 深度审计第六阶段:INSERT 原子 / 索引一致性 / 边界窗口</strong> — INSERT 语句级两阶段原子(三引擎 + Aria PK 批内重复,此前部分提交)· 索引列 IS NULL 恒空修复(Memory/KVStore/Hybrid,对齐 Aria)· delete RESTRICT 预检不再破坏索引 · queryStream 子查询静默空结果修复($subquery/$exists/$col 回退物化)· ALTER DROP 索引列残留清理 · CREATE UNIQUE INDEX 存量重复数据校验(失败原子回滚)· <code>SELECT *, col AS alias</code> 解析与投影 · WAL BEGIN/ROLLBACK 写失败窗口修复(事务不泄漏/数据不复活)· aria $in 批级预加载(消除逐值 drainChain 性能悬崖)· 多条件 AND 等值下推(索引真正生效,EXPLAIN 同步)· ANALYZE 统计含二级索引 · React/Vue hooks 生命周期修复(config 变更重建 / 卸载关闭)· 迁移无主键旧库兜底 · 1256 测试 75 套件 · 90.0% 行覆盖率。</p>
|
||||
<strong>v0.7.3 深度审计第六阶段:INSERT 原子 / 索引一致性 / 边界窗口</strong> — INSERT 语句级两阶段原子(三引擎 + Aria PK 批内重复,此前部分提交)· 索引列 IS NULL 恒空修复(Memory/KVStore/Hybrid,对齐 Aria)· delete RESTRICT 预检不再破坏索引 · queryStream 子查询静默空结果修复($subquery/$exists/$col 回退物化)· ALTER DROP 索引列残留清理 · CREATE UNIQUE INDEX 存量重复数据校验(失败原子回滚)· <code>SELECT *, col AS alias</code> 解析与投影 · WAL BEGIN/ROLLBACK 写失败窗口修复(事务不泄漏/数据不复活)· aria $in 批级预加载(消除逐值 drainChain 性能悬崖)· 多条件 AND 等值下推(索引真正生效,EXPLAIN 同步)· ANALYZE 统计含二级索引 · React/Vue hooks 生命周期修复(config 变更重建 / 卸载关闭)· 迁移无主键旧库兜底 · 1256 测试 75 套件 · 90.0% 行覆盖率。<br>
|
||||
<strong>v0.7.4 深度审计第七阶段:写语句子查询 / 约束硬化 / 真惰性流式</strong> — UPDATE/DELETE WHERE 子查询正确执行(四引擎,此前静默 0 行;关联引用显式 NOT_SUPPORTED;EXPLAIN 估算同步修复)· 主键 NULL/undefined 强制拒绝(SQL 语义 PK 隐含 NOT NULL,此前静默生成 "null"/"undefined" 主键)· DROP INDEX 保留建表 UNIQUE 约束(对齐 SQLite:需重建表解除;仅 CREATE UNIQUE INDEX 添加的可随索引删除)· GROUP BY / DISTINCT / UNION 键类型安全编码(null 与 'null' 字符串不再合并)· UPDATE 未知列显式 COLUMN_NOT_FOUND(此前脏列写入存储行)· queryStream 多语句显式 PARSE_ERROR(此前静默忽略后续语句)· KVStore 后台错误跨 reopen 清理 + Hybrid beginTransaction 失败补偿回滚 · <strong>Aria findStream 真惰性</strong>(MergeIterator 迭代器化 + SSTable/MemTable 生成器扫描,limit 提前终止,大表流式内存 O(1)——此前内部 drain 全量物化)· REINDEX 单次全表扫描重建全部索引列(此前每列一次全扫描)· RB-Tree 删除双黑修复边界 + LSM 死代码清理 · 1304 测试 76 套件 · 90.1% 行覆盖率。</p>
|
||||
|
||||
<h3>存储模式对比</h3>
|
||||
<table>
|
||||
@@ -903,6 +918,8 @@ db.<span class="f">broadcastChange</span>(<span class="s">'users'</span>);</pre>
|
||||
<tr><td><code>NOT_SUPPORTED</code></td><td>引擎不支持的操作(如 Memory 引擎执行 ANALYZE / VACUUM / SAVEPOINT)🆕 v0.5.1</td></tr>
|
||||
<tr><td><code>PARAM_ERROR</code></td><td>参数绑定错误:数量不匹配 / 对象数组参数 🆕 v0.7.0</td></tr>
|
||||
<tr><td><code>QUERY_ERROR</code></td><td>未知 where 操作符(如拼错的 <code>$betwen</code>)🆕 v0.7.2</td></tr>
|
||||
<tr><td><code>COLUMN_NOT_FOUND</code></td><td>列不存在:UPDATE 未知列 / ALTER DROP 不存在列(v0.7.4 UPDATE 未知列显式报错)</td></tr>
|
||||
<tr><td><code>INDEX_NOT_FOUND</code></td><td>DROP 不存在的索引</td></tr>
|
||||
</table>
|
||||
|
||||
</main>
|
||||
|
||||
+6
-6
@@ -153,7 +153,7 @@
|
||||
<!-- Hero -->
|
||||
<section class="hero">
|
||||
<div class="container">
|
||||
<div class="badge" style="margin-bottom:24px;"><span class="dot"></span> v0.7.3 数据正确性深度审计 — 1256测试 75套件 · 90.0% 覆盖率 · INSERT/UPDATE 语句级原子 · 参数化查询 · 自研 KVStore 事务引擎 · AriaEngine LSM+WAL+MVCC · 崩溃恢复</div>
|
||||
<div class="badge" style="margin-bottom:24px;"><span class="dot"></span> v0.7.4 写语句子查询 + 约束硬化 + 真惰性流式 — 1304测试 76套件 · 90.1% 覆盖率 · UPDATE/DELETE 子查询正确执行 · 主键非空强制 · DROP INDEX 保留 UNIQUE 约束 · findStream 真惰性 · 参数化查询 · 自研 KVStore 事务引擎 · AriaEngine LSM+WAL+MVCC · 崩溃恢复</div>
|
||||
<h1>前端的 <span class="gradient-text">SQL 数据库</span></h1>
|
||||
<p>TypeScript 原生构建,5 种存储引擎,支持完整 SQL 查询。<br>零运行时依赖,开箱即用。AriaEngine 自研引擎:LSM-Tree + WAL 同步 + MVCC。</p>
|
||||
<div class="actions">
|
||||
@@ -243,7 +243,7 @@ npm install @metona-team/metona-sqlark
|
||||
</div>
|
||||
<div class="feature-card">
|
||||
<div class="icon">🌲</div>
|
||||
<h3>AriaEngine <span style="font-size:0.65rem;color:var(--accent);vertical-align:super;">v0.7.3</span></h3>
|
||||
<h3>AriaEngine <span style="font-size:0.65rem;color:var(--accent);vertical-align:super;">v0.7.4</span></h3>
|
||||
<p>自研 LSM-Tree 存储引擎:SSTable 4KB 页面化物理存储(BufferPool LRU)、WAL 分片文件原子写入、全库 AES-GCM 透明加密、标准 CRC-32 完整性校验、MVCC 快照隔离、二级索引跨重启恢复、ON UPDATE/DELETE 外键级联、崩溃恢复自愈。</p>
|
||||
</div>
|
||||
<div class="feature-card">
|
||||
@@ -314,7 +314,7 @@ npm install @metona-team/metona-sqlark
|
||||
<div class="feature-card">
|
||||
<div class="icon">🚰</div>
|
||||
<h3>流式查询 + 派生表</h3>
|
||||
<p>queryStream 逐行回调不物化结果集(大表友好)。FROM (SELECT ...) 派生表、多列哈希连接、COUNT(DISTINCT)、NULLS FIRST/LAST。</p>
|
||||
<p>queryStream 逐行回调真惰性扫描(v0.7.4:Aria 引擎迭代器化,limit 提前终止,大表内存 O(1))。FROM (SELECT ...) 派生表、多列哈希连接、COUNT(DISTINCT)、NULLS FIRST/LAST。</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -414,12 +414,12 @@ npm install @metona-team/metona-sqlark
|
||||
<p>MetonaSqlark 的核心指标</p>
|
||||
</div>
|
||||
<div class="stats">
|
||||
<div class="stat-card"><div class="num">1256</div><div class="label">测试用例</div></div>
|
||||
<div class="stat-card"><div class="num">90.0%</div><div class="label">行覆盖率</div></div>
|
||||
<div class="stat-card"><div class="num">1304</div><div class="label">测试用例</div></div>
|
||||
<div class="stat-card"><div class="num">90.1%</div><div class="label">行覆盖率</div></div>
|
||||
<div class="stat-card"><div class="num">~27KB</div><div class="label">gzip 体积</div></div>
|
||||
<div class="stat-card"><div class="num">5</div><div class="label">存储引擎</div></div>
|
||||
<div class="stat-card"><div class="num">72</div><div class="label">SQL 关键字</div></div>
|
||||
<div class="stat-card"><div class="num">75</div><div class="label">测试套件</div></div>
|
||||
<div class="stat-card"><div class="num">76</div><div class="label">测试套件</div></div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
Reference in New Issue
Block a user