feat(grid-db): implement Phase 1 — B+Tree, NamespaceManager, RangeScanner, NearbyQuery, SecondaryIndex, EventLog enhancements

ZY-GDB-P1-001: B+Tree index with insert/find/delete/range/serialization
ZY-GDB-P1-002: NamespaceManager with create/delete/get/list/exists/validation
ZY-GDB-P1-003: RangeScanner for rectangular area queries
ZY-GDB-P1-004: NearbyQuery for proximity search with Euclidean distance
ZY-GDB-P1-005: EventLog replay/replayFromSeqNo/getByOperation/tianyanHook
ZY-GDB-P1-006: SecondaryIndex for one-to-many field indexing

All Phase 0 tests (95/95) and Phase 1 tests (101/101) pass.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot] 2026-03-26 08:43:56 +00:00 committed by GitHub
parent ef35fead3e
commit 524abb4c97
8 changed files with 1289 additions and 2 deletions

View File

@ -0,0 +1,137 @@
// grid-db/src/core/namespace.js
// Grid-DB · 命名空间管理器
// 命名空间隔离与元数据管理
// PRJ-GDB-001 · Phase 1 · ZY-GDB-P1-002
// 版权:国作登字-2026-A-00037559
'use strict';
/**
* NamespaceManager 命名空间管理器
*
* 本体论锚定命名空间 = 纸的不同区域
* 同一张纸上不同区域的格子互不干涉
* 每个区域有自己的名字创建时间和格点统计
*
* 命名空间名称规则非空字符串仅允许字母数字连字符下划线
*/
const NAME_PATTERN = /^[a-zA-Z0-9_-]+$/;
class NamespaceManager {
/**
* @param {object} [options]
*/
constructor(options = {}) {
/** @type {Map<string, object>} */
this._namespaces = new Map();
}
/**
* 创建命名空间
* @param {string} name 命名空间名称
* @param {object} [metadata] 元数据
* @returns {object} 创建的命名空间信息
*/
create(name, metadata = {}) {
this._validateName(name);
if (this._namespaces.has(name)) {
throw new Error(`NamespaceManager: 命名空间 '${name}' 已存在`);
}
const ns = {
name,
metadata,
createdAt: new Date().toISOString(),
cellCount: 0
};
this._namespaces.set(name, ns);
return { ...ns };
}
/**
* 删除命名空间
* @param {string} name
* @returns {boolean} 是否删除成功
*/
delete(name) {
if (!this._namespaces.has(name)) {
return false;
}
this._namespaces.delete(name);
return true;
}
/**
* 获取命名空间信息
* @param {string} name
* @returns {object|null}
*/
get(name) {
const ns = this._namespaces.get(name);
if (!ns) return null;
return { ...ns };
}
/**
* 列出所有命名空间
* @returns {object[]}
*/
list() {
return Array.from(this._namespaces.values()).map(ns => ({ ...ns }));
}
/**
* 检查命名空间是否存在
* @param {string} name
* @returns {boolean}
*/
exists(name) {
return this._namespaces.has(name);
}
/**
* 增加命名空间的格点计数
* @param {string} name
*/
incrementCellCount(name) {
const ns = this._namespaces.get(name);
if (!ns) {
throw new Error(`NamespaceManager: 命名空间 '${name}' 不存在`);
}
ns.cellCount++;
}
/**
* 减少命名空间的格点计数
* @param {string} name
*/
decrementCellCount(name) {
const ns = this._namespaces.get(name);
if (!ns) {
throw new Error(`NamespaceManager: 命名空间 '${name}' 不存在`);
}
if (ns.cellCount > 0) {
ns.cellCount--;
}
}
// ── 内部方法 ──
/**
* 验证命名空间名称
* @param {string} name
*/
_validateName(name) {
if (!name || typeof name !== 'string') {
throw new Error('NamespaceManager: 命名空间名称必须是非空字符串');
}
if (!NAME_PATTERN.test(name)) {
throw new Error(`NamespaceManager: 命名空间名称只能包含字母、数字、连字符和下划线,收到: '${name}'`);
}
}
}
module.exports = NamespaceManager;

View File

