feat: metona-sqlark v0.1.12 — 前端TypeScript关系型数据库
- 4种存储引擎:Memory / IndexedDB / OPFS / Hybrid - 完整SQL支持:SELECT/INSERT/UPDATE/DELETE/JOIN/GROUP BY/HAVING/DISTINCT - Query Builder链式API + TypeScript泛型支持 - 聚合函数:COUNT/SUM/AVG/MIN/MAX - 事务、插件系统(14 hooks)、发布订阅、数据迁移、导入导出 - React/Vue框架集成 - 264个测试用例,93.46%覆盖率 - 零运行时依赖
This commit is contained in:
@@ -0,0 +1,172 @@
|
||||
/**
|
||||
* metona-sqlark Schema — 表结构定义与校验
|
||||
* @module table/schema
|
||||
*/
|
||||
|
||||
import type { TableSchema, ColumnDef, FieldType } from '../constants';
|
||||
import { FIELD_TYPES, DatabaseError } from '../constants';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Schema 工具
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/** 从列定义创建 TableSchema */
|
||||
export function createSchema(name: string, columns: Record<string, ColumnDef>): TableSchema {
|
||||
validateColumns(columns);
|
||||
return { name, columns };
|
||||
}
|
||||
|
||||
/** 校验列定义 */
|
||||
export function validateColumns(columns: Record<string, ColumnDef>): void {
|
||||
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');
|
||||
}
|
||||
}
|
||||
|
||||
/** 获取主键列名 */
|
||||
export function getPrimaryKey(schema: TableSchema): string {
|
||||
for (const [name, col] of Object.entries(schema.columns)) {
|
||||
if (col.primaryKey) return name;
|
||||
}
|
||||
return Object.keys(schema.columns)[0];
|
||||
}
|
||||
|
||||
/** 校验行数据 */
|
||||
export function validateRow(schema: TableSchema, row: Record<string, unknown>): Record<string, unknown> {
|
||||
const validated: Record<string, unknown> = {};
|
||||
|
||||
for (const [colName, colDef] of Object.entries(schema.columns)) {
|
||||
let value = row[colName];
|
||||
|
||||
// 默认值
|
||||
if (value === undefined && colDef.default !== undefined) {
|
||||
value = colDef.default;
|
||||
}
|
||||
|
||||
// 必填检查
|
||||
if (colDef.required && (value === undefined || value === null)) {
|
||||
throw new DatabaseError(
|
||||
`Column "${colName}" is required in table "${schema.name}"`,
|
||||
'VALIDATION_ERROR',
|
||||
);
|
||||
}
|
||||
|
||||
// 类型检查
|
||||
if (value !== undefined && value !== null) {
|
||||
checkFieldType(schema.name, colName, colDef.type, value, colDef);
|
||||
}
|
||||
|
||||
if (value !== undefined) {
|
||||
validated[colName] = value;
|
||||
}
|
||||
}
|
||||
|
||||
return validated;
|
||||
}
|
||||
|
||||
/** 检查字段类型 */
|
||||
export function checkFieldType(
|
||||
tableName: string,
|
||||
colName: string,
|
||||
type: FieldType,
|
||||
value: unknown,
|
||||
_colDef?: ColumnDef,
|
||||
): void {
|
||||
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',
|
||||
);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'number':
|
||||
if (jsType !== 'number') {
|
||||
throw new DatabaseError(
|
||||
`Column "${colName}" in table "${tableName}" expects number, got ${jsType}`,
|
||||
'TYPE_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 as string))) {
|
||||
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 */
|
||||
export function astColumnToColumnDef(astCol: {
|
||||
name: string;
|
||||
type: string;
|
||||
primaryKey?: boolean;
|
||||
unique?: boolean;
|
||||
required?: boolean;
|
||||
default?: unknown;
|
||||
index?: boolean;
|
||||
maxLength?: number;
|
||||
min?: number;
|
||||
max?: number;
|
||||
}): ColumnDef {
|
||||
return {
|
||||
type: astCol.type as FieldType,
|
||||
primaryKey: astCol.primaryKey,
|
||||
unique: astCol.unique,
|
||||
required: astCol.required,
|
||||
default: astCol.default,
|
||||
index: astCol.index,
|
||||
maxLength: astCol.maxLength,
|
||||
min: astCol.min,
|
||||
max: astCol.max,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user