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,7 @@
|
||||
/**
|
||||
* metona-sqlark Table — 表管理
|
||||
* @module table
|
||||
*/
|
||||
|
||||
export { createSchema, validateColumns, validateRow, getPrimaryKey } from './schema';
|
||||
export { Table } from './table';
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
/**
|
||||
* metona-sqlark Table — 表操作 API
|
||||
* @module table/table
|
||||
*/
|
||||
|
||||
import type { IStorageEngine } from '../engine/interface';
|
||||
import type { TableSchema } from '../constants';
|
||||
import { DatabaseError } from '../constants';
|
||||
import { SelectQueryBuilder, UpdateQueryBuilder, DeleteQueryBuilder } from '../query/builder';
|
||||
import { QueryExecutor } from '../query/executor';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Table
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export class Table<T = Record<string, unknown>> {
|
||||
readonly name: string;
|
||||
private engine: IStorageEngine;
|
||||
private schema: TableSchema | null = null;
|
||||
private executor: QueryExecutor | undefined;
|
||||
|
||||
constructor(engine: IStorageEngine, tableName: string, executor?: QueryExecutor) {
|
||||
this.engine = engine;
|
||||
this.name = tableName;
|
||||
this.executor = executor;
|
||||
}
|
||||
|
||||
// ---- Schema ----
|
||||
|
||||
async getSchema(): Promise<TableSchema> {
|
||||
if (!this.schema) {
|
||||
const s = await this.engine.getTableSchema(this.name);
|
||||
if (!s) throw new DatabaseError(`Table "${this.name}" does not exist`, 'TABLE_NOT_FOUND');
|
||||
this.schema = s;
|
||||
}
|
||||
return this.schema;
|
||||
}
|
||||
|
||||
// ---- 插入 ----
|
||||
|
||||
async insert(row: T & Record<string, unknown>): Promise<string> {
|
||||
const pks = await this.engine.insert(this.name, [row as Record<string, unknown>]);
|
||||
return pks[0];
|
||||
}
|
||||
|
||||
async insertMany(rows: (T & Record<string, unknown>)[]): Promise<string[]> {
|
||||
return this.engine.insert(this.name, rows as Record<string, unknown>[]);
|
||||
}
|
||||
|
||||
// ---- 查询 ----
|
||||
|
||||
select(columns: string[] = ['*']): SelectQueryBuilder {
|
||||
return new SelectQueryBuilder(this.engine, this.name, columns, this.executor);
|
||||
}
|
||||
|
||||
// ---- 更新 ----
|
||||
|
||||
update(updates: Partial<T> & Record<string, unknown>): UpdateQueryBuilder {
|
||||
return new UpdateQueryBuilder(this.engine, this.name, updates);
|
||||
}
|
||||
|
||||
// ---- 删除 ----
|
||||
|
||||
delete(): DeleteQueryBuilder {
|
||||
return new DeleteQueryBuilder(this.engine, this.name);
|
||||
}
|
||||
|
||||
// ---- 聚合 ----
|
||||
|
||||
async count(where?: Record<string, unknown>): Promise<number> {
|
||||
return this.engine.count(this.name, where ? { table: this.name, where } : undefined);
|
||||
}
|
||||
|
||||
// ---- 管理 ----
|
||||
|
||||
async clear(): Promise<void> {
|
||||
return this.engine.clear(this.name);
|
||||
}
|
||||
|
||||
async drop(): Promise<void> {
|
||||
return this.engine.dropTable(this.name);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user