@ -1,7 +1,7 @@
// grid-db/src/events/event-log.js // grid-db/src/events/event-log.js
// Grid-DB · 事件溯源日志 // Grid-DB · 事件溯源日志
// 不可变事件流 + 审计日志 // 不可变事件流 + 审计日志
// PRJ-GDB-001 · Phase 0基础结构P1 完善回放与订阅 // PRJ-GDB-001 · Phase 0 + Phase 1回放、序列号回放、操作过滤、天眼钩子
// 版权:国作登字-2026-A-00037559 // 版权:国作登字-2026-A-00037559
'use strict'; 'use strict';
@ -106,6 +106,54 @@ class EventLog {
}; };
} }
/**
* 从指定时间戳回放事件
* @param {string} fromTimestamp ISO 8601 时间戳
* @returns {object[]}
*/
replay(fromTimestamp) {
return this._events.filter(e => e.timestamp >= fromTimestamp);
}
/**
* 从指定序列号回放事件
* @param {number} seqNo 起始序列号包含
* @returns {object[]}
*/
replayFromSeqNo(seqNo) {
return this._events.filter(e => e.seqNo >= seqNo);
}
/**
* 按操作类型过滤事件
* @param {string} operation 操作类型 (put | delete | scan)
* @param {number} [limit] 最大返回数
* @returns {object[]}
*/
getByOperation(operation, limit) {
const filtered = this._events.filter(e => e.operation === operation);
if (limit && limit > 0) {
return filtered.slice(-limit);
}
return filtered;
}
/**
* 设置天眼钩子 天眼集成占位接口
*
* 天眼通过此钩子实时接收事件流实现全域审计
* Phase 1 提供接口Phase 2 实现完整天眼协议
*
* @param {Function} handler 天眼事件处理函数
*/
setTianyanHook(handler) {
this._tianyanHook = handler;
// 注册为特殊订阅者
if (typeof handler === 'function') {
this.subscribe('__tianyan__', handler);
}
}
/** /**
* 获取事件日志状态 * 获取事件日志状态
* @returns {object} * @returns {object}

View File

@ -13,6 +13,11 @@ const WAL = require('./storage/wal');
const PageManager = require('./storage/page-manager'); const PageManager = require('./storage/page-manager');
const EventLog = require('./events/event-log'); const EventLog = require('./events/event-log');
const GridAPI = require('./api/grid-api'); const GridAPI = require('./api/grid-api');
const BTree = require('./index/btree');
const NamespaceManager = require('./core/namespace');
const RangeScanner = require('./query/scan');
const NearbyQuery = require('./query/nearby');
const SecondaryIndex = require('./index/secondary-index');
/** /**
* Grid-DB 初始化器 * Grid-DB 初始化器
@ -82,5 +87,10 @@ module.exports = {
GridCell, GridCell,
WAL, WAL,
PageManager, PageManager,
EventLog EventLog,
BTree,
NamespaceManager,
RangeScanner,
NearbyQuery,
SecondaryIndex
}; };

285
grid-db/src/index/btree.js Normal file
View File

@ -0,0 +1,285 @@
// grid-db/src/index/btree.js
// Grid-DB · B+Tree 主键索引
// 有序键值索引,支持范围扫描
// PRJ-GDB-001 · Phase 1 · ZY-GDB-P1-001
// 版权:国作登字-2026-A-00037559
'use strict';
/**
* B+Tree 主键索引
*
* 本体论锚定索引 = 纸的目录
* 目录按页码排列可以快速定位到任意一页
* 叶子节点串联 = 目录页之间的连续翻阅
*
* 所有数据存储在叶子节点内部节点仅存储路由键
* 叶子节点之间通过 next 指针串联支持高效范围扫描
*/
class BTreeNode {
/**
* @param {boolean} isLeaf 是否为叶子节点
*/
constructor(isLeaf = false) {
this.isLeaf = isLeaf;
this.keys = [];
this.children = []; // 内部节点:子节点引用;叶子节点:对应值
this.next = null; // 叶子节点链表指针
}
}
class BTree {
/**
* @param {number} [order] B+Tree 的阶数每个节点最大子节点数默认 32
*/
constructor(order = 32) {
if (typeof order !== 'number' || order < 3) {
throw new Error('BTree: order 必须是 >= 3 的整数');
}
this._order = order;
this._maxKeys = order - 1;
this._minKeys = Math.ceil(order / 2) - 1;
this._root = new BTreeNode(true);
this._size = 0;
}
/**
* 当前索引中的条目数量
* @returns {number}
*/
get size() {
return this._size;
}
/**
* 插入键值对
* @param {string} key
* @param {*} value
*/
insert(key, value) {
if (typeof key !== 'string') {
throw new Error('BTree.insert: key 必须是字符串');
}
// 检查是否已存在(更新)
const leaf = this._findLeaf(this._root, key);
const idx = leaf.keys.indexOf(key);
if (idx !== -1) {
leaf.children[idx] = value;
return;
}
// 根节点已满,先分裂
if (this._root.keys.length >= this._maxKeys) {
const newRoot = new BTreeNode(false);
newRoot.children.push(this._root);
this._splitChild(newRoot, 0);
this._root = newRoot;
}
this._insertNonFull(this._root, key, value);
this._size++;
}
/**
* 查找键对应的值
* @param {string} key
* @returns {*|undefined} 找到返回值否则 undefined
*/
find(key) {
const leaf = this._findLeaf(this._root, key);
const idx = leaf.keys.indexOf(key);
if (idx !== -1) {
return leaf.children[idx];
}
return undefined;
}
/**
* 删除键
* @param {string} key
* @returns {boolean} 是否删除成功
*/
delete(key) {
const leaf = this._findLeaf(this._root, key);
const idx = leaf.keys.indexOf(key);
if (idx === -1) {
return false;
}
leaf.keys.splice(idx, 1);
leaf.children.splice(idx, 1);
this._size--;
return true;
}
/**
* 范围查询闭区间 [startKey, endKey]
*
* 利用叶子节点链表高效遍历
*
* @param {string} startKey 起始键
* @param {string} endKey 结束键
* @returns {Array<{key: string, value: *}>} 有序结果
*/
range(startKey, endKey) {
const results = [];
let leaf = this._findLeaf(this._root, startKey);
while (leaf) {
for (let i = 0; i < leaf.keys.length; i++) {
if (leaf.keys[i] >= startKey && leaf.keys[i] <= endKey) {
results.push({ key: leaf.keys[i], value: leaf.children[i] });
}
if (leaf.keys[i] > endKey) {
return results;
}
}
leaf = leaf.next;
}
return results;
}
/**
* 序列化为 JSON
* @returns {object}
*/
toJSON() {
const entries = [];
let leaf = this._getFirstLeaf();
while (leaf) {
for (let i = 0; i < leaf.keys.length; i++) {
entries.push({ key: leaf.keys[i], value: leaf.children[i] });
}
leaf = leaf.next;
}
return { order: this._order, entries };
}
/**
* JSON 还原 B+Tree
* @param {object} json
* @returns {BTree}
*/
static fromJSON(json) {
const tree = new BTree(json.order || 32);
for (const entry of json.entries) {
tree.insert(entry.key, entry.value);
}
return tree;
}
/**
* 清空索引
*/
clear() {
this._root = new BTreeNode(true);
this._size = 0;
}
// ── 内部方法 ──
/**
* 找到 key 应当所在的叶子节点
* @param {BTreeNode} node
* @param {string} key
* @returns {BTreeNode}
*/
_findLeaf(node, key) {
if (node.isLeaf) {
return node;
}
// 在内部节点中找到正确的子节点
let i = 0;
while (i < node.keys.length && key >= node.keys[i]) {
i++;
}
return this._findLeaf(node.children[i], key);
}
/**
* 获取最左叶子节点
* @returns {BTreeNode}
*/
_getFirstLeaf() {
let node = this._root;
while (!node.isLeaf) {
node = node.children[0];
}
return node;
}
/**
* 向非满节点插入
* @param {BTreeNode} node
* @param {string} key
* @param {*} value
*/
_insertNonFull(node, key, value) {
if (node.isLeaf) {
// 在叶子节点中找到插入位置(保持有序)
let i = 0;
while (i < node.keys.length && node.keys[i] < key) {
i++;
}
node.keys.splice(i, 0, key);
node.children.splice(i, 0, value);
return;
}
// 内部节点:找到正确的子节点
let i = 0;
while (i < node.keys.length && key >= node.keys[i]) {
i++;
}
// 如果目标子节点已满,先分裂
if (node.children[i].keys.length >= this._maxKeys) {
this._splitChild(node, i);
if (key >= node.keys[i]) {
i++;
}
}
this._insertNonFull(node.children[i], key, value);
}
/**
* 分裂子节点
* @param {BTreeNode} parent 父节点
* @param {number} childIndex 要分裂的子节点索引
*/
_splitChild(parent, childIndex) {
const child = parent.children[childIndex];
const mid = Math.floor(child.keys.length / 2);
const newNode = new BTreeNode(child.isLeaf);
if (child.isLeaf) {
// 叶子节点分裂:右半部分复制到新节点,提升中间键的副本
newNode.keys = child.keys.splice(mid);
newNode.children = child.children.splice(mid);
// 维护叶子链表
newNode.next = child.next;
child.next = newNode;
// 提升第一个新键到父节点
parent.keys.splice(childIndex, 0, newNode.keys[0]);
parent.children.splice(childIndex + 1, 0, newNode);
} else {
// 内部节点分裂:中间键提升,不保留在子节点
const midKey = child.keys[mid];
newNode.keys = child.keys.splice(mid + 1);
newNode.children = child.children.splice(mid + 1);
child.keys.splice(mid); // 移除中间键
parent.keys.splice(childIndex, 0, midKey);
parent.children.splice(childIndex + 1, 0, newNode);
}
}
}
module.exports = BTree;

