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:
thzxx
2026-07-26 15:00:01 +08:00
commit e2a590c5b1
60 changed files with 16359 additions and 0 deletions
+7
View File
@@ -0,0 +1,7 @@
/**
* metona-sqlark Table — 表管理
* @module table
*/
export { createSchema, validateColumns, validateRow, getPrimaryKey } from './schema';
export { Table } from './table';
+172
View File
@@ -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,
};
}
+83
View File
@@ -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);
}
}