release: v0.2.4 — 701 tests, 32 suites, 零死代码, 零空壳, 全模块接入
This commit is contained in:
Vendored
+109
-80
@@ -1015,6 +1015,98 @@ class OPFSEngine {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* metona-sqlark Schema — 表结构定义与校验
|
||||
* @module table/schema
|
||||
*/
|
||||
// ---------------------------------------------------------------------------
|
||||
// Schema 工具
|
||||
// ---------------------------------------------------------------------------
|
||||
/** 从列定义创建 TableSchema */
|
||||
function createSchema(name, columns) {
|
||||
validateColumns(columns);
|
||||
return { name, columns };
|
||||
}
|
||||
/** 校验列定义 */
|
||||
function validateColumns(columns) {
|
||||
const colNames = Object.keys(columns);
|
||||
if (colNames.length === 0) {
|
||||
throw new DatabaseError('Table must have at least one column', 'SCHEMA_ERROR');
|
||||
}
|
||||
let primaryKeyCount = 0;
|
||||
for (const [colName, colDef] of Object.entries(columns)) {
|
||||
// 类型校验
|
||||
if (!FIELD_TYPES.includes(colDef.type)) {
|
||||
throw new DatabaseError(`Invalid type "${colDef.type}" for column "${colName}". Valid types: ${FIELD_TYPES.join(', ')}`, 'SCHEMA_ERROR');
|
||||
}
|
||||
// 主键计数
|
||||
if (colDef.primaryKey) {
|
||||
primaryKeyCount++;
|
||||
}
|
||||
}
|
||||
// 至少需要一个主键
|
||||
if (primaryKeyCount === 0) {
|
||||
throw new DatabaseError('Table must have at least one primary key column', 'SCHEMA_ERROR');
|
||||
}
|
||||
}
|
||||
/** 检查字段类型(含约束校验) */
|
||||
function checkFieldType(tableName, colName, type, value, colDef) {
|
||||
const jsType = typeof value;
|
||||
switch (type) {
|
||||
case 'string':
|
||||
if (jsType !== 'string') {
|
||||
throw new DatabaseError(`Column "${colName}" in table "${tableName}" expects string, got ${jsType}`, 'TYPE_ERROR');
|
||||
}
|
||||
if (colDef?.maxLength !== undefined && value.length > colDef.maxLength) {
|
||||
throw new DatabaseError(`Column "${colName}" in table "${tableName}" exceeds max length ${colDef.maxLength}`, 'VALIDATION_ERROR');
|
||||
}
|
||||
break;
|
||||
case 'number':
|
||||
if (jsType !== 'number') {
|
||||
throw new DatabaseError(`Column "${colName}" in table "${tableName}" expects number, got ${jsType}`, 'TYPE_ERROR');
|
||||
}
|
||||
if (colDef?.min !== undefined && value < colDef.min) {
|
||||
throw new DatabaseError(`Column "${colName}" in table "${tableName}" value ${value} below minimum ${colDef.min}`, 'VALIDATION_ERROR');
|
||||
}
|
||||
if (colDef?.max !== undefined && value > colDef.max) {
|
||||
throw new DatabaseError(`Column "${colName}" in table "${tableName}" value ${value} above maximum ${colDef.max}`, 'VALIDATION_ERROR');
|
||||
}
|
||||
break;
|
||||
case 'boolean':
|
||||
if (jsType !== 'boolean') {
|
||||
throw new DatabaseError(`Column "${colName}" in table "${tableName}" expects boolean, got ${jsType}`, 'TYPE_ERROR');
|
||||
}
|
||||
break;
|
||||
case 'date':
|
||||
if (jsType !== 'string' || isNaN(Date.parse(value))) {
|
||||
throw new DatabaseError(`Column "${colName}" in table "${tableName}" expects valid date string, got ${typeof value}`, 'TYPE_ERROR');
|
||||
}
|
||||
break;
|
||||
case 'json':
|
||||
if (jsType !== 'object') {
|
||||
throw new DatabaseError(`Column "${colName}" in table "${tableName}" expects object/array, got ${jsType}`, 'TYPE_ERROR');
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
/** 将 AST 列定义转换为 ColumnDef */
|
||||
function astColumnToColumnDef(astCol) {
|
||||
return {
|
||||
type: astCol.type,
|
||||
primaryKey: astCol.primaryKey,
|
||||
unique: astCol.unique,
|
||||
required: astCol.required,
|
||||
default: astCol.default,
|
||||
index: astCol.index,
|
||||
maxLength: astCol.maxLength,
|
||||
min: astCol.min,
|
||||
max: astCol.max,
|
||||
references: astCol.references,
|
||||
onDelete: astCol.onDelete,
|
||||
onUpdate: astCol.onUpdate,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* AriaEngine Types — 内部类型定义
|
||||
* @module engine/aria/types
|
||||
@@ -4184,6 +4276,7 @@ class AriaEngine {
|
||||
}
|
||||
async count(tableName, query) {
|
||||
this.ensureOpen();
|
||||
this.ensureTable(tableName);
|
||||
const rows = this.getAllRows(tableName);
|
||||
if (!query?.where || Object.keys(query.where).length === 0)
|
||||
return rows.length;
|
||||
@@ -4322,37 +4415,15 @@ class AriaEngine {
|
||||
throw new DatabaseError(`Column "${colName}" is required in table "${schema.name}"`, 'VALIDATION_ERROR');
|
||||
}
|
||||
if (value !== undefined && value !== null) {
|
||||
this.checkType(colName, colDef.type, value);
|
||||
this.checkType(colName, colDef.type, value, colDef);
|
||||
}
|
||||
if (value !== undefined)
|
||||
validated[colName] = value;
|
||||
}
|
||||
return validated;
|
||||
}
|
||||
checkType(colName, type, value) {
|
||||
const jsType = typeof value;
|
||||
switch (type) {
|
||||
case 'string':
|
||||
if (jsType !== 'string')
|
||||
throw new DatabaseError(`Column "${colName}" expects string, got ${jsType}`, 'TYPE_ERROR');
|
||||
break;
|
||||
case 'number':
|
||||
if (jsType !== 'number')
|
||||
throw new DatabaseError(`Column "${colName}" expects number, got ${jsType}`, 'TYPE_ERROR');
|
||||
break;
|
||||
case 'boolean':
|
||||
if (jsType !== 'boolean')
|
||||
throw new DatabaseError(`Column "${colName}" expects boolean, got ${jsType}`, 'TYPE_ERROR');
|
||||
break;
|
||||
case 'date':
|
||||
if (jsType !== 'string' || isNaN(Date.parse(value)))
|
||||
throw new DatabaseError(`Column "${colName}" expects valid date`, 'TYPE_ERROR');
|
||||
break;
|
||||
case 'json':
|
||||
if (jsType !== 'object')
|
||||
throw new DatabaseError(`Column "${colName}" expects object/array, got ${jsType}`, 'TYPE_ERROR');
|
||||
break;
|
||||
}
|
||||
checkType(colName, type, value, colDef) {
|
||||
checkFieldType('', colName, type, value, colDef);
|
||||
}
|
||||
// =======================================================================
|
||||
// Schema 持久化
|
||||
@@ -4571,8 +4642,16 @@ class AriaEngine {
|
||||
}
|
||||
// $gt / $gte / $lt / $lte → 范围扫描
|
||||
if ('$gt' in c || '$gte' in c || '$lt' in c || '$lte' in c) {
|
||||
const startKey = c.$gt ? `${String(Number(c.$gt) + 1)}:` : c.$gte ? `${String(c.$gte)}:` : `${col}:`;
|
||||
const endKey = c.$lt ? `${String(Number(c.$lt) - 1)}:\uffff` : c.$lte ? `${String(c.$lte)}:\uffff` : `${col}:\uffff`;
|
||||
let startKey = '';
|
||||
let endKey = '\uffff';
|
||||
if (c.$gt !== undefined)
|
||||
startKey = `${String(Number(c.$gt) + 1)}:`;
|
||||
else if (c.$gte !== undefined)
|
||||
startKey = `${String(c.$gte)}:`;
|
||||
if (c.$lt !== undefined)
|
||||
endKey = `${String(Number(c.$lt) - 1)}:\uffff`;
|
||||
else if (c.$lte !== undefined)
|
||||
endKey = `${String(c.$lte)}:\uffff`;
|
||||
return this.indexScanToRows(tableName, pkCol, idxLsm, col, startKey, endKey);
|
||||
}
|
||||
}
|
||||
@@ -4580,7 +4659,9 @@ class AriaEngine {
|
||||
}
|
||||
/** 从索引扫描结果恢复完整行 */
|
||||
indexScanToRows(tableName, pkCol, idxLsm, _col, startKey, endKey) {
|
||||
const entries = idxLsm.rangeScan(startKey, endKey);
|
||||
// 使用前缀扫描:endKey 需要包含 \uffff 以匹配所有带后缀的 key
|
||||
const actualEndKey = endKey.includes('\uffff') ? endKey : `${endKey}\uffff`;
|
||||
const entries = idxLsm.rangeScan(startKey, actualEndKey);
|
||||
const rows = [];
|
||||
for (const [, idxEntry] of entries) {
|
||||
const pk = idxEntry.pk;
|
||||
@@ -5084,58 +5165,6 @@ class Table {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* metona-sqlark Schema — 表结构定义与校验
|
||||
* @module table/schema
|
||||
*/
|
||||
// ---------------------------------------------------------------------------
|
||||
// Schema 工具
|
||||
// ---------------------------------------------------------------------------
|
||||
/** 从列定义创建 TableSchema */
|
||||
function createSchema(name, columns) {
|
||||
validateColumns(columns);
|
||||
return { name, columns };
|
||||
}
|
||||
/** 校验列定义 */
|
||||
function validateColumns(columns) {
|
||||
const colNames = Object.keys(columns);
|
||||
if (colNames.length === 0) {
|
||||
throw new DatabaseError('Table must have at least one column', 'SCHEMA_ERROR');
|
||||
}
|
||||
let primaryKeyCount = 0;
|
||||
for (const [colName, colDef] of Object.entries(columns)) {
|
||||
// 类型校验
|
||||
if (!FIELD_TYPES.includes(colDef.type)) {
|
||||
throw new DatabaseError(`Invalid type "${colDef.type}" for column "${colName}". Valid types: ${FIELD_TYPES.join(', ')}`, 'SCHEMA_ERROR');
|
||||
}
|
||||
// 主键计数
|
||||
if (colDef.primaryKey) {
|
||||
primaryKeyCount++;
|
||||
}
|
||||
}
|
||||
// 至少需要一个主键
|
||||
if (primaryKeyCount === 0) {
|
||||
throw new DatabaseError('Table must have at least one primary key column', 'SCHEMA_ERROR');
|
||||
}
|
||||
}
|
||||
/** 将 AST 列定义转换为 ColumnDef */
|
||||
function astColumnToColumnDef(astCol) {
|
||||
return {
|
||||
type: astCol.type,
|
||||
primaryKey: astCol.primaryKey,
|
||||
unique: astCol.unique,
|
||||
required: astCol.required,
|
||||
default: astCol.default,
|
||||
index: astCol.index,
|
||||
maxLength: astCol.maxLength,
|
||||
min: astCol.min,
|
||||
max: astCol.max,
|
||||
references: astCol.references,
|
||||
onDelete: astCol.onDelete,
|
||||
onUpdate: astCol.onUpdate,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* metona-sqlark Query Compiler — AST → 查询计划
|
||||
* @module query/compiler
|
||||
|
||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+109
-80
@@ -1011,6 +1011,98 @@ class OPFSEngine {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* metona-sqlark Schema — 表结构定义与校验
|
||||
* @module table/schema
|
||||
*/
|
||||
// ---------------------------------------------------------------------------
|
||||
// Schema 工具
|
||||
// ---------------------------------------------------------------------------
|
||||
/** 从列定义创建 TableSchema */
|
||||
function createSchema(name, columns) {
|
||||
validateColumns(columns);
|
||||
return { name, columns };
|
||||
}
|
||||
/** 校验列定义 */
|
||||
function validateColumns(columns) {
|
||||
const colNames = Object.keys(columns);
|
||||
if (colNames.length === 0) {
|
||||
throw new DatabaseError('Table must have at least one column', 'SCHEMA_ERROR');
|
||||
}
|
||||
let primaryKeyCount = 0;
|
||||
for (const [colName, colDef] of Object.entries(columns)) {
|
||||
// 类型校验
|
||||
if (!FIELD_TYPES.includes(colDef.type)) {
|
||||
throw new DatabaseError(`Invalid type "${colDef.type}" for column "${colName}". Valid types: ${FIELD_TYPES.join(', ')}`, 'SCHEMA_ERROR');
|
||||
}
|
||||
// 主键计数
|
||||
if (colDef.primaryKey) {
|
||||
primaryKeyCount++;
|
||||
}
|
||||
}
|
||||
// 至少需要一个主键
|
||||
if (primaryKeyCount === 0) {
|
||||
throw new DatabaseError('Table must have at least one primary key column', 'SCHEMA_ERROR');
|
||||
}
|
||||
}
|
||||
/** 检查字段类型(含约束校验) */
|
||||
function checkFieldType(tableName, colName, type, value, colDef) {
|
||||
const jsType = typeof value;
|
||||
switch (type) {
|
||||
case 'string':
|
||||
if (jsType !== 'string') {
|
||||
throw new DatabaseError(`Column "${colName}" in table "${tableName}" expects string, got ${jsType}`, 'TYPE_ERROR');
|
||||
}
|
||||
if (colDef?.maxLength !== undefined && value.length > colDef.maxLength) {
|
||||
throw new DatabaseError(`Column "${colName}" in table "${tableName}" exceeds max length ${colDef.maxLength}`, 'VALIDATION_ERROR');
|
||||
}
|
||||
break;
|
||||
case 'number':
|
||||
if (jsType !== 'number') {
|
||||
throw new DatabaseError(`Column "${colName}" in table "${tableName}" expects number, got ${jsType}`, 'TYPE_ERROR');
|
||||
}
|
||||
if (colDef?.min !== undefined && value < colDef.min) {
|
||||
throw new DatabaseError(`Column "${colName}" in table "${tableName}" value ${value} below minimum ${colDef.min}`, 'VALIDATION_ERROR');
|
||||
}
|
||||
if (colDef?.max !== undefined && value > colDef.max) {
|
||||
throw new DatabaseError(`Column "${colName}" in table "${tableName}" value ${value} above maximum ${colDef.max}`, 'VALIDATION_ERROR');
|
||||
}
|
||||
break;
|
||||
case 'boolean':
|
||||
if (jsType !== 'boolean') {
|
||||
throw new DatabaseError(`Column "${colName}" in table "${tableName}" expects boolean, got ${jsType}`, 'TYPE_ERROR');
|
||||
}
|
||||
break;
|
||||
case 'date':
|
||||
if (jsType !== 'string' || isNaN(Date.parse(value))) {
|
||||
throw new DatabaseError(`Column "${colName}" in table "${tableName}" expects valid date string, got ${typeof value}`, 'TYPE_ERROR');
|
||||
}
|
||||
break;
|
||||
case 'json':
|
||||
if (jsType !== 'object') {
|
||||
throw new DatabaseError(`Column "${colName}" in table "${tableName}" expects object/array, got ${jsType}`, 'TYPE_ERROR');
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
/** 将 AST 列定义转换为 ColumnDef */
|
||||
function astColumnToColumnDef(astCol) {
|
||||
return {
|
||||
type: astCol.type,
|
||||
primaryKey: astCol.primaryKey,
|
||||
unique: astCol.unique,
|
||||
required: astCol.required,
|
||||
default: astCol.default,
|
||||
index: astCol.index,
|
||||
maxLength: astCol.maxLength,
|
||||
min: astCol.min,
|
||||
max: astCol.max,
|
||||
references: astCol.references,
|
||||
onDelete: astCol.onDelete,
|
||||
onUpdate: astCol.onUpdate,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* AriaEngine Types — 内部类型定义
|
||||
* @module engine/aria/types
|
||||
@@ -4180,6 +4272,7 @@ class AriaEngine {
|
||||
}
|
||||
async count(tableName, query) {
|
||||
this.ensureOpen();
|
||||
this.ensureTable(tableName);
|
||||
const rows = this.getAllRows(tableName);
|
||||
if (!query?.where || Object.keys(query.where).length === 0)
|
||||
return rows.length;
|
||||
@@ -4318,37 +4411,15 @@ class AriaEngine {
|
||||
throw new DatabaseError(`Column "${colName}" is required in table "${schema.name}"`, 'VALIDATION_ERROR');
|
||||
}
|
||||
if (value !== undefined && value !== null) {
|
||||
this.checkType(colName, colDef.type, value);
|
||||
this.checkType(colName, colDef.type, value, colDef);
|
||||
}
|
||||
if (value !== undefined)
|
||||
validated[colName] = value;
|
||||
}
|
||||
return validated;
|
||||
}
|
||||
checkType(colName, type, value) {
|
||||
const jsType = typeof value;
|
||||
switch (type) {
|
||||
case 'string':
|
||||
if (jsType !== 'string')
|
||||
throw new DatabaseError(`Column "${colName}" expects string, got ${jsType}`, 'TYPE_ERROR');
|
||||
break;
|
||||
case 'number':
|
||||
if (jsType !== 'number')
|
||||
throw new DatabaseError(`Column "${colName}" expects number, got ${jsType}`, 'TYPE_ERROR');
|
||||
break;
|
||||
case 'boolean':
|
||||
if (jsType !== 'boolean')
|
||||
throw new DatabaseError(`Column "${colName}" expects boolean, got ${jsType}`, 'TYPE_ERROR');
|
||||
break;
|
||||
case 'date':
|
||||
if (jsType !== 'string' || isNaN(Date.parse(value)))
|
||||
throw new DatabaseError(`Column "${colName}" expects valid date`, 'TYPE_ERROR');
|
||||
break;
|
||||
case 'json':
|
||||
if (jsType !== 'object')
|
||||
throw new DatabaseError(`Column "${colName}" expects object/array, got ${jsType}`, 'TYPE_ERROR');
|
||||
break;
|
||||
}
|
||||
checkType(colName, type, value, colDef) {
|
||||
checkFieldType('', colName, type, value, colDef);
|
||||
}
|
||||
// =======================================================================
|
||||
// Schema 持久化
|
||||
@@ -4567,8 +4638,16 @@ class AriaEngine {
|
||||
}
|
||||
// $gt / $gte / $lt / $lte → 范围扫描
|
||||
if ('$gt' in c || '$gte' in c || '$lt' in c || '$lte' in c) {
|
||||
const startKey = c.$gt ? `${String(Number(c.$gt) + 1)}:` : c.$gte ? `${String(c.$gte)}:` : `${col}:`;
|
||||
const endKey = c.$lt ? `${String(Number(c.$lt) - 1)}:\uffff` : c.$lte ? `${String(c.$lte)}:\uffff` : `${col}:\uffff`;
|
||||
let startKey = '';
|
||||
let endKey = '\uffff';
|
||||
if (c.$gt !== undefined)
|
||||
startKey = `${String(Number(c.$gt) + 1)}:`;
|
||||
else if (c.$gte !== undefined)
|
||||
startKey = `${String(c.$gte)}:`;
|
||||
if (c.$lt !== undefined)
|
||||
endKey = `${String(Number(c.$lt) - 1)}:\uffff`;
|
||||
else if (c.$lte !== undefined)
|
||||
endKey = `${String(c.$lte)}:\uffff`;
|
||||
return this.indexScanToRows(tableName, pkCol, idxLsm, col, startKey, endKey);
|
||||
}
|
||||
}
|
||||
@@ -4576,7 +4655,9 @@ class AriaEngine {
|
||||
}
|
||||
/** 从索引扫描结果恢复完整行 */
|
||||
indexScanToRows(tableName, pkCol, idxLsm, _col, startKey, endKey) {
|
||||
const entries = idxLsm.rangeScan(startKey, endKey);
|
||||
// 使用前缀扫描:endKey 需要包含 \uffff 以匹配所有带后缀的 key
|
||||
const actualEndKey = endKey.includes('\uffff') ? endKey : `${endKey}\uffff`;
|
||||
const entries = idxLsm.rangeScan(startKey, actualEndKey);
|
||||
const rows = [];
|
||||
for (const [, idxEntry] of entries) {
|
||||
const pk = idxEntry.pk;
|
||||
@@ -5080,58 +5161,6 @@ class Table {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* metona-sqlark Schema — 表结构定义与校验
|
||||
* @module table/schema
|
||||
*/
|
||||
// ---------------------------------------------------------------------------
|
||||
// Schema 工具
|
||||
// ---------------------------------------------------------------------------
|
||||
/** 从列定义创建 TableSchema */
|
||||
function createSchema(name, columns) {
|
||||
validateColumns(columns);
|
||||
return { name, columns };
|
||||
}
|
||||
/** 校验列定义 */
|
||||
function validateColumns(columns) {
|
||||
const colNames = Object.keys(columns);
|
||||
if (colNames.length === 0) {
|
||||
throw new DatabaseError('Table must have at least one column', 'SCHEMA_ERROR');
|
||||
}
|
||||
let primaryKeyCount = 0;
|
||||
for (const [colName, colDef] of Object.entries(columns)) {
|
||||
// 类型校验
|
||||
if (!FIELD_TYPES.includes(colDef.type)) {
|
||||
throw new DatabaseError(`Invalid type "${colDef.type}" for column "${colName}". Valid types: ${FIELD_TYPES.join(', ')}`, 'SCHEMA_ERROR');
|
||||
}
|
||||
// 主键计数
|
||||
if (colDef.primaryKey) {
|
||||
primaryKeyCount++;
|
||||
}
|
||||
}
|
||||
// 至少需要一个主键
|
||||
if (primaryKeyCount === 0) {
|
||||
throw new DatabaseError('Table must have at least one primary key column', 'SCHEMA_ERROR');
|
||||
}
|
||||
}
|
||||
/** 将 AST 列定义转换为 ColumnDef */
|
||||
function astColumnToColumnDef(astCol) {
|
||||
return {
|
||||
type: astCol.type,
|
||||
primaryKey: astCol.primaryKey,
|
||||
unique: astCol.unique,
|
||||
required: astCol.required,
|
||||
default: astCol.default,
|
||||
index: astCol.index,
|
||||
maxLength: astCol.maxLength,
|
||||
min: astCol.min,
|
||||
max: astCol.max,
|
||||
references: astCol.references,
|
||||
onDelete: astCol.onDelete,
|
||||
onUpdate: astCol.onUpdate,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* metona-sqlark Query Compiler — AST → 查询计划
|
||||
* @module query/compiler
|
||||
|
||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+109
-80
@@ -1017,6 +1017,98 @@
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* metona-sqlark Schema — 表结构定义与校验
|
||||
* @module table/schema
|
||||
*/
|
||||
// ---------------------------------------------------------------------------
|
||||
// Schema 工具
|
||||
// ---------------------------------------------------------------------------
|
||||
/** 从列定义创建 TableSchema */
|
||||
function createSchema(name, columns) {
|
||||
validateColumns(columns);
|
||||
return { name, columns };
|
||||
}
|
||||
/** 校验列定义 */
|
||||
function validateColumns(columns) {
|
||||
const colNames = Object.keys(columns);
|
||||
if (colNames.length === 0) {
|
||||
throw new DatabaseError('Table must have at least one column', 'SCHEMA_ERROR');
|
||||
}
|
||||
let primaryKeyCount = 0;
|
||||
for (const [colName, colDef] of Object.entries(columns)) {
|
||||
// 类型校验
|
||||
if (!FIELD_TYPES.includes(colDef.type)) {
|
||||
throw new DatabaseError(`Invalid type "${colDef.type}" for column "${colName}". Valid types: ${FIELD_TYPES.join(', ')}`, 'SCHEMA_ERROR');
|
||||
}
|
||||
// 主键计数
|
||||
if (colDef.primaryKey) {
|
||||
primaryKeyCount++;
|
||||
}
|
||||
}
|
||||
// 至少需要一个主键
|
||||
if (primaryKeyCount === 0) {
|
||||
throw new DatabaseError('Table must have at least one primary key column', 'SCHEMA_ERROR');
|
||||
}
|
||||
}
|
||||
/** 检查字段类型(含约束校验) */
|
||||
function checkFieldType(tableName, colName, type, value, colDef) {
|
||||
const jsType = typeof value;
|
||||
switch (type) {
|
||||
case 'string':
|
||||
if (jsType !== 'string') {
|
||||
throw new DatabaseError(`Column "${colName}" in table "${tableName}" expects string, got ${jsType}`, 'TYPE_ERROR');
|
||||
}
|
||||
if (colDef?.maxLength !== undefined && value.length > colDef.maxLength) {
|
||||
throw new DatabaseError(`Column "${colName}" in table "${tableName}" exceeds max length ${colDef.maxLength}`, 'VALIDATION_ERROR');
|
||||
}
|
||||
break;
|
||||
case 'number':
|
||||
if (jsType !== 'number') {
|
||||
throw new DatabaseError(`Column "${colName}" in table "${tableName}" expects number, got ${jsType}`, 'TYPE_ERROR');
|
||||
}
|
||||
if (colDef?.min !== undefined && value < colDef.min) {
|
||||
throw new DatabaseError(`Column "${colName}" in table "${tableName}" value ${value} below minimum ${colDef.min}`, 'VALIDATION_ERROR');
|
||||
}
|
||||
if (colDef?.max !== undefined && value > colDef.max) {
|
||||
throw new DatabaseError(`Column "${colName}" in table "${tableName}" value ${value} above maximum ${colDef.max}`, 'VALIDATION_ERROR');
|
||||
}
|
||||
break;
|
||||
case 'boolean':
|
||||
if (jsType !== 'boolean') {
|
||||
throw new DatabaseError(`Column "${colName}" in table "${tableName}" expects boolean, got ${jsType}`, 'TYPE_ERROR');
|
||||
}
|
||||
break;
|
||||
case 'date':
|
||||
if (jsType !== 'string' || isNaN(Date.parse(value))) {
|
||||
throw new DatabaseError(`Column "${colName}" in table "${tableName}" expects valid date string, got ${typeof value}`, 'TYPE_ERROR');
|
||||
}
|
||||
break;
|
||||
case 'json':
|
||||
if (jsType !== 'object') {
|
||||
throw new DatabaseError(`Column "${colName}" in table "${tableName}" expects object/array, got ${jsType}`, 'TYPE_ERROR');
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
/** 将 AST 列定义转换为 ColumnDef */
|
||||
function astColumnToColumnDef(astCol) {
|
||||
return {
|
||||
type: astCol.type,
|
||||
primaryKey: astCol.primaryKey,
|
||||
unique: astCol.unique,
|
||||
required: astCol.required,
|
||||
default: astCol.default,
|
||||
index: astCol.index,
|
||||
maxLength: astCol.maxLength,
|
||||
min: astCol.min,
|
||||
max: astCol.max,
|
||||
references: astCol.references,
|
||||
onDelete: astCol.onDelete,
|
||||
onUpdate: astCol.onUpdate,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* AriaEngine Types — 内部类型定义
|
||||
* @module engine/aria/types
|
||||
@@ -4186,6 +4278,7 @@
|
||||
}
|
||||
async count(tableName, query) {
|
||||
this.ensureOpen();
|
||||
this.ensureTable(tableName);
|
||||
const rows = this.getAllRows(tableName);
|
||||
if (!query?.where || Object.keys(query.where).length === 0)
|
||||
return rows.length;
|
||||
@@ -4324,37 +4417,15 @@
|
||||
throw new DatabaseError(`Column "${colName}" is required in table "${schema.name}"`, 'VALIDATION_ERROR');
|
||||
}
|
||||
if (value !== undefined && value !== null) {
|
||||
this.checkType(colName, colDef.type, value);
|
||||
this.checkType(colName, colDef.type, value, colDef);
|
||||
}
|
||||
if (value !== undefined)
|
||||
validated[colName] = value;
|
||||
}
|
||||
return validated;
|
||||
}
|
||||
checkType(colName, type, value) {
|
||||
const jsType = typeof value;
|
||||
switch (type) {
|
||||
case 'string':
|
||||
if (jsType !== 'string')
|
||||
throw new DatabaseError(`Column "${colName}" expects string, got ${jsType}`, 'TYPE_ERROR');
|
||||
break;
|
||||
case 'number':
|
||||
if (jsType !== 'number')
|
||||
throw new DatabaseError(`Column "${colName}" expects number, got ${jsType}`, 'TYPE_ERROR');
|
||||
break;
|
||||
case 'boolean':
|
||||
if (jsType !== 'boolean')
|
||||
throw new DatabaseError(`Column "${colName}" expects boolean, got ${jsType}`, 'TYPE_ERROR');
|
||||
break;
|
||||
case 'date':
|
||||
if (jsType !== 'string' || isNaN(Date.parse(value)))
|
||||
throw new DatabaseError(`Column "${colName}" expects valid date`, 'TYPE_ERROR');
|
||||
break;
|
||||
case 'json':
|
||||
if (jsType !== 'object')
|
||||
throw new DatabaseError(`Column "${colName}" expects object/array, got ${jsType}`, 'TYPE_ERROR');
|
||||
break;
|
||||
}
|
||||
checkType(colName, type, value, colDef) {
|
||||
checkFieldType('', colName, type, value, colDef);
|
||||
}
|
||||
// =======================================================================
|
||||
// Schema 持久化
|
||||
@@ -4573,8 +4644,16 @@
|
||||
}
|
||||
// $gt / $gte / $lt / $lte → 范围扫描
|
||||
if ('$gt' in c || '$gte' in c || '$lt' in c || '$lte' in c) {
|
||||
const startKey = c.$gt ? `${String(Number(c.$gt) + 1)}:` : c.$gte ? `${String(c.$gte)}:` : `${col}:`;
|
||||
const endKey = c.$lt ? `${String(Number(c.$lt) - 1)}:\uffff` : c.$lte ? `${String(c.$lte)}:\uffff` : `${col}:\uffff`;
|
||||
let startKey = '';
|
||||
let endKey = '\uffff';
|
||||
if (c.$gt !== undefined)
|
||||
startKey = `${String(Number(c.$gt) + 1)}:`;
|
||||
else if (c.$gte !== undefined)
|
||||
startKey = `${String(c.$gte)}:`;
|
||||
if (c.$lt !== undefined)
|
||||
endKey = `${String(Number(c.$lt) - 1)}:\uffff`;
|
||||
else if (c.$lte !== undefined)
|
||||
endKey = `${String(c.$lte)}:\uffff`;
|
||||
return this.indexScanToRows(tableName, pkCol, idxLsm, col, startKey, endKey);
|
||||
}
|
||||
}
|
||||
@@ -4582,7 +4661,9 @@
|
||||
}
|
||||
/** 从索引扫描结果恢复完整行 */
|
||||
indexScanToRows(tableName, pkCol, idxLsm, _col, startKey, endKey) {
|
||||
const entries = idxLsm.rangeScan(startKey, endKey);
|
||||
// 使用前缀扫描:endKey 需要包含 \uffff 以匹配所有带后缀的 key
|
||||
const actualEndKey = endKey.includes('\uffff') ? endKey : `${endKey}\uffff`;
|
||||
const entries = idxLsm.rangeScan(startKey, actualEndKey);
|
||||
const rows = [];
|
||||
for (const [, idxEntry] of entries) {
|
||||
const pk = idxEntry.pk;
|
||||
@@ -5086,58 +5167,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* metona-sqlark Schema — 表结构定义与校验
|
||||
* @module table/schema
|
||||
*/
|
||||
// ---------------------------------------------------------------------------
|
||||
// Schema 工具
|
||||
// ---------------------------------------------------------------------------
|
||||
/** 从列定义创建 TableSchema */
|
||||
function createSchema(name, columns) {
|
||||
validateColumns(columns);
|
||||
return { name, columns };
|
||||
}
|
||||
/** 校验列定义 */
|
||||
function validateColumns(columns) {
|
||||
const colNames = Object.keys(columns);
|
||||
if (colNames.length === 0) {
|
||||
throw new DatabaseError('Table must have at least one column', 'SCHEMA_ERROR');
|
||||
}
|
||||
let primaryKeyCount = 0;
|
||||
for (const [colName, colDef] of Object.entries(columns)) {
|
||||
// 类型校验
|
||||
if (!FIELD_TYPES.includes(colDef.type)) {
|
||||
throw new DatabaseError(`Invalid type "${colDef.type}" for column "${colName}". Valid types: ${FIELD_TYPES.join(', ')}`, 'SCHEMA_ERROR');
|
||||
}
|
||||
// 主键计数
|
||||
if (colDef.primaryKey) {
|
||||
primaryKeyCount++;
|
||||
}
|
||||
}
|
||||
// 至少需要一个主键
|
||||
if (primaryKeyCount === 0) {
|
||||
throw new DatabaseError('Table must have at least one primary key column', 'SCHEMA_ERROR');
|
||||
}
|
||||
}
|
||||
/** 将 AST 列定义转换为 ColumnDef */
|
||||
function astColumnToColumnDef(astCol) {
|
||||
return {
|
||||
type: astCol.type,
|
||||
primaryKey: astCol.primaryKey,
|
||||
unique: astCol.unique,
|
||||
required: astCol.required,
|
||||
default: astCol.default,
|
||||
index: astCol.index,
|
||||
maxLength: astCol.maxLength,
|
||||
min: astCol.min,
|
||||
max: astCol.max,
|
||||
references: astCol.references,
|
||||
onDelete: astCol.onDelete,
|
||||
onUpdate: astCol.onUpdate,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* metona-sqlark Query Compiler — AST → 查询计划
|
||||
* @module query/compiler
|
||||
|
||||
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
+14
-13
@@ -9,6 +9,7 @@ import type { IStorageEngine } from '../interface';
|
||||
import type { QueryPlan, TableSchema, ColumnDef } from '../../constants';
|
||||
import { DatabaseError } from '../../constants';
|
||||
import { matchWhere, applyOrderBy, projectColumns } from '../../query/where-matcher';
|
||||
import { checkFieldType } from '../../table/schema';
|
||||
|
||||
import type { AriaEngineConfig, SSTableMeta } from './types';
|
||||
import { DEFAULT_ARIA_CONFIG } from './types';
|
||||
@@ -460,6 +461,7 @@ export class AriaEngine implements IStorageEngine {
|
||||
|
||||
async count(tableName: string, query?: QueryPlan): Promise<number> {
|
||||
this.ensureOpen();
|
||||
this.ensureTable(tableName);
|
||||
const rows = this.getAllRows(tableName);
|
||||
if (!query?.where || Object.keys(query.where).length === 0) return rows.length;
|
||||
return rows.filter((row) => matchWhere(row, query.where!)).length;
|
||||
@@ -609,22 +611,15 @@ export class AriaEngine implements IStorageEngine {
|
||||
throw new DatabaseError(`Column "${colName}" is required in table "${schema.name}"`, 'VALIDATION_ERROR');
|
||||
}
|
||||
if (value !== undefined && value !== null) {
|
||||
this.checkType(colName, colDef.type, value);
|
||||
this.checkType(colName, colDef.type, value, colDef);
|
||||
}
|
||||
if (value !== undefined) validated[colName] = value;
|
||||
}
|
||||
return validated;
|
||||
}
|
||||
|
||||
private checkType(colName: string, type: string, value: unknown): void {
|
||||
const jsType = typeof value;
|
||||
switch (type) {
|
||||
case 'string': if (jsType !== 'string') throw new DatabaseError(`Column "${colName}" expects string, got ${jsType}`, 'TYPE_ERROR'); break;
|
||||
case 'number': if (jsType !== 'number') throw new DatabaseError(`Column "${colName}" expects number, got ${jsType}`, 'TYPE_ERROR'); break;
|
||||
case 'boolean': if (jsType !== 'boolean') throw new DatabaseError(`Column "${colName}" expects boolean, got ${jsType}`, 'TYPE_ERROR'); break;
|
||||
case 'date': if (jsType !== 'string' || isNaN(Date.parse(value as string))) throw new DatabaseError(`Column "${colName}" expects valid date`, 'TYPE_ERROR'); break;
|
||||
case 'json': if (jsType !== 'object') throw new DatabaseError(`Column "${colName}" expects object/array, got ${jsType}`, 'TYPE_ERROR'); break;
|
||||
}
|
||||
private checkType(colName: string, type: string, value: unknown, colDef?: ColumnDef): void {
|
||||
checkFieldType('', colName, type as any, value, colDef);
|
||||
}
|
||||
|
||||
// =======================================================================
|
||||
@@ -873,8 +868,12 @@ export class AriaEngine implements IStorageEngine {
|
||||
}
|
||||
// $gt / $gte / $lt / $lte → 范围扫描
|
||||
if ('$gt' in c || '$gte' in c || '$lt' in c || '$lte' in c) {
|
||||
const startKey = c.$gt ? `${String(Number(c.$gt) + 1)}:` : c.$gte ? `${String(c.$gte)}:` : `${col}:`;
|
||||
const endKey = c.$lt ? `${String(Number(c.$lt) - 1)}:\uffff` : c.$lte ? `${String(c.$lte)}:\uffff` : `${col}:\uffff`;
|
||||
let startKey = '';
|
||||
let endKey = '\uffff';
|
||||
if (c.$gt !== undefined) startKey = `${String(Number(c.$gt) + 1)}:`;
|
||||
else if (c.$gte !== undefined) startKey = `${String(c.$gte)}:`;
|
||||
if (c.$lt !== undefined) endKey = `${String(Number(c.$lt) - 1)}:\uffff`;
|
||||
else if (c.$lte !== undefined) endKey = `${String(c.$lte)}:\uffff`;
|
||||
return this.indexScanToRows(tableName, pkCol, idxLsm, col, startKey, endKey);
|
||||
}
|
||||
}
|
||||
@@ -887,7 +886,9 @@ export class AriaEngine implements IStorageEngine {
|
||||
tableName: string, pkCol: string, idxLsm: LSM,
|
||||
_col: string, startKey: string, endKey: string,
|
||||
): Record<string, unknown>[] {
|
||||
const entries = idxLsm.rangeScan(startKey, endKey);
|
||||
// 使用前缀扫描:endKey 需要包含 \uffff 以匹配所有带后缀的 key
|
||||
const actualEndKey = endKey.includes('\uffff') ? endKey : `${endKey}\uffff`;
|
||||
const entries = idxLsm.rangeScan(startKey, actualEndKey);
|
||||
const rows: Record<string, unknown>[] = [];
|
||||
for (const [, idxEntry] of entries) {
|
||||
const pk = (idxEntry as any).pk as string;
|
||||
|
||||
@@ -0,0 +1,158 @@
|
||||
/**
|
||||
* AriaEngine BloomFilter + WAL恢复 + 压缩 + BufferPool 完整测试
|
||||
* 全部 MemoryBackend,零卡死
|
||||
*/
|
||||
import { AriaEngine } from '../../src/engine/aria/index';
|
||||
import { BloomFilter } from '../../src/engine/aria/index/bloom';
|
||||
import { WAL, type WALStore } from '../../src/engine/aria/wal/log';
|
||||
import { WALRecordType } from '../../src/engine/aria/types';
|
||||
import { BufferPool } from '../../src/engine/aria/buffer/pool';
|
||||
import { MemoryBackend } from '../../src/engine/aria/store/backend';
|
||||
import { createSchema } from '../../src/table/schema';
|
||||
|
||||
describe('AriaEngine — Bloom + WAL + BufferPool', () => {
|
||||
// ---- BloomFilter 完整测试 ----
|
||||
describe('BloomFilter', () => {
|
||||
it('1000 条插入后 false positive 率 < 5%', () => {
|
||||
const bf = new BloomFilter(1000, 10);
|
||||
for (let i = 0; i < 1000; i++) bf.insert(`key-${i}`);
|
||||
let fp = 0;
|
||||
for (let i = 0; i < 1000; i++) {
|
||||
if (bf.mayContain(`absent-${i}`)) fp++;
|
||||
}
|
||||
expect(fp).toBeLessThan(50);
|
||||
});
|
||||
|
||||
it('大量数据 serialize/fromData 往返', () => {
|
||||
const bf1 = new BloomFilter(500, 8);
|
||||
for (let i = 0; i < 500; i++) bf1.insert(`item-${i}`);
|
||||
const data = bf1.serialize();
|
||||
const bf2 = BloomFilter.fromData(data, bf1.getHashCount());
|
||||
for (let i = 0; i < 500; i++) {
|
||||
expect(bf2.mayContain(`item-${i}`)).toBe(true);
|
||||
}
|
||||
expect(bf2.mayContain('never-added')).toBe(false);
|
||||
});
|
||||
|
||||
it('不同哈希函数数量影响误判率', () => {
|
||||
const bf1 = new BloomFilter(200, 2);
|
||||
const bf2 = new BloomFilter(200, 8);
|
||||
for (let i = 0; i < 200; i++) {
|
||||
bf1.insert(`k-${i}`);
|
||||
bf2.insert(`k-${i}`);
|
||||
}
|
||||
let fp1 = 0, fp2 = 0;
|
||||
for (let i = 0; i < 200; i++) {
|
||||
if (bf1.mayContain(`x-${i}`)) fp1++;
|
||||
if (bf2.mayContain(`x-${i}`)) fp2++;
|
||||
}
|
||||
expect(fp2).toBeLessThanOrEqual(fp1 + 20);
|
||||
});
|
||||
});
|
||||
|
||||
// ---- WAL 恢复完整测试 ----
|
||||
describe('WAL Recovery', () => {
|
||||
class MemStore implements WALStore {
|
||||
chunks: Uint8Array[] = [];
|
||||
async append(d: Uint8Array) { this.chunks.push(d); }
|
||||
async readAll() {
|
||||
const t = this.chunks.reduce((s, c) => s + c.byteLength, 0);
|
||||
const c = new Uint8Array(t); let o = 0;
|
||||
for (const ch of this.chunks) { c.set(ch, o); o += ch.byteLength; }
|
||||
return c;
|
||||
}
|
||||
async truncate() { this.chunks = []; }
|
||||
async exists() { return this.chunks.length > 0; }
|
||||
}
|
||||
|
||||
it('多事务 WAL 恢复仅回放已提交', async () => {
|
||||
const store = new MemStore();
|
||||
const wal = new WAL(store, true, 'full');
|
||||
|
||||
// 事务1: commit
|
||||
wal.append({ type: WALRecordType.BEGIN, txnId: 1, tableName: '', key: '' });
|
||||
wal.append({ type: WALRecordType.INSERT, txnId: 1, tableName: 't', key: 'a', data: { v: 1 } });
|
||||
wal.append({ type: WALRecordType.COMMIT, txnId: 1, tableName: '', key: '' });
|
||||
|
||||
// 事务2: rollback (不回放)
|
||||
wal.append({ type: WALRecordType.BEGIN, txnId: 2, tableName: '', key: '' });
|
||||
wal.append({ type: WALRecordType.INSERT, txnId: 2, tableName: 't', key: 'b', data: { v: 2 } });
|
||||
wal.append({ type: WALRecordType.ROLLBACK, txnId: 2, tableName: '', key: '' });
|
||||
|
||||
// 事务3: 未完成(无 COMMIT/ROLLBACK)— 不回放
|
||||
wal.append({ type: WALRecordType.BEGIN, txnId: 3, tableName: '', key: '' });
|
||||
wal.append({ type: WALRecordType.INSERT, txnId: 3, tableName: 't', key: 'c', data: { v: 3 } });
|
||||
|
||||
const records: any[] = [];
|
||||
await wal.recover((r) => records.push(r));
|
||||
const committed = records.filter((r: any) => r.type === WALRecordType.INSERT && r.data);
|
||||
expect(committed).toHaveLength(3); // recover keeps all, filtering is done in engine
|
||||
});
|
||||
|
||||
it('CRC 损坏记录被跳过', async () => {
|
||||
const store = new MemStore();
|
||||
const wal = new WAL(store, true, 'full');
|
||||
wal.append({ type: WALRecordType.INSERT, txnId: 1, tableName: 't', key: 'ok', data: { v: 1 } });
|
||||
// 手动损坏 WAL 数据
|
||||
store.chunks[0][0] = 0xFF; // 破坏第一个字节
|
||||
const records: any[] = [];
|
||||
await wal.recover((r) => records.push(r));
|
||||
expect(records.length).toBeLessThanOrEqual(1);
|
||||
});
|
||||
});
|
||||
|
||||
// ---- BufferPool 集成 ----
|
||||
describe('BufferPool', () => {
|
||||
it('BufferPool 能分配和释放页面', async () => {
|
||||
const backend = new MemoryBackend();
|
||||
await backend.open('bp-test');
|
||||
const pageIO = {
|
||||
readPage: async (id: number) => backend.read(`pg_${id}`),
|
||||
writePage: async (id: number, d: ArrayBuffer) => backend.write(`pg_${id}`, d),
|
||||
allocatePageId: async () => Date.now(),
|
||||
freePageId: async (_id: number) => {},
|
||||
};
|
||||
const pool = new BufferPool(pageIO, 4);
|
||||
const page = await pool.newPage();
|
||||
expect(page.pageId).toBeGreaterThan(0);
|
||||
expect(page.pins).toBe(1);
|
||||
pool.unpin(page);
|
||||
await backend.close();
|
||||
});
|
||||
|
||||
it('BufferPool flushAll 刷新脏页', async () => {
|
||||
const backend = new MemoryBackend();
|
||||
await backend.open('bp-test2');
|
||||
const pageIO = {
|
||||
readPage: async (id: number) => backend.read(`pg_${id}`),
|
||||
writePage: async (id: number, d: ArrayBuffer) => backend.write(`pg_${id}`, d),
|
||||
allocatePageId: async () => 1,
|
||||
freePageId: async (_id: number) => {},
|
||||
};
|
||||
const pool = new BufferPool(pageIO, 4);
|
||||
const page = await pool.newPage();
|
||||
pool.markDirty(page);
|
||||
pool.unpin(page);
|
||||
await pool.flushAll();
|
||||
expect(pool.getDirtyPageCount()).toBe(0);
|
||||
await backend.close();
|
||||
});
|
||||
});
|
||||
|
||||
// ---- 引擎级压缩/加密开关测试 ----
|
||||
describe('Compression+Crypto toggle', () => {
|
||||
it('compression=false 引擎正常启动', async () => {
|
||||
const e = new AriaEngine({ storageBackend: 'memory', compression: false });
|
||||
await e.open('comp-test', 1);
|
||||
expect(e.isOpen()).toBe(true);
|
||||
await e.close();
|
||||
});
|
||||
|
||||
it('compression=true 引擎正常启动', async () => {
|
||||
const e = new AriaEngine({ storageBackend: 'memory', compression: true });
|
||||
await e.open('comp-test2', 1);
|
||||
expect(e.isOpen()).toBe(true);
|
||||
await e.close();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,137 @@
|
||||
/**
|
||||
* AriaEngine 最终扩展测试 — 全部 MemoryBackend
|
||||
*/
|
||||
import { AriaEngine } from '../../src/engine/aria/index';
|
||||
import { createSchema } from '../../src/table/schema';
|
||||
import { MemoryBackend } from '../../src/engine/aria/store/backend';
|
||||
|
||||
describe('AriaEngine — 批量扩展测试', () => {
|
||||
let engine: AriaEngine;
|
||||
beforeEach(async () => { engine = new AriaEngine({ storageBackend: 'memory' }); await engine.open('bat', 1); });
|
||||
afterEach(async () => { await engine.close(); });
|
||||
|
||||
it('事务 commit', async () => {
|
||||
await engine.createTable(createSchema('t', { id: { type: 'string', primaryKey: true }, n: { type: 'string' } }));
|
||||
await engine.beginTransaction();
|
||||
await engine.insert('t', [{ id: '1', n: 'a' }, { id: '2', n: 'b' }]);
|
||||
await engine.commitTransaction();
|
||||
expect(await engine.count('t')).toBe(2);
|
||||
});
|
||||
it('事务 rollback', async () => {
|
||||
await engine.createTable(createSchema('t', { id: { type: 'string', primaryKey: true }, n: { type: 'string' } }));
|
||||
await engine.insert('t', [{ id: '1', n: 'a' }]);
|
||||
await engine.beginTransaction();
|
||||
await engine.insert('t', [{ id: '2', n: 'b' }]);
|
||||
await engine.rollbackTransaction();
|
||||
expect(await engine.count('t')).toBe(1);
|
||||
});
|
||||
it('insert 空数组', async () => {
|
||||
await engine.createTable(createSchema('t', { id: { type: 'string', primaryKey: true } }));
|
||||
const pks = await engine.insert('t', []);
|
||||
expect(pks).toHaveLength(0);
|
||||
});
|
||||
it('update 无匹配', async () => {
|
||||
await engine.createTable(createSchema('t', { id: { type: 'string', primaryKey: true }, n: { type: 'string' } }));
|
||||
await engine.insert('t', [{ id: '1', n: 'a' }]);
|
||||
expect(await engine.update('t', { table: 't', where: { id: 'x' } }, { n: 'x' })).toBe(0);
|
||||
});
|
||||
it('delete 无匹配', async () => {
|
||||
await engine.createTable(createSchema('t', { id: { type: 'string', primaryKey: true } }));
|
||||
await engine.insert('t', [{ id: '1' }]);
|
||||
expect(await engine.delete('t', { table: 't', where: { id: 'x' } })).toBe(0);
|
||||
});
|
||||
it('count 空表', async () => {
|
||||
await engine.createTable(createSchema('t', { id: { type: 'string', primaryKey: true } }));
|
||||
expect(await engine.count('t')).toBe(0);
|
||||
});
|
||||
it('find 空表', async () => {
|
||||
await engine.createTable(createSchema('t', { id: { type: 'string', primaryKey: true } }));
|
||||
expect(await engine.find('t', { table: 't' })).toHaveLength(0);
|
||||
});
|
||||
it('hasTable false', async () => { expect(await engine.hasTable('nope')).toBe(false); });
|
||||
it('dropTable 后 hasTable false', async () => {
|
||||
await engine.createTable(createSchema('t', { id: { type: 'string', primaryKey: true } }));
|
||||
await engine.dropTable('t');
|
||||
expect(await engine.hasTable('t')).toBe(false);
|
||||
});
|
||||
it('清空表保留结构', async () => {
|
||||
await engine.createTable(createSchema('t', { id: { type: 'string', primaryKey: true } }));
|
||||
await engine.insert('t', [{ id: '1' }, { id: '2' }]);
|
||||
await engine.clear('t');
|
||||
expect(await engine.count('t')).toBe(0);
|
||||
expect(await engine.hasTable('t')).toBe(true);
|
||||
});
|
||||
it('重复主键抛异常', async () => {
|
||||
await engine.createTable(createSchema('t', { id: { type: 'string', primaryKey: true } }));
|
||||
await engine.insert('t', [{ id: '1' }]);
|
||||
await expect(engine.insert('t', [{ id: '1' }])).rejects.toThrow('Duplicate');
|
||||
});
|
||||
it('类型不匹配抛异常', async () => {
|
||||
await engine.createTable(createSchema('t', { id: { type: 'string', primaryKey: true }, age: { type: 'number' } }));
|
||||
await expect(engine.insert('t', [{ id: '1', age: 'x' as any }])).rejects.toThrow('expects number');
|
||||
});
|
||||
it('必填字段缺失抛异常', async () => {
|
||||
await engine.createTable(createSchema('t', { id: { type: 'string', primaryKey: true }, name: { type: 'string', required: true } }));
|
||||
await expect(engine.insert('t', [{ id: '1' }])).rejects.toThrow('required');
|
||||
});
|
||||
it('MemoryBackend isOpen', async () => {
|
||||
const be = new MemoryBackend();
|
||||
expect(be.isOpen()).toBe(false);
|
||||
await be.open('x'); expect(be.isOpen()).toBe(true);
|
||||
await be.close();
|
||||
});
|
||||
it('引擎名称为 aria', () => { expect(engine.name).toBe('aria'); });
|
||||
it('默认值 number', async () => {
|
||||
await engine.createTable(createSchema('t', { id: { type: 'string', primaryKey: true }, val: { type: 'number', default: 42 } }));
|
||||
await engine.insert('t', [{ id: '1' }]);
|
||||
expect((await engine.find('t', { table: 't' }))[0].val).toBe(42);
|
||||
});
|
||||
it('默认值 boolean', async () => {
|
||||
await engine.createTable(createSchema('t', { id: { type: 'string', primaryKey: true }, active: { type: 'boolean', default: true } }));
|
||||
await engine.insert('t', [{ id: '1' }]);
|
||||
expect((await engine.find('t', { table: 't' }))[0].active).toBe(true);
|
||||
});
|
||||
it('Schema json 往返', async () => {
|
||||
await engine.createTable(createSchema('t', { id: { type: 'string', primaryKey: true }, meta: { type: 'json' } }));
|
||||
await engine.insert('t', [{ id: '1', meta: { a: 1, b: [2, 3] } }]);
|
||||
expect((await engine.find('t', { table: 't' }))[0].meta).toEqual({ a: 1, b: [2, 3] });
|
||||
});
|
||||
it('date 类型存储', async () => {
|
||||
await engine.createTable(createSchema('t', { id: { type: 'string', primaryKey: true }, ts: { type: 'date' } }));
|
||||
await engine.insert('t', [{ id: '1', ts: '2026-01-01T00:00:00.000Z' }]);
|
||||
expect((await engine.find('t', { table: 't' }))[0].ts).toBe('2026-01-01T00:00:00.000Z');
|
||||
});
|
||||
it('重复 open 不改变状态', async () => { await engine.open('bat', 1); expect(engine.isOpen()).toBe(true); });
|
||||
it('GC 调用不抛异常', () => { (engine as any).tryGC(); });
|
||||
it('内存检查不抛异常', () => { (engine as any).checkMemoryBudget(); });
|
||||
it('引擎状态 getTableNames', async () => {
|
||||
await engine.createTable(createSchema('t', { id: { type: 'string', primaryKey: true } }));
|
||||
expect(await engine.getTableNames()).toContain('t');
|
||||
});
|
||||
it('getTableSchema 返回正确结构', async () => {
|
||||
await engine.createTable(createSchema('t', { id: { type: 'string', primaryKey: true }, name: { type: 'string' } }));
|
||||
const s = await engine.getTableSchema('t');
|
||||
expect(s!.columns.id.primaryKey).toBe(true);
|
||||
});
|
||||
it('LIMIT 0 返回空', async () => {
|
||||
await engine.createTable(createSchema('t', { id: { type: 'string', primaryKey: true } }));
|
||||
await engine.insert('t', [{ id: '1' }, { id: '2' }]);
|
||||
expect(await engine.find('t', { table: 't', limit: 0 })).toHaveLength(0);
|
||||
});
|
||||
it('ORDER BY desc', async () => {
|
||||
await engine.createTable(createSchema('t', { id: { type: 'string', primaryKey: true }, v: { type: 'number' } }));
|
||||
await engine.insert('t', [{ id: 'a', v: 1 }, { id: 'b', v: 3 }, { id: 'c', v: 2 }]);
|
||||
const r = await engine.find('t', { table: 't', orderBy: [{ column: 'v', direction: 'desc' }] });
|
||||
expect(r[0].v).toBe(3); expect(r[2].v).toBe(1);
|
||||
});
|
||||
it('$eq 直接值', async () => {
|
||||
await engine.createTable(createSchema('t', { id: { type: 'string', primaryKey: true }, n: { type: 'string' } }));
|
||||
await engine.insert('t', [{ id: '1', n: 'Alice' }, { id: '2', n: 'Bob' }]);
|
||||
expect(await engine.find('t', { table: 't', where: { n: 'Alice' } })).toHaveLength(1);
|
||||
});
|
||||
it('$gte $lte 组合', async () => {
|
||||
await engine.createTable(createSchema('t', { id: { type: 'string', primaryKey: true }, v: { type: 'number' } }));
|
||||
for (let i = 0; i < 10; i++) await engine.insert('t', [{ id: `${i}`, v: i * 10 }]);
|
||||
expect(await engine.count('t', { table: 't', where: { v: { $gte: 20, $lte: 50 } } })).toBe(4);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,206 @@
|
||||
/**
|
||||
* AriaEngine 边缘场景扩展测试
|
||||
* 全部 MemoryBackend,零卡死
|
||||
*/
|
||||
import { AriaEngine } from '../../src/engine/aria/index';
|
||||
import { createSchema } from '../../src/table/schema';
|
||||
|
||||
describe('AriaEngine — 扩展边缘测试', () => {
|
||||
let engine: AriaEngine;
|
||||
|
||||
beforeEach(async () => {
|
||||
engine = new AriaEngine({ storageBackend: 'memory' });
|
||||
await engine.open('edge-ext-test', 1);
|
||||
});
|
||||
|
||||
afterEach(async () => { await engine.close(); });
|
||||
|
||||
describe('复杂查询', () => {
|
||||
beforeEach(async () => {
|
||||
await engine.createTable(createSchema('users', {
|
||||
id: { type: 'string', primaryKey: true },
|
||||
name: { type: 'string', required: true },
|
||||
age: { type: 'number', default: 0 },
|
||||
active: { type: 'boolean', default: true },
|
||||
score: { type: 'number', default: 0 },
|
||||
}));
|
||||
await engine.insert('users', [
|
||||
{ id: '1', name: 'Alice', age: 30, active: true, score: 85 },
|
||||
{ id: '2', name: 'Bob', age: 25, active: true, score: 92 },
|
||||
{ id: '3', name: 'Charlie', age: 35, active: false, score: 78 },
|
||||
{ id: '4', name: 'Diana', age: 30, active: true, score: 95 },
|
||||
{ id: '5', name: 'Eve', age: 25, active: true, score: 88 },
|
||||
]);
|
||||
});
|
||||
|
||||
it('$and 多条件', async () => {
|
||||
const rows = await engine.find('users', { table: 'users', where: { $and: [{ age: 30 }, { active: true }] } });
|
||||
expect(rows).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('$or 多条件', async () => {
|
||||
const rows = await engine.find('users', { table: 'users', where: { $or: [{ name: 'Alice' }, { name: 'Charlie' }] } });
|
||||
expect(rows).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('$not 条件', async () => {
|
||||
const rows = await engine.find('users', { table: 'users', where: { age: { $not: { $eq: 30 } } } });
|
||||
expect(rows).toHaveLength(3);
|
||||
});
|
||||
|
||||
it('$ne 不等于', async () => {
|
||||
const rows = await engine.find('users', { table: 'users', where: { age: { $ne: 25 } } });
|
||||
expect(rows).toHaveLength(3);
|
||||
});
|
||||
|
||||
it('$nin 不在列表中', async () => {
|
||||
const rows = await engine.find('users', { table: 'users', where: { age: { $nin: [25, 35] } } });
|
||||
expect(rows).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('$like 模糊匹配', async () => {
|
||||
const rows = await engine.find('users', { table: 'users', where: { name: { $like: 'A%' } } });
|
||||
expect(rows).toHaveLength(1);
|
||||
expect(rows[0].name).toBe('Alice');
|
||||
});
|
||||
|
||||
it('多列 ORDER BY', async () => {
|
||||
const rows = await engine.find('users', { table: 'users', orderBy: [{ column: 'age', direction: 'asc' }, { column: 'score', direction: 'desc' }] });
|
||||
expect(rows[0].age).toBeLessThanOrEqual(rows[4].age);
|
||||
});
|
||||
|
||||
it('LIMIT + OFFSET 翻页', async () => {
|
||||
const page1 = await engine.find('users', { table: 'users', orderBy: [{ column: 'id', direction: 'asc' }], limit: 2, offset: 0 });
|
||||
const page2 = await engine.find('users', { table: 'users', orderBy: [{ column: 'id', direction: 'asc' }], limit: 2, offset: 2 });
|
||||
expect(page1).toHaveLength(2);
|
||||
expect(page2).toHaveLength(2);
|
||||
expect(page1[0].id).not.toBe(page2[0].id);
|
||||
});
|
||||
|
||||
it('列投影', async () => {
|
||||
const rows = await engine.find('users', { table: 'users', columns: ['id', 'name'], where: { id: '1' } });
|
||||
expect(Object.keys(rows[0]).sort()).toEqual(['id', 'name']);
|
||||
});
|
||||
|
||||
it('空结果查询', async () => {
|
||||
const rows = await engine.find('users', { table: 'users', where: { age: { $gt: 999 } } });
|
||||
expect(rows).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('边界与错误', () => {
|
||||
it('未打开引擎操作抛异常', () => {
|
||||
const e = new AriaEngine({ storageBackend: 'memory' });
|
||||
return expect(e.createTable(createSchema('t', { id: { type: 'string', primaryKey: true } }))).rejects.toThrow('not opened');
|
||||
});
|
||||
|
||||
it('close 后操作抛异常', async () => {
|
||||
await engine.close();
|
||||
await expect(engine.find('users', { table: 'users' })).rejects.toThrow('not opened');
|
||||
});
|
||||
|
||||
it('重复 open 幂等', async () => {
|
||||
await engine.open('edge-ext-test', 1);
|
||||
expect(engine.isOpen()).toBe(true);
|
||||
});
|
||||
|
||||
it('创建已存在的表抛异常', async () => {
|
||||
await engine.createTable(createSchema('dup', { id: { type: 'string', primaryKey: true } }));
|
||||
await expect(engine.createTable(createSchema('dup', { id: { type: 'string', primaryKey: true } }))).rejects.toThrow('already exists');
|
||||
});
|
||||
|
||||
it('获取不存在的表 schema 返回 null', async () => {
|
||||
expect(await engine.getTableSchema('ghost')).toBeNull();
|
||||
});
|
||||
|
||||
it('hasTable 不存在的表返回 false', async () => {
|
||||
expect(await engine.hasTable('nope')).toBe(false);
|
||||
});
|
||||
|
||||
it('dropTable 不存在的表抛异常', async () => {
|
||||
await expect(engine.dropTable('nope')).rejects.toThrow('does not exist');
|
||||
});
|
||||
|
||||
it('insert 不存在的表抛异常', async () => {
|
||||
await expect(engine.insert('ghost', [{ id: '1' }])).rejects.toThrow('does not exist');
|
||||
});
|
||||
|
||||
it('update 不存在的表抛异常', async () => {
|
||||
await expect(engine.update('ghost', { table: 'ghost' }, {})).rejects.toThrow('does not exist');
|
||||
});
|
||||
|
||||
it('delete 不存在的表抛异常', async () => {
|
||||
await expect(engine.delete('ghost', { table: 'ghost' })).rejects.toThrow('does not exist');
|
||||
});
|
||||
|
||||
it('count 不存在的表抛异常', async () => {
|
||||
await expect(engine.count('ghost')).rejects.toThrow('does not exist');
|
||||
});
|
||||
|
||||
it('空表 count 返回 0', async () => {
|
||||
await engine.createTable(createSchema('empty', { id: { type: 'string', primaryKey: true } }));
|
||||
expect(await engine.count('empty')).toBe(0);
|
||||
});
|
||||
|
||||
it('空表 find 返回空数组', async () => {
|
||||
await engine.createTable(createSchema('empty2', { id: { type: 'string', primaryKey: true } }));
|
||||
expect(await engine.find('empty2', { table: 'empty2' })).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('clear 空表不报错', async () => {
|
||||
await engine.createTable(createSchema('empty3', { id: { type: 'string', primaryKey: true } }));
|
||||
await engine.clear('empty3');
|
||||
});
|
||||
});
|
||||
|
||||
describe('类型系统', () => {
|
||||
beforeEach(async () => {
|
||||
await engine.createTable(createSchema('types', {
|
||||
id: { type: 'string', primaryKey: true },
|
||||
name: { type: 'string', maxLength: 10 },
|
||||
age: { type: 'number', min: 0, max: 150 },
|
||||
active: { type: 'boolean' },
|
||||
created: { type: 'date' },
|
||||
meta: { type: 'json' },
|
||||
}));
|
||||
});
|
||||
|
||||
it('string maxLength 约束', async () => {
|
||||
await expect(engine.insert('types', [{ id: '1', name: 'verylongnamehere' }])).rejects.toThrow('exceeds max length');
|
||||
});
|
||||
|
||||
it('number min 约束', async () => {
|
||||
await expect(engine.insert('types', [{ id: '1', name: 'ok', age: -1 }])).rejects.toThrow('below minimum');
|
||||
});
|
||||
|
||||
it('number max 约束', async () => {
|
||||
await expect(engine.insert('types', [{ id: '1', name: 'ok', age: 200 }])).rejects.toThrow('above maximum');
|
||||
});
|
||||
|
||||
it('正确值通过所有约束', async () => {
|
||||
await engine.insert('types', [{ id: '1', name: 'Alice', age: 30, active: true, created: '2024-01-01', meta: { x: 1 } }]);
|
||||
expect(await engine.count('types')).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('大量数据', () => {
|
||||
it('200 行批量插入查询', async () => {
|
||||
await engine.createTable(createSchema('big', { id: { type: 'string', primaryKey: true }, val: { type: 'number' } }));
|
||||
const rows = [];
|
||||
for (let i = 0; i < 200; i++) rows.push({ id: `${i}`, val: i });
|
||||
await engine.insert('big', rows);
|
||||
expect(await engine.count('big')).toBe(200);
|
||||
const result = await engine.find('big', { table: 'big', orderBy: [{ column: 'val', direction: 'asc' }], limit: 10 });
|
||||
expect(result).toHaveLength(10);
|
||||
expect(result[0].val).toBe(0);
|
||||
});
|
||||
|
||||
it('200 行全量更新', async () => {
|
||||
await engine.createTable(createSchema('big2', { id: { type: 'string', primaryKey: true }, val: { type: 'number' } }));
|
||||
for (let i = 0; i < 200; i++) await engine.insert('big2', [{ id: `${i}`, val: i }]);
|
||||
await engine.update('big2', { table: 'big2' }, { val: 999 });
|
||||
const rows = await engine.find('big2', { table: 'big2', limit: 5 });
|
||||
expect(rows[0].val).toBe(999);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,164 @@
|
||||
/**
|
||||
* AriaEngine 最后补充测试
|
||||
*/
|
||||
import { AriaEngine } from '../../src/engine/aria/index';
|
||||
import { createSchema } from '../../src/table/schema';
|
||||
|
||||
describe('AriaEngine — 补充测试', () => {
|
||||
let engine: AriaEngine;
|
||||
beforeEach(async () => { engine = new AriaEngine({ storageBackend: 'memory' }); await engine.open('sup', 1); });
|
||||
afterEach(async () => { await engine.close(); });
|
||||
|
||||
it('a1 PK lookup', async () => {
|
||||
await engine.createTable(createSchema('t', { id: { type: 'string', primaryKey: true } }));
|
||||
await engine.insert('t', [{ id: 'x' }]);
|
||||
expect((await engine.find('t', { table: 't', where: { id: 'x' } }))[0].id).toBe('x');
|
||||
});
|
||||
it('a2 multi insert', async () => {
|
||||
await engine.createTable(createSchema('t', { id: { type: 'string', primaryKey: true }, v: { type: 'number' } }));
|
||||
const pks = await engine.insert('t', [{ id: 'a', v: 1 }, { id: 'b', v: 2 }]);
|
||||
expect(pks).toEqual(['a', 'b']);
|
||||
});
|
||||
it('a3 count after insert', async () => {
|
||||
await engine.createTable(createSchema('t', { id: { type: 'string', primaryKey: true } }));
|
||||
expect(await engine.count('t')).toBe(0);
|
||||
await engine.insert('t', [{ id: '1' }]);
|
||||
expect(await engine.count('t')).toBe(1);
|
||||
});
|
||||
it('a4 count with where', async () => {
|
||||
await engine.createTable(createSchema('t', { id: { type: 'string', primaryKey: true }, v: { type: 'number' } }));
|
||||
await engine.insert('t', [{ id: '1', v: 10 }, { id: '2', v: 20 }, { id: '3', v: 30 }]);
|
||||
expect(await engine.count('t', { table: 't', where: { v: { $gt: 15 } } })).toBe(2);
|
||||
});
|
||||
it('a5 find all', async () => {
|
||||
await engine.createTable(createSchema('t', { id: { type: 'string', primaryKey: true } }));
|
||||
await engine.insert('t', [{ id: '1' }, { id: '2' }, { id: '3' }]);
|
||||
expect(await engine.find('t', { table: 't' })).toHaveLength(3);
|
||||
});
|
||||
it('a6 find with $in', async () => {
|
||||
await engine.createTable(createSchema('t', { id: { type: 'string', primaryKey: true }, v: { type: 'number' } }));
|
||||
await engine.insert('t', [{ id: '1', v: 10 }, { id: '2', v: 20 }, { id: '3', v: 30 }]);
|
||||
expect(await engine.find('t', { table: 't', where: { v: { $in: [10, 30] } } })).toHaveLength(2);
|
||||
});
|
||||
it('a7 find with $ne', async () => {
|
||||
await engine.createTable(createSchema('t', { id: { type: 'string', primaryKey: true }, v: { type: 'number' } }));
|
||||
await engine.insert('t', [{ id: '1', v: 10 }, { id: '2', v: 20 }]);
|
||||
expect(await engine.find('t', { table: 't', where: { v: { $ne: 10 } } })).toHaveLength(1);
|
||||
});
|
||||
it('a8 order asc', async () => {
|
||||
await engine.createTable(createSchema('t', { id: { type: 'string', primaryKey: true }, v: { type: 'number' } }));
|
||||
await engine.insert('t', [{ id: 'c', v: 3 }, { id: 'a', v: 1 }, { id: 'b', v: 2 }]);
|
||||
const r = await engine.find('t', { table: 't', orderBy: [{ column: 'v', direction: 'asc' }] });
|
||||
expect(r.map((x: any) => x.v)).toEqual([1, 2, 3]);
|
||||
});
|
||||
it('a9 limit only', async () => {
|
||||
await engine.createTable(createSchema('t', { id: { type: 'string', primaryKey: true } }));
|
||||
for (let i = 0; i < 10; i++) await engine.insert('t', [{ id: `${i}` }]);
|
||||
expect(await engine.find('t', { table: 't', limit: 3 })).toHaveLength(3);
|
||||
});
|
||||
it('a10 offset only', async () => {
|
||||
await engine.createTable(createSchema('t', { id: { type: 'string', primaryKey: true } }));
|
||||
for (let i = 0; i < 5; i++) await engine.insert('t', [{ id: `${i}` }]);
|
||||
expect(await engine.find('t', { table: 't', offset: 3 })).toHaveLength(2);
|
||||
});
|
||||
it('a11 columns projection', async () => {
|
||||
await engine.createTable(createSchema('t', { id: { type: 'string', primaryKey: true }, a: { type: 'number' }, b: { type: 'number' } }));
|
||||
await engine.insert('t', [{ id: '1', a: 1, b: 2 }]);
|
||||
expect(Object.keys((await engine.find('t', { table: 't', columns: ['a'] }))[0])).toEqual(['a']);
|
||||
});
|
||||
it('a12 boolean true', async () => {
|
||||
await engine.createTable(createSchema('t', { id: { type: 'string', primaryKey: true }, f: { type: 'boolean' } }));
|
||||
await engine.insert('t', [{ id: '1', f: true }]);
|
||||
expect((await engine.find('t', { table: 't' }))[0].f).toBe(true);
|
||||
});
|
||||
it('a13 boolean false', async () => {
|
||||
await engine.createTable(createSchema('t', { id: { type: 'string', primaryKey: true }, f: { type: 'boolean' } }));
|
||||
await engine.insert('t', [{ id: '1', f: false }]);
|
||||
expect((await engine.find('t', { table: 't' }))[0].f).toBe(false);
|
||||
});
|
||||
it('a14 date type', async () => {
|
||||
await engine.createTable(createSchema('t', { id: { type: 'string', primaryKey: true }, d: { type: 'date' } }));
|
||||
await engine.insert('t', [{ id: '1', d: '2024-06-15' }]);
|
||||
expect((await engine.find('t', { table: 't' }))[0].d).toBe('2024-06-15');
|
||||
});
|
||||
it('a15 json array', async () => {
|
||||
await engine.createTable(createSchema('t', { id: { type: 'string', primaryKey: true }, j: { type: 'json' } }));
|
||||
await engine.insert('t', [{ id: '1', j: [1, 2, 3] }]);
|
||||
expect((await engine.find('t', { table: 't' }))[0].j).toEqual([1, 2, 3]);
|
||||
});
|
||||
it('a16 long string', async () => {
|
||||
await engine.createTable(createSchema('t', { id: { type: 'string', primaryKey: true }, s: { type: 'string' } }));
|
||||
const long = 'x'.repeat(500);
|
||||
await engine.insert('t', [{ id: '1', s: long }]);
|
||||
expect((await engine.find('t', { table: 't' }))[0].s).toBe(long);
|
||||
});
|
||||
it('a17 50 rows bulk', async () => {
|
||||
await engine.createTable(createSchema('t', { id: { type: 'string', primaryKey: true } }));
|
||||
const data = []; for (let i = 0; i < 50; i++) data.push({ id: `${i}` });
|
||||
await engine.insert('t', data);
|
||||
expect(await engine.count('t')).toBe(50);
|
||||
});
|
||||
it('a18 getTableSchema null', async () => {
|
||||
expect(await engine.getTableSchema('nope')).toBeNull();
|
||||
});
|
||||
it('a19 close reopen', async () => {
|
||||
await engine.close();
|
||||
engine = new AriaEngine({ storageBackend: 'memory' });
|
||||
await engine.open('sup2', 1);
|
||||
expect(engine.isOpen()).toBe(true);
|
||||
});
|
||||
it('a20 重复 createTable', async () => {
|
||||
await engine.createTable(createSchema('t', { id: { type: 'string', primaryKey: true } }));
|
||||
await expect(engine.createTable(createSchema('t', { id: { type: 'string', primaryKey: true } }))).rejects.toThrow();
|
||||
});
|
||||
it('a21 find PK not found', async () => {
|
||||
await engine.createTable(createSchema('t', { id: { type: 'string', primaryKey: true } }));
|
||||
expect(await engine.find('t', { table: 't', where: { id: 'nope' } })).toHaveLength(0);
|
||||
});
|
||||
it('a22 update affect zero', async () => {
|
||||
await engine.createTable(createSchema('t', { id: { type: 'string', primaryKey: true }, v: { type: 'number' } }));
|
||||
await engine.insert('t', [{ id: '1', v: 1 }]);
|
||||
expect(await engine.update('t', { table: 't', where: { id: 'x' } }, { v: 9 })).toBe(0);
|
||||
});
|
||||
it('a23 delete affect zero', async () => {
|
||||
await engine.createTable(createSchema('t', { id: { type: 'string', primaryKey: true } }));
|
||||
await engine.insert('t', [{ id: '1' }]);
|
||||
expect(await engine.delete('t', { table: 't', where: { id: 'x' } })).toBe(0);
|
||||
});
|
||||
it('a24 $and with 3 conditions', async () => {
|
||||
await engine.createTable(createSchema('t', { id: { type: 'string', primaryKey: true }, a: { type: 'number' }, b: { type: 'number' }, c: { type: 'number' } }));
|
||||
await engine.insert('t', [{ id: '1', a: 1, b: 1, c: 1 }, { id: '2', a: 1, b: 1, c: 2 }, { id: '3', a: 1, b: 2, c: 1 }]);
|
||||
expect(await engine.find('t', { table: 't', where: { $and: [{ a: 1 }, { b: 1 }, { c: 1 }] } })).toHaveLength(1);
|
||||
});
|
||||
it('a25 $or with 3 conditions', async () => {
|
||||
await engine.createTable(createSchema('t', { id: { type: 'string', primaryKey: true }, v: { type: 'number' } }));
|
||||
await engine.insert('t', [{ id: '1', v: 1 }, { id: '2', v: 2 }, { id: '3', v: 3 }, { id: '4', v: 4 }]);
|
||||
expect(await engine.find('t', { table: 't', where: { $or: [{ v: 1 }, { v: 3 }, { v: 4 }] } })).toHaveLength(3);
|
||||
});
|
||||
it('a26 $like with percent', async () => {
|
||||
await engine.createTable(createSchema('t', { id: { type: 'string', primaryKey: true }, n: { type: 'string' } }));
|
||||
await engine.insert('t', [{ id: '1', n: 'hello' }, { id: '2', n: 'help' }]);
|
||||
expect(await engine.find('t', { table: 't', where: { n: { $like: 'hel%' } } })).toHaveLength(2);
|
||||
});
|
||||
it('a27 col with dot in name', async () => {
|
||||
await engine.createTable(createSchema('t', { id: { type: 'string', primaryKey: true }, 'full.name': { type: 'string' } }));
|
||||
await engine.insert('t', [{ id: '1', 'full.name': 'Test' }]);
|
||||
expect(await engine.count('t')).toBe(1);
|
||||
});
|
||||
it('a28 table with underscore', async () => {
|
||||
await engine.createTable(createSchema('my_table', { id: { type: 'string', primaryKey: true } }));
|
||||
expect(await engine.hasTable('my_table')).toBe(true);
|
||||
});
|
||||
it('a29 3-column orderBy', async () => {
|
||||
await engine.createTable(createSchema('t', { id: { type: 'string', primaryKey: true }, a: { type: 'number' }, b: { type: 'number' }, c: { type: 'number' } }));
|
||||
await engine.insert('t', [{ id: '1', a: 1, b: 2, c: 3 }, { id: '2', a: 1, b: 1, c: 3 }, { id: '3', a: 2, b: 1, c: 1 }]);
|
||||
const r = await engine.find('t', { table: 't', orderBy: [{ column: 'a', direction: 'asc' }, { column: 'b', direction: 'asc' }, { column: 'c', direction: 'asc' }] });
|
||||
expect(r[0].id).toBe('2');
|
||||
});
|
||||
it('a30 savepoint create and release', async () => {
|
||||
await engine.beginTransaction();
|
||||
await (engine as any).savepoint('sp1');
|
||||
await (engine as any).releaseSavepoint('sp1');
|
||||
await engine.rollbackTransaction();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,198 @@
|
||||
/**
|
||||
* AriaEngine 并发 + Schema + 事务 + 性能 最终测试
|
||||
* 全部 MemoryBackend,零卡死
|
||||
*/
|
||||
import { AriaEngine } from '../../src/engine/aria/index';
|
||||
import { createSchema } from '../../src/table/schema';
|
||||
import { MemoryBackend } from '../../src/engine/aria/store/backend';
|
||||
import { checkFieldType } from '../../src/table/schema';
|
||||
|
||||
describe('AriaEngine — 最终扩展测试', () => {
|
||||
let engine: AriaEngine;
|
||||
|
||||
beforeEach(async () => {
|
||||
engine = new AriaEngine({ storageBackend: 'memory' });
|
||||
await engine.open('final-test', 1);
|
||||
});
|
||||
afterEach(async () => { await engine.close(); });
|
||||
|
||||
describe('多表操作', () => {
|
||||
it('创建多表各自CRUD互不干扰', async () => {
|
||||
await engine.createTable(createSchema('t1', { id: { type: 'string', primaryKey: true }, v: { type: 'number' } }));
|
||||
await engine.createTable(createSchema('t2', { id: { type: 'string', primaryKey: true }, v: { type: 'number' } }));
|
||||
await engine.insert('t1', [{ id: 'a', v: 1 }]);
|
||||
await engine.insert('t2', [{ id: 'b', v: 2 }]);
|
||||
expect(await engine.count('t1')).toBe(1);
|
||||
expect(await engine.count('t2')).toBe(1);
|
||||
});
|
||||
|
||||
it('删除表后同名重建', async () => {
|
||||
await engine.createTable(createSchema('t', { id: { type: 'string', primaryKey: true }, v: { type: 'number' } }));
|
||||
await engine.dropTable('t');
|
||||
await engine.createTable(createSchema('t', { id: { type: 'string', primaryKey: true }, x: { type: 'string' } }));
|
||||
const s = await engine.getTableSchema('t');
|
||||
expect(s!.columns.x).toBeDefined();
|
||||
expect(s!.columns.v).toBeUndefined();
|
||||
});
|
||||
|
||||
it('getTableNames 返回所有表', async () => {
|
||||
await engine.createTable(createSchema('a', { id: { type: 'string', primaryKey: true } }));
|
||||
await engine.createTable(createSchema('b', { id: { type: 'string', primaryKey: true } }));
|
||||
const names = await engine.getTableNames();
|
||||
expect(names.sort()).toEqual(['a', 'b']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('类型边界', () => {
|
||||
it('boolean false 存储查询', async () => {
|
||||
await engine.createTable(createSchema('flags', { id: { type: 'string', primaryKey: true }, active: { type: 'boolean' } }));
|
||||
await engine.insert('flags', [{ id: '1', active: false }]);
|
||||
const rows = await engine.find('flags', { table: 'flags', where: { active: false } });
|
||||
expect(rows).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('null JSON 字段', async () => {
|
||||
await engine.createTable(createSchema('docs', { id: { type: 'string', primaryKey: true }, meta: { type: 'json' } }));
|
||||
await engine.insert('docs', [{ id: '1', meta: null as any }]);
|
||||
expect(await engine.count('docs')).toBe(1);
|
||||
});
|
||||
|
||||
it('负数存储查询', async () => {
|
||||
await engine.createTable(createSchema('vals', { id: { type: 'string', primaryKey: true }, n: { type: 'number' } }));
|
||||
await engine.insert('vals', [{ id: '1', n: -42 }]);
|
||||
const rows = await engine.find('vals', { table: 'vals' });
|
||||
expect(rows[0].n).toBe(-42);
|
||||
});
|
||||
|
||||
it('零值存储查询', async () => {
|
||||
await engine.createTable(createSchema('vals2', { id: { type: 'string', primaryKey: true }, n: { type: 'number', default: 0 } }));
|
||||
await engine.insert('vals2', [{ id: '1', n: 0 }]);
|
||||
const rows = await engine.find('vals2', { table: 'vals2' });
|
||||
expect(rows[0].n).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('查询边界', () => {
|
||||
beforeEach(async () => {
|
||||
await engine.createTable(createSchema('items', {
|
||||
id: { type: 'string', primaryKey: true }, price: { type: 'number' }, qty: { type: 'number' },
|
||||
}));
|
||||
for (let i = 0; i < 20; i++) {
|
||||
await engine.insert('items', [{ id: `${i}`, price: i * 10, qty: i }]);
|
||||
}
|
||||
});
|
||||
|
||||
it('count with $or where', async () => {
|
||||
const c = await engine.count('items', { table: 'items', where: { $or: [{ price: 0 }, { price: 190 }] } });
|
||||
expect(c).toBe(2);
|
||||
});
|
||||
|
||||
it('count with $gt where', async () => {
|
||||
const c = await engine.count('items', { table: 'items', where: { price: { $gt: 100 } } });
|
||||
expect(c).toBe(9); // 110~190 = 9 items
|
||||
});
|
||||
|
||||
it('find with $and and $or', async () => {
|
||||
const rows = await engine.find('items', { table: 'items', where: { $and: [{ price: { $gt: 50 } }, { $or: [{ qty: 6 }, { qty: 10 }] }] } });
|
||||
expect(rows).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('单列排序 + limit', async () => {
|
||||
const rows = await engine.find('items', { table: 'items', orderBy: [{ column: 'price', direction: 'desc' }], limit: 5 });
|
||||
expect(rows[0].price).toBe(190);
|
||||
});
|
||||
|
||||
it('无匹配 update 返回 0', async () => {
|
||||
const c = await engine.update('items', { table: 'items', where: { id: 'nonexistent' } }, { price: 999 });
|
||||
expect(c).toBe(0);
|
||||
});
|
||||
|
||||
it('无匹配 delete 返回 0', async () => {
|
||||
const c = await engine.delete('items', { table: 'items', where: { id: 'nonexistent' } });
|
||||
expect(c).toBe(0);
|
||||
});
|
||||
|
||||
it('全表 update', async () => {
|
||||
const c = await engine.update('items', { table: 'items' }, { qty: 100 });
|
||||
expect(c).toBe(20);
|
||||
});
|
||||
|
||||
it('全表 delete', async () => {
|
||||
const c = await engine.delete('items', { table: 'items' });
|
||||
expect(c).toBe(20);
|
||||
expect(await engine.count('items')).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('MemoryBackend 直接测试', () => {
|
||||
it('read 不存在的 key 返回 null', async () => {
|
||||
const be = new MemoryBackend();
|
||||
await be.open('test');
|
||||
expect(await be.read('nokey')).toBeNull();
|
||||
await be.close();
|
||||
});
|
||||
|
||||
it('write+read 往返', async () => {
|
||||
const be = new MemoryBackend();
|
||||
await be.open('test2');
|
||||
const data = new TextEncoder().encode('hello').buffer;
|
||||
await be.write('k', data);
|
||||
const read = await be.read('k');
|
||||
expect(new TextDecoder().decode(read!)).toBe('hello');
|
||||
await be.close();
|
||||
});
|
||||
|
||||
it('exists 检测', async () => {
|
||||
const be = new MemoryBackend();
|
||||
await be.open('test3');
|
||||
expect(await be.exists('k')).toBe(false);
|
||||
await be.write('k', new ArrayBuffer(4));
|
||||
expect(await be.exists('k')).toBe(true);
|
||||
await be.close();
|
||||
});
|
||||
|
||||
it('delete 删除', async () => {
|
||||
const be = new MemoryBackend();
|
||||
await be.open('test4');
|
||||
await be.write('k', new ArrayBuffer(4));
|
||||
await be.delete('k');
|
||||
expect(await be.exists('k')).toBe(false);
|
||||
await be.close();
|
||||
});
|
||||
|
||||
it('listKeys 列出所有 key', async () => {
|
||||
const be = new MemoryBackend();
|
||||
await be.open('test5');
|
||||
await be.write('a', new ArrayBuffer(1));
|
||||
await be.write('b', new ArrayBuffer(1));
|
||||
const keys = await be.listKeys();
|
||||
expect(keys.sort()).toEqual(['a', 'b']);
|
||||
await be.close();
|
||||
});
|
||||
|
||||
it('clear 清空', async () => {
|
||||
const be = new MemoryBackend();
|
||||
await be.open('test6');
|
||||
await be.write('a', new ArrayBuffer(1));
|
||||
await be.clear();
|
||||
expect(await be.listKeys()).toHaveLength(0);
|
||||
await be.close();
|
||||
});
|
||||
});
|
||||
|
||||
describe('checkFieldType 约束全覆盖', () => {
|
||||
it('maxLength=0 拒绝任何非空字符串', () => {
|
||||
expect(() => checkFieldType('t','c','string','x', { type:'string',maxLength:0 })).toThrow('exceeds max length');
|
||||
});
|
||||
it('min=max 限定精确值', () => {
|
||||
expect(() => checkFieldType('t','c','number',5, { type:'number',min:5,max:5 })).not.toThrow();
|
||||
expect(() => checkFieldType('t','c','number',4, { type:'number',min:5,max:5 })).toThrow('below minimum');
|
||||
});
|
||||
it('无 colDef 时不校验约束', () => {
|
||||
expect(() => checkFieldType('t','c','string','anylength')).not.toThrow();
|
||||
});
|
||||
it('boolean 不受 min/max 约束', () => {
|
||||
expect(() => checkFieldType('t','c','boolean',true, { type:'boolean',min:0,max:1 } as any)).not.toThrow();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,105 @@
|
||||
/**
|
||||
* AriaEngine 二级索引查询测试
|
||||
* 全部 MemoryBackend,纯异步无定时器,不会卡死
|
||||
*/
|
||||
import { AriaEngine } from '../../src/engine/aria/index';
|
||||
import { createSchema } from '../../src/table/schema';
|
||||
|
||||
describe('AriaEngine — 二级索引查询', () => {
|
||||
let engine: AriaEngine;
|
||||
|
||||
beforeEach(async () => {
|
||||
engine = new AriaEngine({ storageBackend: 'memory' });
|
||||
await engine.open('idx-test', 1);
|
||||
await engine.createTable(createSchema('users', {
|
||||
id: { type: 'string', primaryKey: true },
|
||||
name: { type: 'string', index: true },
|
||||
age: { type: 'number', index: true },
|
||||
email: { type: 'string', unique: true },
|
||||
}));
|
||||
await engine.insert('users', [
|
||||
{ id: '1', name: 'Alice', age: 30, email: 'a@t.com' },
|
||||
{ id: '2', name: 'Bob', age: 25, email: 'b@t.com' },
|
||||
{ id: '3', name: 'Charlie', age: 35, email: 'c@t.com' },
|
||||
{ id: '4', name: 'Alice', age: 30, email: 'a2@t.com' },
|
||||
]);
|
||||
});
|
||||
|
||||
afterEach(async () => { await engine.close(); });
|
||||
|
||||
it('索引 $eq 查询 — name 列', async () => {
|
||||
const rows = await engine.find('users', { table: 'users', where: { name: 'Alice' } });
|
||||
expect(rows).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('索引 $eq 查询 — age 列', async () => {
|
||||
const rows = await engine.find('users', { table: 'users', where: { age: 30 } });
|
||||
expect(rows).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('索引 $in 查询', async () => {
|
||||
const rows = await engine.find('users', { table: 'users', where: { age: { $in: [25, 35] } } });
|
||||
expect(rows).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('索引 $gt 查询', async () => {
|
||||
const rows = await engine.find('users', { table: 'users', where: { age: { $gt: 28 } } });
|
||||
expect(rows).toHaveLength(3);
|
||||
});
|
||||
|
||||
it('索引 $lt 查询', async () => {
|
||||
const rows = await engine.find('users', { table: 'users', where: { age: { $lt: 30 } } });
|
||||
expect(rows).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('索引 $gte + $lte 范围', async () => {
|
||||
const rows = await engine.find('users', { table: 'users', where: { age: { $gte: 25, $lte: 30 } } });
|
||||
expect(rows).toHaveLength(3);
|
||||
});
|
||||
|
||||
it('索引不存在的值返回空', async () => {
|
||||
const rows = await engine.find('users', { table: 'users', where: { name: 'Nobody' } });
|
||||
expect(rows).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('唯一索引 $eq 查询', async () => {
|
||||
const rows = await engine.find('users', { table: 'users', where: { email: 'a@t.com' } });
|
||||
expect(rows).toHaveLength(1);
|
||||
expect(rows[0].id).toBe('1');
|
||||
});
|
||||
|
||||
it('PK 等值优先走主索引', async () => {
|
||||
const rows = await engine.find('users', { table: 'users', where: { id: '2' } });
|
||||
expect(rows).toHaveLength(1);
|
||||
expect(rows[0].name).toBe('Bob');
|
||||
});
|
||||
|
||||
it('无索引列回退全表扫描', async () => {
|
||||
// email 是唯一索引但查询用了 $like(不支持索引)
|
||||
const rows = await engine.find('users', { table: 'users', where: { email: { $like: 'a%' } } });
|
||||
expect(rows).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('插入后索引立即可查', async () => {
|
||||
await engine.insert('users', [{ id: '5', name: 'Diana', age: 28, email: 'd@t.com' }]);
|
||||
const rows = await engine.find('users', { table: 'users', where: { name: 'Diana' } });
|
||||
expect(rows).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('删除后索引不再返回', async () => {
|
||||
await engine.delete('users', { table: 'users', where: { id: '1' } });
|
||||
const rows = await engine.find('users', { table: 'users', where: { name: 'Alice' } });
|
||||
expect(rows).toHaveLength(1);
|
||||
expect(rows[0].id).toBe('4');
|
||||
});
|
||||
|
||||
it('更新后新旧索引均正确', async () => {
|
||||
await engine.update('users', { table: 'users', where: { id: '1' } }, { name: 'Alicia', age: 31 });
|
||||
const old = await engine.find('users', { table: 'users', where: { name: 'Alice' } });
|
||||
expect(old).toHaveLength(1);
|
||||
const updated = await engine.find('users', { table: 'users', where: { name: 'Alicia' } });
|
||||
expect(updated).toHaveLength(1);
|
||||
const byAge = await engine.find('users', { table: 'users', where: { age: 31 } });
|
||||
expect(byAge).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,108 @@
|
||||
/**
|
||||
* AriaEngine ANALYZE / VACUUM / REINDEX + EXPLAIN 测试
|
||||
* 全部 MemoryBackend,零卡死
|
||||
*/
|
||||
import { AriaEngine } from '../../src/engine/aria/index';
|
||||
import { createSchema } from '../../src/table/schema';
|
||||
import { QueryExecutor } from '../../src/query/executor';
|
||||
import { parse } from '../../src/sql/parser';
|
||||
|
||||
describe('AriaEngine — ANALYZE/VACUUM/REINDEX + EXPLAIN', () => {
|
||||
let engine: AriaEngine;
|
||||
|
||||
beforeEach(async () => {
|
||||
engine = new AriaEngine({ storageBackend: 'memory' });
|
||||
await engine.open('maint-test', 1);
|
||||
await engine.createTable(createSchema('users', {
|
||||
id: { type: 'string', primaryKey: true },
|
||||
name: { type: 'string', index: true },
|
||||
age: { type: 'number', default: 0 },
|
||||
}));
|
||||
await engine.insert('users', [
|
||||
{ id: '1', name: 'Alice', age: 30 },
|
||||
{ id: '2', name: 'Bob', age: 25 },
|
||||
{ id: '3', name: 'Charlie', age: 35 },
|
||||
]);
|
||||
});
|
||||
|
||||
afterEach(async () => { await engine.close(); });
|
||||
|
||||
// ---- ANALYZE ----
|
||||
it('ANALYZE 返回表统计信息', async () => {
|
||||
const stats = await (engine as any).analyzeTable('users');
|
||||
expect(stats.table).toBe('users');
|
||||
expect(stats.rowCount).toBe(3);
|
||||
expect(stats.avgRowSize).toBeGreaterThan(0);
|
||||
expect(stats.indexDepth).toBeGreaterThanOrEqual(0);
|
||||
expect(stats.sstableCount).toBeGreaterThanOrEqual(0);
|
||||
});
|
||||
|
||||
it('ANALYZE 包含列基数统计', async () => {
|
||||
const stats = await (engine as any).analyzeTable('users');
|
||||
expect(stats.columnStats).toBeDefined();
|
||||
expect(stats.columnStats.name.distinctValues).toBe(3);
|
||||
expect(stats.columnStats.age.distinctValues).toBe(3);
|
||||
});
|
||||
|
||||
it('ANALYZE 空表返回 rowCount=0', async () => {
|
||||
await engine.createTable(createSchema('empty', { id: { type: 'string', primaryKey: true } }));
|
||||
const stats = await (engine as any).analyzeTable('empty');
|
||||
expect(stats.rowCount).toBe(0);
|
||||
});
|
||||
|
||||
// ---- REINDEX ----
|
||||
it('REINDEX 重建索引后查询正常', async () => {
|
||||
const count = await (engine as any).reindexTable('users');
|
||||
expect(count).toBeGreaterThanOrEqual(1);
|
||||
const rows = await engine.find('users', { table: 'users', where: { name: 'Alice' } });
|
||||
expect(rows).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('REINDEX 不存在的表抛出错误', async () => {
|
||||
await expect((engine as any).reindexTable('ghost')).rejects.toThrow('does not exist');
|
||||
});
|
||||
|
||||
// ---- VACUUM ----
|
||||
it('VACUUM 返回压缩统计', async () => {
|
||||
const result = await (engine as any).vacuum();
|
||||
expect(result).toHaveProperty('compactedLevels');
|
||||
expect(result).toHaveProperty('gcVersions');
|
||||
});
|
||||
|
||||
// ---- EXPLAIN ----
|
||||
it('EXPLAIN SELECT 输出查询计划', async () => {
|
||||
const executor = new QueryExecutor(engine);
|
||||
const selectStmt = parse('SELECT * FROM users WHERE id = \'1\'');
|
||||
const explainStmt: any = { type: 'EXPLAIN', query: selectStmt };
|
||||
const plan = await executor.execute(explainStmt);
|
||||
expect(plan.type).toBe('SELECT');
|
||||
expect(plan.table).toBe('users');
|
||||
expect(plan.usingIndex).toBeDefined();
|
||||
expect(plan.actualTimeMs).toBeGreaterThanOrEqual(0);
|
||||
});
|
||||
|
||||
// ---- 查询优化器 ----
|
||||
it('estimateQueryCost PK 等值 → pk_lookup', () => {
|
||||
const cost = (engine as any).estimateQueryCost('users', { table: 'users', where: { id: '1' } });
|
||||
expect(cost.strategy).toBe('pk_lookup');
|
||||
expect(cost.estimatedRows).toBe(1);
|
||||
});
|
||||
|
||||
it('estimateQueryCost 索引等值 → index_eq', () => {
|
||||
const cost = (engine as any).estimateQueryCost('users', { table: 'users', where: { name: 'Alice' } });
|
||||
expect(cost.strategy).toBe('index_eq:name');
|
||||
});
|
||||
|
||||
it('estimateQueryCost 无索引 → full_scan', () => {
|
||||
const cost = (engine as any).estimateQueryCost('users', { table: 'users', where: { age: { $gt: 20 } } });
|
||||
expect(cost.strategy).toMatch(/index_range|full_scan/);
|
||||
});
|
||||
|
||||
// ---- 在线备份 ----
|
||||
it('backup 导出全库一致性快照', async () => {
|
||||
const backup = await (engine as any).backup();
|
||||
expect(backup.users).toHaveLength(3);
|
||||
expect(backup.users[0]).toHaveProperty('id');
|
||||
expect(backup.users[0]).toHaveProperty('name');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,117 @@
|
||||
/**
|
||||
* AriaEngine MVCC 事务隔离 + Savepoint 测试
|
||||
* 全部 MemoryBackend,零卡死
|
||||
*/
|
||||
import { AriaEngine } from '../../src/engine/aria/index';
|
||||
import { createSchema } from '../../src/table/schema';
|
||||
|
||||
describe('AriaEngine — MVCC 事务 + Savepoint', () => {
|
||||
let engine: AriaEngine;
|
||||
|
||||
beforeEach(async () => {
|
||||
engine = new AriaEngine({ storageBackend: 'memory' });
|
||||
await engine.open('mvcc-test', 1);
|
||||
await engine.createTable(createSchema('users', {
|
||||
id: { type: 'string', primaryKey: true },
|
||||
name: { type: 'string', required: true },
|
||||
balance: { type: 'number', default: 0 },
|
||||
}));
|
||||
});
|
||||
|
||||
afterEach(async () => { await engine.close(); });
|
||||
|
||||
// ---- MVCC 事务 ----
|
||||
it('beginTransaction 分配唯一事务ID', () => {
|
||||
const id1 = (engine as any).mvcc.beginTransaction();
|
||||
const id2 = (engine as any).mvcc.beginTransaction();
|
||||
expect(id1).not.toBe(id2);
|
||||
});
|
||||
|
||||
it('commit 后事务标记为非活跃', () => {
|
||||
const txnId = (engine as any).mvcc.beginTransaction();
|
||||
(engine as any).mvcc.commitTransaction(txnId);
|
||||
expect((engine as any).mvcc.isActive(txnId)).toBe(false);
|
||||
});
|
||||
|
||||
it('rollback 后事务标记为非活跃', () => {
|
||||
const txnId = (engine as any).mvcc.beginTransaction();
|
||||
(engine as any).mvcc.rollbackTransaction(txnId);
|
||||
expect((engine as any).mvcc.isActive(txnId)).toBe(false);
|
||||
});
|
||||
|
||||
it('事务中写入版本可读', () => {
|
||||
const txnId = (engine as any).mvcc.beginTransaction();
|
||||
(engine as any).mvcc.writeVersion('users', '1', { name: 'Alice', balance: 100 }, txnId);
|
||||
const val = (engine as any).mvcc.readVersion('users', '1', txnId);
|
||||
expect(val).not.toBeNull();
|
||||
expect(val!.name).toBe('Alice');
|
||||
});
|
||||
|
||||
it('未提交版本对其他事务不可见', () => {
|
||||
const txn1 = (engine as any).mvcc.beginTransaction();
|
||||
(engine as any).mvcc.writeVersion('users', '1', { name: 'Alice' }, txn1);
|
||||
const txn2 = (engine as any).mvcc.beginTransaction();
|
||||
expect((engine as any).mvcc.readVersion('users', '1', txn2)).toBeNull();
|
||||
});
|
||||
|
||||
it('引擎层事务 commit 后数据可见', async () => {
|
||||
await engine.beginTransaction();
|
||||
await engine.insert('users', [{ id: '1', name: 'Alice', balance: 100 }]);
|
||||
await engine.commitTransaction();
|
||||
expect(await engine.count('users')).toBe(1);
|
||||
});
|
||||
|
||||
it('引擎层事务 rollback 后数据消失', async () => {
|
||||
await engine.insert('users', [{ id: '1', name: 'Alice' }]);
|
||||
await engine.beginTransaction();
|
||||
await engine.insert('users', [{ id: '2', name: 'Bob' }]);
|
||||
await engine.rollbackTransaction();
|
||||
expect(await engine.count('users')).toBe(1);
|
||||
});
|
||||
|
||||
it('GC 清理过旧版本', () => {
|
||||
for (let i = 0; i < 200; i++) {
|
||||
const txnId = (engine as any).mvcc.beginTransaction();
|
||||
(engine as any).mvcc.writeVersion('users', '1', { ver: i }, txnId);
|
||||
(engine as any).mvcc.commitTransaction(txnId);
|
||||
}
|
||||
(engine as any).mvcc.gc(50);
|
||||
const txnId = (engine as any).mvcc.beginTransaction();
|
||||
expect((engine as any).mvcc.readVersion('users', '1', txnId)).not.toBeNull();
|
||||
(engine as any).mvcc.commitTransaction(txnId);
|
||||
});
|
||||
|
||||
// ---- Savepoint ----
|
||||
it('savepoint 创建成功', async () => {
|
||||
await engine.beginTransaction();
|
||||
await (engine as any).savepoint('sp1');
|
||||
expect((engine as any).savepoints.has('sp1')).toBe(true);
|
||||
await engine.rollbackTransaction();
|
||||
});
|
||||
|
||||
it('rollbackToSavepoint 恢复快照', async () => {
|
||||
await engine.insert('users', [{ id: '1', name: 'Alice' }]);
|
||||
await engine.beginTransaction();
|
||||
await engine.insert('users', [{ id: '2', name: 'Bob' }]);
|
||||
await (engine as any).savepoint('sp1');
|
||||
await engine.insert('users', [{ id: '3', name: 'Charlie' }]);
|
||||
await (engine as any).rollbackToSavepoint('sp1');
|
||||
await engine.commitTransaction();
|
||||
expect(await engine.count('users')).toBe(2);
|
||||
});
|
||||
|
||||
it('releaseSavepoint 释放后不可回滚', async () => {
|
||||
await engine.beginTransaction();
|
||||
await (engine as any).savepoint('sp1');
|
||||
await (engine as any).releaseSavepoint('sp1');
|
||||
expect((engine as any).savepoints.has('sp1')).toBe(false);
|
||||
await engine.rollbackTransaction();
|
||||
});
|
||||
|
||||
it('重复 savepoint 名称抛出错误', async () => {
|
||||
await engine.beginTransaction();
|
||||
await (engine as any).savepoint('sp1');
|
||||
await expect((engine as any).savepoint('sp1')).rejects.toThrow('already exists');
|
||||
await engine.rollbackTransaction();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user