View File

@ -0,0 +1,128 @@
// grid-db/src/index/secondary-index.js
// Grid-DB · 二级索引
// 按任意字段建立索引,支持一对多映射
// PRJ-GDB-001 · Phase 1 · ZY-GDB-P1-006
// 版权:国作登字-2026-A-00037559
'use strict';
/**
* SecondaryIndex 二级索引
*
* 本体论锚定二级索引 = 纸的侧面标签
* 同一个标签可以贴在多个格子上一对多
* 通过标签可以快速找到所有相关的格子
*
* fieldValue Set<primaryKey> 的映射结构
* 支持范围查询利用排序后的键列表
*/
class SecondaryIndex {
/**
* @param {string} fieldName 索引的字段名
*/
constructor(fieldName) {
if (!fieldName || typeof fieldName !== 'string') {
throw new Error('SecondaryIndex: fieldName 必须是非空字符串');
}
this._fieldName = fieldName;
/** @type {Map<string, Set<string>>} */
this._map = new Map();
this._size = 0;
}
/**
* 索引中的条目总数所有 primaryKey 的总计
* @returns {number}
*/
get size() {
return this._size;
}
/**
* 添加索引条目
* @param {string} fieldValue 字段值
* @param {string} primaryKey 主键
*/
add(fieldValue, primaryKey) {
const strValue = String(fieldValue);
let keys = this._map.get(strValue);
if (!keys) {
keys = new Set();
this._map.set(strValue, keys);
}
if (!keys.has(primaryKey)) {
keys.add(primaryKey);
this._size++;
}
}
/**
* 移除索引条目
* @param {string} fieldValue 字段值
* @param {string} primaryKey 主键
* @returns {boolean} 是否移除成功
*/
remove(fieldValue, primaryKey) {
const strValue = String(fieldValue);
const keys = this._map.get(strValue);
if (!keys) return false;
if (keys.delete(primaryKey)) {
this._size--;
if (keys.size === 0) {
this._map.delete(strValue);
}
return true;
}
return false;
}
/**
* 查找指定字段值的所有主键
* @param {string} fieldValue
* @returns {string[]} 主键数组
*/
find(fieldValue) {
const strValue = String(fieldValue);
const keys = this._map.get(strValue);
if (!keys) return [];
return Array.from(keys);
}
/**
* 范围查询闭区间 [startValue, endValue]
* @param {string} startValue 起始值
* @param {string} endValue 结束值
* @returns {string[]} 主键数组去重
*/
range(startValue, endValue) {
const start = String(startValue);
const end = String(endValue);
const resultSet = new Set();
// 获取所有键并排序
const sortedKeys = Array.from(this._map.keys()).sort();
for (const key of sortedKeys) {
if (key >= start && key <= end) {
const primaryKeys = this._map.get(key);
for (const pk of primaryKeys) {
resultSet.add(pk);
}
}
if (key > end) break;
}
return Array.from(resultSet);
}
/**
* 清空索引
*/
clear() {
this._map.clear();
this._size = 0;
}
}
module.exports = SecondaryIndex;

