字符串字面量:类型的链表!#

发布时间:2026/7/23 10:50:41
字符串字面量:类型的链表!# 字符串字面量就比较有趣了。这里我选择在类型层面构建一条字符链表用接口 IStringNode 来描述Copyinternal interface IStringNode{static abstract int Length { get; }static abstract void Write(Span destination, int index);}有三个实现StringEnd字符串的结尾长度 0StringNull表示 null 字符串长度 -1StringNodeTChar, TNext当前一个字符 剩余部分。Copyinternal readonly struct StringEnd : IStringNode{public static int Length 0;public static void Write(Span destination, int index) { }}internal readonly struct StringNull : IStringNode{public static int Length -1;public static void Write(Span destination, int index) { }}internal readonly struct StringNodeTChar, TNext : IStringNodewhere TChar : ILiteralwhere TNext : IStringNode{public static int Length 1 TNext.Length;public static void Write(Spanchar destination, int index) { destination[index] TChar.Value; TNext.Write(destination, index 1); }}有了这样的类型链表我们就可以基于某个 IStringNode构造出真正的 ValueStringCopyinternal readonly struct StringLiteral : ILiteralwhere TString : IStringNode{public static ValueString Value Cache.Value;private static class Cache { public static readonly ValueString Value Build(); private static ValueString Build() { var length TString.Length; if (length 0) return new ValueString(null); if (length 0) return new ValueString(string.Empty); var chars new char[length]; TString.Write(chars.AsSpan(), 0); return new string(chars, 0, length); } }}StringLiteral 就是一个 ILiteral它的 Value 在类型初始化时算好并缓存下来所以只需要计算一次后续访问都是直接读静态字段非常高效。把字符串塞进类型#LiteralTypeFactory.CreateStringLiteral 负责把字符串字面量转换成这样一个类型Copypublic static Type CreateStringLiteral(string? value){if (value is null){return typeof(StringLiteral);}var type typeof(StringEnd); for (var i value.Length - 1; i 0; i--) { var charType CreateCharType(value[i]); // Char... type typeof(StringNode,).MakeGenericType(charType, type); } return typeof(StringLiteral).MakeGenericType(type);}比如我们有一个字面量 ‘Seattle’整个流程大致是解析阶段读到 ‘Seattle’生成一个 LiteralValueKind LiteralKind.StringStringValue “Seattle”编译阶段根据列的类型判断这是个字符串列于是对应的运行时类型是 ValueString。调用 CreateStringLiteral(“Seattle”)初始 type typeof(StringEnd)从右到左遍历每个字符‘e’ → 得到一个 Char… 类型4 个十六进制数位对应 Unicodetype StringNodeChar‘e’, StringEnd‘l’ 再往前type StringNodeChar‘l’, StringNodeChar‘e’, StringEnd一直重复‘t’、‘t’、‘a’、‘e’、‘S’……最终得到类似这样一个类型CopyStringNodeChar‘S’,StringNodeChar‘e’,StringNodeChar‘a’,StringNodeChar‘t’,StringNodeChar‘t’,StringNodeChar‘l’,StringNodeChar‘e’, StringEnd最后再用 StringLiteral 把它包起来CopyStringLiteralStringNodeChar‘S’,StringNodeChar‘e’,…这一整个封闭泛型类型就是字面量 ‘Seattle’ 的类型版本。而过滤器在需要值的时候只是简单地访问 TLiteral.Value再通过 TString.Length 和 TString.Write 复原出一个 ValueString(“Seattle”)其中复原通过静态类型的缓存完成借助类型系统的力量每一个独立的字面量都会产生一个单独的类型实例我们的字面量就缓存在那个类型的静态字段里从而避免了一切运行时的计算开销。null 字符串字面量#null 的处理稍微特殊一点写类似 WHERE Team ! null 这种代码时解析器会把它识别为 LiteralKind.Null对字符串列来说CreateStringLiteral(null) 会返回 typeof(StringLiteral)StringNull.Length -1于是 StringLiteral.Value 直接返回 new ValueString(null)。这样一来null 和 “” 在类型层面和运行时都可以被区分开。字面量工厂#上面这些编码最后都归到一个工厂类里统一封装Copyinternal static class LiteralTypeFactory{public static Type CreateIntLiteral(int value) { … }public static Type CreateFloatLiteral(float value) { … }public static Type CreateBoolLiteral(bool value) { … }public static Type CreateStringLiteral(string? value) { … }}SQL 编译阶段会根据两方面信息来调用它列的运行时类型int、float、bool、ValueString字面量的种类Integer、Float、Boolean、String、Null。最终的效果就是WHERE 子句里每一个字面量都会变成一个具体的 ILiteral 类型值直接嵌在类型参数里。搭好整个管道类型#到目前为止我们已经有了一棵解析出来的查询SELECT WHERE一份 schema把列名映射到具体的 IColumnTRow, TValue 实现一套机制把字面量变成 ILiteral 类型。SQL 编译器接下来要做的就是把这些东西变成一个封闭的管道类型 TPipeline它实现 IQueryNodeTRow, TRuntimeResult, TRoot一个运行时结果类型 TRuntimeResult一个对外公开的结果类型 TPublicResult。编译 SELECT#先看选择部分。SELECT *#最简单的情况就是SELECT * FROM $。这时候运行时结果类型 行类型本身TRuntimeResult TRow公共结果类型也是 TRow管道尾部就是一个 StopTRow, TRow 节点。大致逻辑如下CopyTRuntimeResult typeof(TRow);TPublicResult typeof(TRow);TPipelineTail typeof(Stop,).MakeGenericType(TRuntimeResult, typeof(TRow));SELECT col / SELECT col1, col2, …#当有明确列投影时步骤稍微多一点SELECT col根据列名解析出对应的 ColumnMetadata决定它的运行时值类型如果列类型本身不是 string运行时类型就跟它一致如果是 string运行时类型改为 ValueString构建一个 ColumnProjectionTRuntimeColumn, TRow, TRuntimeValue。SELECT col1, col2, …分别解析每一列构造一个 ValueTupleProjection返回一个 ValueTuple…里面放运行时类型同时记录一份公共 ValueTuple… 类型用声明的 CLR 类型如 string。最后无论是一列还是多列都会在 Stop 前面再加一个 Select 节点CopySelectTRow, TProjection, Stop…, TMiddle, TRuntimeResult, TRoot → Stop…这个节点内部会调用投影的静态 Project 方法再把结果转交给 Stop.Process 处理。编译 WHERE#WHERE 子句以递归方式编译成类型。布尔结构#给定一个解析后的 WhereExpression 树A AND B → AndFilterTRow, TA, TBA OR B → OrFilterTRow, TA, TBNOT A → NotFilterTRow, TA。编译器做的事情大概是对这棵树一层层往下调自己的方法CopyType BuildPredicate(WhereExpression expr){return expr switch{ComparisonExpression cmpExpr BuildComparisonPredicate(cmpExpr),AndExpression andExpr typeof(AndFilter,).MakeGenericType(typeof(TRow), BuildPredicate(andExpr.Left), BuildPredicate(andExpr.Right)),OrExpression orExpr typeof(OrFilter,).MakeGenericType(typeof(TRow), BuildPredicate(orExpr.Left), BuildPredicate(orExpr.Right)),NotExpression notExpr typeof(NotFilter,).MakeGenericType(typeof(TRow), BuildPredicate(notExpr.Expression)),_ throw …};}比较表达式#每一个叶子比较表达式比如CopyCity ‘Seattle’Salary 180000Team ! null都会变成一个具体的过滤器类型CopyType BuildComparisonPredicate(ComparisonExpression comparison){var rowType typeof(TRow);var column SchemaRegistry.ResolveColumn(comparison.ColumnIdentifier);var runtimeColumnType column.GetRuntimeColumnType(rowType); var runtimeColumnValueType column.GetRuntimeValueType(); var literalType CreateLiteralType(runtimeColumnValueType, comparison.Literal); var filterDefinition comparison.Operator switch { ComparisonOperator.Equals typeof(EqualsFilter,,,), ComparisonOperator.GreaterThan typeof(GreaterThanFilter,,,), ComparisonOperator.LessThan typeof(LessThanFilter,,,), ComparisonOperator.GreaterOrEqual typeof(GreaterOrEqualFilter,,,), ComparisonOperator.LessOrEqual typeof(LessOrEqualFilter,,,), ComparisonOperator.NotEqual typeof(NotEqualFilter,,,), _ throw … }; return filterDefinition.MakeGenericType( rowType, runtimeColumnType, literalType, runtimeColumnValueType);}以 City ‘Seattle’ 为例如果那一列是字符串列那么运行时列类型是ValueStringColumnPersonCityColumn, Person运行时值类型是ValueString字面量类型则是通过 CreateStringLiteral(“Seattle”) 得到的某个 StringLiteralSomeStringNode…。最后组合出一个过滤器类型CopyEqualsFilterPerson,ValueStringColumnPersonCityColumn, Person,StringLiteral…,ValueString到这一步我们就可以把一个 Where 节点挂到管道上了CopyWhereTRow, TPredicate, TNext, TRuntimeResult, TRoot → …把 Where 和 Select 融合起来#直接这么拼出来的管道是正确的但在性能上还能再优化一点Where 和 Select 其实可以合并成一步。TypedSql 里有一个很小的优化器会去找这样的模式WhereTRow, TPredicate, SelectTRow, TProjection, TNext, TMiddle, TResult, TRoot, TResult, TRoot一旦发现就把它替换成CopyWhereSelectTRow, TPredicate, TProjection, TNext, TMiddle, TResult, TRoot这个融合节点的实现如下Copyinternal readonly struct WhereSelectTRow, TPredicate, TProjection, TNext, TMiddle, TResult, TRoot: IQueryNodeTRow, TResult, TRootwhere TPredicate : IFilterwhere TProjection : IProjectionTRow, TMiddlewhere TNext : IQueryNodeTMiddle, TResult, TRoot{public static void Run(ReadOnlySpan rows, scoped ref QueryRuntime runtime){for (var i 0; i rows.Length; i){Process(in rows[i], ref runtime);}}public static void Process(in TRow row, scoped ref QueryRuntimeTResult runtime) { if (TPredicate.Evaluate(in row)) { var projected TProjection.Project(in row); TNext.Process(in projected, ref runtime); } }}于是像下面这种常见的查询CopySELECT Name FROM $ WHERE City ‘Seattle’最终就会是CopyWhereSelect… → Stop…也就是说一个循环里完成过滤和投影不需要再分两趟。并且我们的优化器还能识别更复杂的嵌套结构尽可能地把 Where 和 Select 融合在一起减少中间步骤提升性能。而这并不需要复杂的优化算法只需要简单地把泛型参数取出来重新带入到新的融合类型即可实现起来非常简单。结果转换#管道把所有行跑完之后最后还得把结果以某种形式“交出去”。一个查询的入口长这样