feat(A37): 未限定列可作比较操作数 + WHERE/JOIN ON 列引用存在性校验

两个此前互相纠缠的缺陷(PLAN §5 #35/#37、§3 根因 7 的最后一块)。

① 未限定列不能作比较操作数(语法层)
   parser 只把 `a.b` 形态当列引用,裸标识符一律走字面量解析,于是:
     WHERE x = y  → PARSE_ERROR: Expected value, got "y"
     ON k = k     → 同上
   列对列比较被迫写成 `WHERE t.x = t.y` —— 而多表 JOIN 里未限定列恰恰是最
   自然的写法(`ON user_id = id`)。
   修法:操作数位置上的 IDENTIFIER **必然是列引用**(字面量各有自己的 token
   类型:NUMBER/STRING/TRUE/FALSE/NULL),这一条不需要猜测。

   * 排查记录:这里试错了两次。起初用 peekToken 判"下一个是否运算符",
     实测仍报错 —— 因为进入该分支时运算符**已被 parseComparisonOp 消费**,
     `peek` 是 EOF/AND 而非运算符。最终按"位置"判定,不再依赖 lookahead。

② `$col` 引用到不存在的列 → 静默空集(语义层)
   投影侧早有列存在性校验,WHERE 侧一直没有:`$col` 取不到值 → UNRESOLVED →
   比较判 UNKNOWN → **所有行被过滤且不报错**。实测修复前:
     SELECT id FROM t WHERE id = oops          → []
     SELECT id FROM t WHERE id = t.oops        → []
     SELECT id FROM t WHERE x = nope           → []
     SELECT ... FROM l JOIN r ON l.k = r.nope  → [](连接不上任何行)
   用户看到的是"没有数据",与"列名拼错"完全无法区分。
   修法:新增 `validateWhereColumns`(WHERE 的键位 + `$col` 值位,含
   `$and`/`$or`/`$not` 内部)与 `validateJoinOnColumns`(ON 两侧归属判定),
   两者共用同一遍历实现。
   契约变更:
     - 拼错的列名 → `COLUMN_NOT_FOUND`(此前 PARSE_ERROR 或静默空集);
     - JOIN 的 WHERE 里裸写两表同名列 → 歧义报错(SQL 标准要求限定);
     - `ON k = k` 这类裸写法仍按"取主表列"解释(不因两表同名而拒绝,
       否则会把常见等值连接写法判为错误)。

附带修正:`schema.ts ↔ validation.ts` 的**循环依赖**(rollup 构建告警
"Circular dependency")。`checkFieldType` 的实现搬到 validation.ts(唯一校验
定义),schema.ts 重新导出以保持公开 API —— 依赖方向改为单向
(validation ← schema)。循环依赖在 ESM 下求值顺序不稳定,是难查的运行时陷阱。

验证:新增 tests/v080-column-resolution.test.ts(4 引擎 × 8 项,共 32 断言,
期望值全部逐个实测得出);tests/v080-correlated.test.ts 的"裸 x = y 为
PARSE_ERROR"用例改为断言两种写法等价。
全量 86 套件 / 1697 测试通过;e2e 14 项通过(真实 Chromium + OPFS);
typecheck(src+tests)、lint、build 零错误/零告警;dist 已重建。
This commit is contained in:
thzxx
2026-09-15 01:21:58 +08:00
parent 6a0cfebc4a
commit 567d150257
14 changed files with 12546 additions and 2251 deletions
+10 -76
View File
@@ -7,6 +7,16 @@ import type { TableSchema, ColumnDef, FieldType } from '../constants';
import { FIELD_TYPES, DatabaseError } from '../constants';
import { compileValidator } from './validation';
// v0.8.0B-1):`checkFieldType` 的实现已迁到 `validation.ts`(唯一校验定义),
// 这里重新导出以保持公开 API 不变。
//
// 为什么必须搬走:'schema.ts ↔ validation.ts' 之前是**循环依赖**
//schema 需要 compileValidatorvalidation 需要 checkFieldType),
// rollup 构建时明确告警 'Circular dependency'。循环依赖在 ESM 下的
// 求值顺序不稳定(谁先被 import 谁就先初始化),是难查的运行时陷阱;
// 依赖方向必须单向:validation(约束实现)← schemaDDL 工具)。
export { checkFieldType } from './validation';
// ---------------------------------------------------------------------------
// Schema 工具
// ---------------------------------------------------------------------------
@@ -100,82 +110,6 @@ export function validateRow(schema: TableSchema, row: Record<string, unknown>):
return compileValidator(schema).validateRow(row);
}
/** 检查字段类型(含约束校验) */
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',
);
}
if (colDef?.maxLength !== undefined && (value as string).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 as number) < colDef.min) {
throw new DatabaseError(
`Column "${colName}" in table "${tableName}" value ${value} below minimum ${colDef.min}`,
'VALIDATION_ERROR',
);
}
if (colDef?.max !== undefined && (value as number) > 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 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;