116
grid-db/src/query/nearby.js Normal file
View File

@ -0,0 +1,116 @@
// grid-db/src/query/nearby.js
// Grid-DB · 近邻查询
// 基于欧几里得距离的邻近查询
// PRJ-GDB-001 · Phase 1 · ZY-GDB-P1-004
// 版权:国作登字-2026-A-00037559
'use strict';
const GridCell = require('../core/grid-cell');
/**
* NearbyQuery 近邻查询
*
* 本体论锚定近邻 = 以某个格子为中心画一个圆找出圆内所有格子
* 圆由中心坐标和半径定义
* 结果按距离排序最近的在前面
*/
class NearbyQuery {
/**
* @param {object} deps
* @param {Map|object} deps.index 索引
* @param {object} deps.pageManager 页管理器
*/
constructor({ index, pageManager }) {
if (!index) {
throw new Error('NearbyQuery: index 不能为空');
}
if (!pageManager) {
throw new Error('NearbyQuery: pageManager 不能为空');
}
this._index = index;
this._pageManager = pageManager;
}
/**
* 近邻查询
*
* @param {string} namespace 命名空间
* @param {number} centerX 中心 X 坐标
* @param {number} centerY 中心 Y 坐标
* @param {number} radius 搜索半径欧几里得距离
* @param {object} [options]
* @param {string} [options.layer] 层级过滤
* @param {number} [options.limit] 最大返回数
* @param {string} [options.sortBy] 排序方式默认 'distance'
* @returns {Array<{cell: GridCell, data: *, distance: number}>}
*/
nearby(namespace, centerX, centerY, radius, options = {}) {
const { layer, limit, sortBy = 'distance' } = options;
const results = [];
const entries = this._getEntries();
for (const [key, pageId] of entries) {
try {
const cell = GridCell.fromKey(key);
// 命名空间过滤
if (cell.namespace !== namespace) continue;
// 层级过滤
if (layer && cell.layer !== layer) continue;
// 欧几里得距离计算
const dx = cell.gridX - centerX;
const dy = cell.gridY - centerY;
const distance = Math.sqrt(dx * dx + dy * dy);
// 半径过滤
if (distance > radius) continue;
// 读取数据
const dataBuf = this._pageManager.readPage(pageId);
if (dataBuf) {
let data;
try { data = JSON.parse(dataBuf.toString('utf-8')); } catch { data = dataBuf; }
results.push({ cell, data, distance });
}
} catch {
// 跳过无效键
}
}
// 按距离升序排序
if (sortBy === 'distance') {
results.sort((a, b) => a.distance - b.distance);
}
// limit
if (limit && limit > 0) {
return results.slice(0, limit);
}
return results;
}
/**
* 获取索引中的所有条目兼容 Map BTree
* @returns {Array<[string, *]>}
*/
_getEntries() {
if (this._index instanceof Map) {
return Array.from(this._index.entries());
}
if (typeof this._index.toJSON === 'function') {
const json = this._index.toJSON();
return json.entries.map(e => [e.key, e.value]);
}
if (typeof this._index.entries === 'function') {
return Array.from(this._index.entries());
}
return [];
}
}
module.exports = NearbyQuery;

120
grid-db/src/query/scan.js Normal file
View File

