release: v0.2.4 — 701 tests, 32 suites, 零死代码, 零空壳, 全模块接入
CI / test (18.x) (push) Successful in 10m0s
CI / test (20.x) (push) Successful in 10m0s
CI / test (22.x) (push) Successful in 10m0s
CI / test (24.x) (push) Successful in 9m54s

This commit is contained in:
thzxx
2026-07-27 22:18:08 +08:00
parent d799e968ab
commit 4c26882caa
16 changed files with 1538 additions and 257 deletions
+109 -80
View File
@@ -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