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
Reference in New Issue
Block a user