@ -0,0 +1,120 @@
// grid-db/src/query/scan.js
// Grid-DB · 范围扫描器
// 矩形区域查询
// PRJ-GDB-001 · Phase 1 · ZY-GDB-P1-003
// 版权:国作登字-2026-A-00037559
'use strict';
const GridCell = require('../core/grid-cell');
/**
* RangeScanner 范围扫描器
*
* 本体论锚定扫描 = 在纸上画一个矩形区域找出所有落在区域内的格子
* 矩形由 xRange yRange 定义layer 可选过滤
*
* 支持 BTree 索引和 Map 索引两种后端
*/
class RangeScanner {
/**
* @param {object} deps
* @param {Map|object} deps.index 索引Map BTree需支持遍历
* @param {object} deps.pageManager 页管理器
*/
constructor({ index, pageManager }) {
if (!index) {
throw new Error('RangeScanner: index 不能为空');
}
if (!pageManager) {
throw new Error('RangeScanner: pageManager 不能为空');
}
this._index = index;
this._pageManager = pageManager;
}
/**
* 矩形区域扫描
*
* @param {string} namespace 命名空间
* @param {object} [options]
* @param {number[]} [options.xRange] X 范围 [min, max]
* @param {number[]} [options.yRange] Y 范围 [min, max]
* @param {string} [options.layer] 层级过滤
* @param {number} [options.limit] 最大返回数
* @param {number} [options.offset] 跳过前 N
* @returns {Array<{cell: GridCell, data: *}>} (gridX, gridY) 排序
*/
scan(namespace, options = {}) {
const { xRange, yRange, layer, limit, offset = 0 } = options;
const results = [];
// 遍历索引中所有条目
const entries = this._getEntries();
for (const [key, pageId] of entries) {
try {
const cell = GridCell.fromKey(key);
// 命名空间过滤
if (cell.namespace !== namespace) continue;
// 层级过滤
if (layer && cell.layer !== layer) continue;
// X 范围过滤
if (xRange) {
if (cell.gridX < xRange[0] || cell.gridX > xRange[1]) continue;
}
// Y 范围过滤
if (yRange) {
if (cell.gridY < yRange[0] || cell.gridY > yRange[1]) continue;
}
// 读取数据
const dataBuf = this._pageManager.readPage(pageId);
if (dataBuf) {
let data;
try { data = JSON.parse(dataBuf.toString('utf-8')); } catch { data = dataBuf; }
results.push({ cell, data });
}
} catch {
// 跳过无效键
}
}
// 按 (gridX, gridY) 排序
results.sort((a, b) => {
if (a.cell.gridX !== b.cell.gridX) return a.cell.gridX - b.cell.gridX;
return a.cell.gridY - b.cell.gridY;
});
// offset + limit
const start = offset || 0;
const end = limit ? start + limit : results.length;
return results.slice(start, end);
}
/**
* 获取索引中的所有条目兼容 Map BTree
* @returns {Array<[string, *]>}
*/
_getEntries() {
if (this._index instanceof Map) {
return Array.from(this._index.entries());
}
// BTree通过 toJSON 获取所有条目
if (typeof this._index.toJSON === 'function') {
const json = this._index.toJSON();
return json.entries.map(e => [e.key, e.value]);
}
// 其他可迭代对象
if (typeof this._index.entries === 'function') {
return Array.from(this._index.entries());
}
return [];
}
}
module.exports = RangeScanner;

View File

