/** * metona-sqlark Schema — 表结构定义与校验 * @module table/schema */ import type { TableSchema, ColumnDef, FieldType } from '../constants'; import { FIELD_TYPES, DatabaseError } from '../constants'; import { compileValidator } from './validation'; // v0.8.0(B-1):`checkFieldType` 的实现已迁到 `validation.ts`(唯一校验定义), // 这里重新导出以保持公开 API 不变。 // // 为什么必须搬走:'schema.ts ↔ validation.ts' 之前是**循环依赖** //(schema 需要 compileValidator;validation 需要 checkFieldType), // rollup 构建时明确告警 'Circular dependency'。循环依赖在 ESM 下的 // 求值顺序不稳定(谁先被 import 谁就先初始化),是难查的运行时陷阱; // 依赖方向必须单向:validation(约束实现)← schema(DDL 工具)。 export { checkFieldType } from './validation'; // --------------------------------------------------------------------------- // Schema 工具 // --------------------------------------------------------------------------- /** 从列定义创建 TableSchema */ export function createSchema(name: string, columns: Record): TableSchema { validateColumns(columns); return { name, columns }; } /** 校验列定义 */ export function validateColumns(columns: Record): 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)) { // v0.7.1: '__proto__' 作为列名会触发对象原型 setter(列静默丢失); // 显式拒绝避免原型污染类攻击面 if (colName === '__proto__') { throw new DatabaseError('Column name "__proto__" is not allowed', 'SCHEMA_ERROR'); } // 类型校验 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'); } // v0.7.0: 复合主键(多列 primaryKey)当前不支持 —— 所有引擎的存储布局与 // 外键引用均为单主键假设(此前静默取第一个主键,其余标记被忽略 → 语义陷阱)。 // 显式拒绝避免用户误用;复合主键列入 v0.8 路线图。 if (primaryKeyCount > 1) { throw new DatabaseError( `Composite primary keys are not supported yet: table has ${primaryKeyCount} primary key columns. ` + 'Use a single primary key column (or a unique column combination) instead.', '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]; } /** * v0.7.2: 更新载荷清洗 —— undefined 值视为"不更新该列"(保留旧值)。 * 此前 `update({ col: undefined })` 会把 undefined 写入行(覆盖旧值、列键丢失)。 * null 保留(显式置空语义)。 */ export function stripUndefinedUpdates(updates: Record): Record { const clean: Record = {}; for (const [key, value] of Object.entries(updates)) { if (value !== undefined) clean[key] = value; } return clean; } /** * 校验行数据(INSERT 语义:`default` 生效、缺列合法)。 * * v0.8.0(B-1):实现已迁移到 `table/validation.ts#compileValidator`。 * 这里保留同名导出是因为它是**公开 API**(`table/index.ts` 与 `src/index.ts` 转发), * 且历史调用方(含 tests)依赖它 —— 但实现只有一份:委托过去。 * * 为什么必须收敛:修复前本项目有**三份**行校验(memory / aria / 本文件), * 覆盖面不同(memory 那份缺 maxLength / min / max)。于是"约束是否生效"取决于 * 引擎选择(缺陷 A12),而"未知列"三份都静默丢弃(A17)。 * 现在三处都是同一个 `compileValidator`。 */ export function validateRow(schema: TableSchema, row: Record): Record { return compileValidator(schema).validateRow(row); } /** 将 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; references?: string; onDelete?: 'CASCADE' | 'SET NULL' | 'RESTRICT'; onUpdate?: 'CASCADE' | 'SET NULL' | 'RESTRICT'; }): 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, references: astCol.references, onDelete: astCol.onDelete, onUpdate: astCol.onUpdate, }; }