@ -0,0 +1,443 @@
// grid-db/tests/smoke/grid-db-p1.test.js
// Grid-DB · Phase 1 冒烟测试
// PRJ-GDB-001 · Phase 1 · ZY-GDB-P1-TEST
// 版权:国作登字-2026-A-00037559
'use strict';
const path = require('path');
const fs = require('fs');
const os = require('os');
const {
open,
GridAPI,
GridCell,
WAL,
PageManager,
EventLog,
BTree,
NamespaceManager,
RangeScanner,
NearbyQuery,
SecondaryIndex
} = require('../../src/index');
let passed = 0;
let failed = 0;
function assert(condition, message) {
if (condition) {
passed++;
// eslint-disable-next-line no-console
console.log(`${message}`);
} else {
failed++;
// eslint-disable-next-line no-console
console.error(`${message}`);
}
}
/**
* 创建临时测试目录
* @param {string} suffix
* @returns {string}
*/
function makeTempDir(suffix) {
const { randomUUID } = require('crypto');
const dir = path.join(os.tmpdir(), `griddb-p1-test-${suffix}-${randomUUID().slice(0, 8)}`);
fs.mkdirSync(dir, { recursive: true });
return dir;
}
/**
* 递归删除目录
* @param {string} dir
*/
function cleanDir(dir) {
try {
fs.rmSync(dir, { recursive: true, force: true });
} catch {
// 忽略清理错误
}
}
// eslint-disable-next-line no-console
console.log('🗄️ Grid-DB Phase 1 冒烟测试\n');
// ── 测试 1: Phase 1 模块导出完整性 ──
// eslint-disable-next-line no-console
console.log('── 测试 1: Phase 1 模块导出完整性 ──');
assert(typeof BTree === 'function', 'BTree 是构造函数');
assert(typeof NamespaceManager === 'function', 'NamespaceManager 是构造函数');
assert(typeof RangeScanner === 'function', 'RangeScanner 是构造函数');
assert(typeof NearbyQuery === 'function', 'NearbyQuery 是构造函数');
assert(typeof SecondaryIndex === 'function', 'SecondaryIndex 是构造函数');
// Phase 0 模块仍然存在
assert(typeof open === 'function', 'open 仍然导出');
assert(typeof GridAPI === 'function', 'GridAPI 仍然导出');
assert(typeof GridCell === 'function', 'GridCell 仍然导出');
assert(typeof EventLog === 'function', 'EventLog 仍然导出');
// ── 测试 2: B+Tree 基础功能 ──
// eslint-disable-next-line no-console
console.log('\n── 测试 2: B+Tree 基础功能 ──');
const tree = new BTree(4); // 小阶数方便测试分裂
// 插入
tree.insert('c', 3);
tree.insert('a', 1);
tree.insert('b', 2);
tree.insert('e', 5);
tree.insert('d', 4);
assert(tree.size === 5, '插入 5 条后 size = 5');
// 查找
assert(tree.find('a') === 1, 'find("a") = 1');
assert(tree.find('c') === 3, 'find("c") = 3');
assert(tree.find('e') === 5, 'find("e") = 5');
assert(tree.find('z') === undefined, 'find("z") = undefined');
// 更新
tree.insert('a', 100);
assert(tree.find('a') === 100, '更新后 find("a") = 100');
assert(tree.size === 5, '更新不增加 size');
// 删除
const delOk = tree.delete('c');
assert(delOk === true, 'delete("c") 返回 true');
assert(tree.find('c') === undefined, '删除后 find("c") = undefined');
assert(tree.size === 4, '删除后 size = 4');
const delFail = tree.delete('z');
assert(delFail === false, 'delete 不存在的键返回 false');
// 范围查询
tree.insert('c', 3); // 重新插入
const rangeResult = tree.range('b', 'd');
assert(rangeResult.length === 3, 'range("b","d") 返回 3 条');
assert(rangeResult[0].key === 'b', 'range 第一条 key = "b"');
assert(rangeResult[1].key === 'c', 'range 第二条 key = "c"');
assert(rangeResult[2].key === 'd', 'range 第三条 key = "d"');
// 序列化 / 反序列化
const json = tree.toJSON();
assert(json.order === 4, 'toJSON 包含 order');
assert(json.entries.length === 5, 'toJSON 包含 5 个条目');
const tree2 = BTree.fromJSON(json);
assert(tree2.size === 5, 'fromJSON 还原 size = 5');
assert(tree2.find('a') === 100, 'fromJSON 还原后 find("a") = 100');
assert(tree2.find('e') === 5, 'fromJSON 还原后 find("e") = 5');
// clear
tree.clear();
assert(tree.size === 0, 'clear 后 size = 0');
assert(tree.find('a') === undefined, 'clear 后 find 返回 undefined');
// 大量插入测试分裂逻辑
const treeLarge = new BTree(4);
for (let i = 0; i < 50; i++) {
treeLarge.insert(`key-${String(i).padStart(3, '0')}`, i);
}
assert(treeLarge.size === 50, '大量插入后 size = 50');
assert(treeLarge.find('key-025') === 25, '大量插入后查找正确');
const largeRange = treeLarge.range('key-010', 'key-019');
assert(largeRange.length === 10, '大量数据范围查询返回 10 条');
// 无效参数
let errThrown = false;
try { new BTree(1); } catch { errThrown = true; }
assert(errThrown, 'order < 3 抛出错误');
// ── 测试 3: NamespaceManager ──
// eslint-disable-next-line no-console
console.log('\n── 测试 3: NamespaceManager ──');
const nsManager = new NamespaceManager();
// 创建
const ns1 = nsManager.create('agent-zy', { desc: '铸渊代理' });
assert(ns1.name === 'agent-zy', '创建命名空间 name 正确');
assert(ns1.metadata.desc === '铸渊代理', '创建命名空间 metadata 正确');
assert(ns1.cellCount === 0, '新命名空间 cellCount = 0');
assert(typeof ns1.createdAt === 'string', '包含 createdAt');
// exists
assert(nsManager.exists('agent-zy') === true, 'exists 已存在返回 true');
assert(nsManager.exists('not-exist') === false, 'exists 不存在返回 false');
// get
const got = nsManager.get('agent-zy');
assert(got !== null, 'get 返回非 null');
assert(got.name === 'agent-zy', 'get name 正确');
// 创建多个
nsManager.create('dc-v1');
nsManager.create('exe_engine');
const all = nsManager.list();
assert(all.length === 3, 'list 返回 3 个命名空间');
// incrementCellCount / decrementCellCount
nsManager.incrementCellCount('agent-zy');
nsManager.incrementCellCount('agent-zy');
assert(nsManager.get('agent-zy').cellCount === 2, 'increment 后 cellCount = 2');
nsManager.decrementCellCount('agent-zy');
assert(nsManager.get('agent-zy').cellCount === 1, 'decrement 后 cellCount = 1');
// 删除
const delNs = nsManager.delete('dc-v1');
assert(delNs === true, 'delete 返回 true');
assert(nsManager.exists('dc-v1') === false, '删除后不存在');
const delNs2 = nsManager.delete('not-exist');
assert(delNs2 === false, 'delete 不存在返回 false');
// 验证:无效名称
errThrown = false;
try { nsManager.create(''); } catch { errThrown = true; }
assert(errThrown, '空名称抛出错误');
errThrown = false;
try { nsManager.create('invalid name!'); } catch { errThrown = true; }
assert(errThrown, '含特殊字符的名称抛出错误');
// 重复创建
errThrown = false;
try { nsManager.create('agent-zy'); } catch { errThrown = true; }
assert(errThrown, '重复创建抛出错误');
// ── 测试 4: RangeScanner ──
// eslint-disable-next-line no-console
console.log('\n── 测试 4: RangeScanner ──');
const scanDir = makeTempDir('scan');
const scanDb = open({ dataDir: scanDir });
// 插入测试数据
scanDb.put('scan-test', { gridX: 1, gridY: 1, layer: 'raw' }, { id: 'a' });
scanDb.put('scan-test', { gridX: 2, gridY: 3, layer: 'raw' }, { id: 'b' });
scanDb.put('scan-test', { gridX: 4, gridY: 2, layer: 'raw' }, { id: 'c' });
scanDb.put('scan-test', { gridX: 5, gridY: 5, layer: 'indexed' }, { id: 'd' });
scanDb.put('other-ns', { gridX: 1, gridY: 1, layer: 'raw' }, { id: 'e' });
const scanner = new RangeScanner({
index: scanDb._index,
pageManager: scanDb._pageManager
});
// 全命名空间扫描
const scanAll = scanner.scan('scan-test');
assert(scanAll.length === 4, 'RangeScanner 全扫描 = 4');
// X/Y 范围过滤
const scanXY = scanner.scan('scan-test', { xRange: [1, 3], yRange: [1, 3] });
assert(scanXY.length === 2, 'X/Y 范围过滤 = 2');
// 层级过滤
const scanLayer = scanner.scan('scan-test', { layer: 'indexed' });
assert(scanLayer.length === 1, 'layer 过滤 = 1');
assert(scanLayer[0].data.id === 'd', 'layer 过滤结果正确');
// 排序验证:按 gridX, gridY
const sorted = scanner.scan('scan-test');
assert(sorted[0].cell.gridX <= sorted[1].cell.gridX, '结果按 gridX 升序');
// limit
const scanLimited = scanner.scan('scan-test', { limit: 2 });
assert(scanLimited.length === 2, 'limit = 2 返回 2 条');
// offset
const scanOffset = scanner.scan('scan-test', { offset: 2 });
assert(scanOffset.length === 2, 'offset = 2 跳过 2 条');
// 命名空间隔离
const scanOther = scanner.scan('other-ns');
assert(scanOther.length === 1, '不同命名空间隔离');
scanDb.close();
cleanDir(scanDir);
// ── 测试 5: NearbyQuery ──
// eslint-disable-next-line no-console
console.log('\n── 测试 5: NearbyQuery ──');
const nearDir = makeTempDir('nearby');
const nearDb = open({ dataDir: nearDir });
nearDb.put('near-ns', { gridX: 0, gridY: 0, layer: 'raw' }, { id: 'origin' });
nearDb.put('near-ns', { gridX: 1, gridY: 1, layer: 'raw' }, { id: 'close' });
nearDb.put('near-ns', { gridX: 3, gridY: 4, layer: 'raw' }, { id: 'mid' });
nearDb.put('near-ns', { gridX: 10, gridY: 10, layer: 'raw' }, { id: 'far' });
nearDb.put('near-ns', { gridX: 2, gridY: 0, layer: 'indexed' }, { id: 'layer-diff' });
const nearQuery = new NearbyQuery({
index: nearDb._index,
pageManager: nearDb._pageManager
});
// 半径 2 内:(0,0)=0, (1,1)=√2≈1.414, (2,0)=2.0
const near2 = nearQuery.nearby('near-ns', 0, 0, 2);
assert(near2.length === 3, '半径 2 内 = 3 个点');
assert(near2[0].distance === 0, '最近点距离 = 0');
assert(near2[0].data.id === 'origin', '最近点是 origin');
// 半径 6 内
const near6 = nearQuery.nearby('near-ns', 0, 0, 6);
assert(near6.length === 4, '半径 6 内 = 4 个点');
// 距离排序验证
assert(near6[0].distance <= near6[1].distance, '结果按距离升序');
assert(near6[1].distance <= near6[2].distance, '第二条 ≤ 第三条');
// layer 过滤
const nearLayer = nearQuery.nearby('near-ns', 0, 0, 6, { layer: 'indexed' });
assert(nearLayer.length === 1, 'layer 过滤 = 1');
assert(nearLayer[0].data.id === 'layer-diff', 'layer 过滤结果正确');
// limit
const nearLimit = nearQuery.nearby('near-ns', 0, 0, 100, { limit: 2 });
assert(nearLimit.length === 2, 'limit = 2 返回 2 条');
// 距离正确性验证
const sqrt2 = Math.sqrt(2);
assert(Math.abs(near2[1].distance - sqrt2) < 0.001, '(1,1) 距离 = √2');
nearDb.close();
cleanDir(nearDir);
// ── 测试 6: EventLog 增强功能 ──
// eslint-disable-next-line no-console
console.log('\n── 测试 6: EventLog 增强功能 ──');
const elog = new EventLog({ maxEvents: 100 });
// 插入一些事件,带时间间隔
const t0 = new Date().toISOString();
elog.append('ns-a', 'put', 'ns-a:1:1:raw', { size: 10 });
elog.append('ns-a', 'put', 'ns-a:2:2:raw', { size: 20 });
elog.append('ns-a', 'delete', 'ns-a:1:1:raw');
elog.append('ns-b', 'put', 'ns-b:1:1:raw', { size: 30 });
elog.append('ns-b', 'scan', 'scan:ns-b', { count: 5 });
// replay — 从 t0 开始应返回所有 5 条
const replayed = elog.replay(t0);
assert(replayed.length === 5, 'replay 从 t0 返回 5 条');
// replayFromSeqNo
const fromSeq3 = elog.replayFromSeqNo(3);
assert(fromSeq3.length === 3, 'replayFromSeqNo(3) 返回 3 条');
assert(fromSeq3[0].seqNo === 3, '第一条 seqNo = 3');
// getByOperation
const puts = elog.getByOperation('put');
assert(puts.length === 3, 'getByOperation("put") = 3');
const deletes = elog.getByOperation('delete');
assert(deletes.length === 1, 'getByOperation("delete") = 1');
// getByOperation with limit
const putsLimited = elog.getByOperation('put', 2);
assert(putsLimited.length === 2, 'getByOperation 带 limit = 2');
// 天眼钩子
let tianyanReceived = null;
elog.setTianyanHook((evt) => { tianyanReceived = evt; });
elog.append('ns-a', 'put', 'ns-a:3:3:raw');
assert(tianyanReceived !== null, '天眼钩子收到事件');
assert(tianyanReceived.namespace === 'ns-a', '天眼事件 namespace 正确');
assert(tianyanReceived.operation === 'put', '天眼事件 operation 正确');
// 既有功能不受影响
const recent = elog.getRecent(3);
assert(recent.length === 3, 'getRecent 仍然正常工作');
const byNs = elog.getByNamespace('ns-b');
assert(byNs.length === 2, 'getByNamespace 仍然正常工作');
// ── 测试 7: SecondaryIndex ──
// eslint-disable-next-line no-console
console.log('\n── 测试 7: SecondaryIndex ──');
const secIdx = new SecondaryIndex('layer');
// add
secIdx.add('raw', 'ns:1:1:raw');
secIdx.add('raw', 'ns:2:2:raw');
secIdx.add('indexed', 'ns:3:3:indexed');
secIdx.add('semantic', 'ns:4:4:semantic');
assert(secIdx.size === 4, '添加 4 条后 size = 4');
// find
const rawKeys = secIdx.find('raw');
assert(rawKeys.length === 2, 'find("raw") = 2');
assert(rawKeys.includes('ns:1:1:raw'), 'find 包含 ns:1:1:raw');
assert(rawKeys.includes('ns:2:2:raw'), 'find 包含 ns:2:2:raw');
const indexedKeys = secIdx.find('indexed');
assert(indexedKeys.length === 1, 'find("indexed") = 1');
const noKeys = secIdx.find('cleaned');
assert(noKeys.length === 0, 'find 不存在值 = 空数组');
// remove
const removed = secIdx.remove('raw', 'ns:1:1:raw');
assert(removed === true, 'remove 返回 true');
assert(secIdx.size === 3, 'remove 后 size = 3');
assert(secIdx.find('raw').length === 1, 'remove 后 find("raw") = 1');
const removeFail = secIdx.remove('raw', 'not:exist');
assert(removeFail === false, 'remove 不存在的主键返回 false');
// 重复 add 不增加计数
secIdx.add('raw', 'ns:2:2:raw');
assert(secIdx.size === 3, '重复 add 不增加 size');
// range
secIdx.add('cleaned', 'ns:5:5:cleaned');
const rangeKeys = secIdx.range('indexed', 'semantic');
assert(rangeKeys.length >= 2, 'range("indexed","semantic") >= 2');
assert(rangeKeys.includes('ns:3:3:indexed'), 'range 包含 indexed 条目');
assert(rangeKeys.includes('ns:4:4:semantic'), 'range 包含 semantic 条目');
// clear
secIdx.clear();
assert(secIdx.size === 0, 'clear 后 size = 0');
assert(secIdx.find('raw').length === 0, 'clear 后 find 返回空');
// 无效参数
errThrown = false;
try { new SecondaryIndex(''); } catch { errThrown = true; }
assert(errThrown, '空 fieldName 抛出错误');
// ── 测试 8: 集成验证 — Phase 0 功能仍正常 ──
// eslint-disable-next-line no-console
console.log('\n── 测试 8: 集成验证 — Phase 0 功能仍正常 ──');
const intDir = makeTempDir('integration');
const intDb = open({ dataDir: intDir });
intDb.put('int-ns', { gridX: 1, gridY: 2, layer: 'raw' }, { val: 'hello' });
const intResult = intDb.get('int-ns', { gridX: 1, gridY: 2, layer: 'raw' });
assert(intResult !== null, 'Phase 0 put/get 仍然正常');
assert(intResult.val === 'hello', 'Phase 0 数据正确');
const intScan = intDb.scan('int-ns');
assert(intScan.length === 1, 'Phase 0 scan 仍然正常');
intDb.close();
cleanDir(intDir);
// ── 测试结果汇总 ──
// eslint-disable-next-line no-console
console.log('\n══════════════════════════════════════');
// eslint-disable-next-line no-console
console.log('🗄️ Grid-DB Phase 1 冒烟测试完成');
// eslint-disable-next-line no-console
console.log(` ✅ 通过: ${passed}`);
// eslint-disable-next-line no-console
console.log(` ❌ 失败: ${failed}`);
// eslint-disable-next-line no-console
console.log(` 📊 总计: ${passed + failed}`);
// eslint-disable-next-line no-console
console.log('══════════════════════════════════════\n');
if (failed > 0) {
process.exit(1);
}