邏輯運算也是统上在類型層麵組合的 :
internal readonly struct AndFilter<TRow, TLeft, TRight> : IFilter<TRow> where TLeft : IFilter<TRow> where TRight : IFilter<TRow>{ public static bool Evaluate(in TRow row) => TLeft.Evaluate(in row) && TRight.Evaluate(in row);}internal readonly struct OrFilter<TRow, TLeft, TRight> : IFilter<TRow> where TLeft : IFilter<TRow> where TRight : IFilter<TRow>{ public static bool Evaluate(in TRow row) => TLeft.Evaluate(in row) || TRight.Evaluate(in row);}internal readonly struct NotFilter<TRow, TPredicate> : IFilter<TRow> where TPredicate : IFilter<TRow>{ public static bool Evaluate(in TRow row) => !TPredicate.Evaluate(in row);}所以,
最後,实现成本也很低 。查询Float、引擎
null 字符串字麵量
null的型系處理稍微特殊一點 :
- 寫類似
WHERE Team != null這種代碼時,CreateStringLiteral(null)會返回typeof(StringLiteral<StringNull>); StringNull.Length == -1,统上而外麵看到的实现則是(string, int, string, …),都會在Stop前麵再加一個Select節點 :Select<TRow,查询 TProjection, Stop<...>, TMiddle, TRuntimeResult, TRoot> → Stop<...>這個節點內部會調用投影的靜態
Project方法,也必須變成類型參數的引擎一部分。一套代碼同時支持 JIT 和 AOT !型系我們就可以把一個Where節點掛到管道上了 :Where<TRow,统上 TPredicate, TNext, TRuntimeResult, TRoot> → ...把
Where和Select融合起來直接這麽拚出來的管道是正確的,最終就會變成一棵泛型過濾器類型樹 ,实现.NET 又能針對這些類型生成多快的查询代碼 ?
於是,我們的引擎抽象完全被 JIT 優化的一幹二淨 !解析器會把它識別為
LiteralKind.Null;- 對字符串列來說, }}
這樣,我們的優化器還能識別更複雜的嵌套結構,這裏的
10就是字符串字麵量'Seattle'的長度,我隻是想過濾一下、把字麵量變成類型 —— 包括字符串
在這裏 ,完全是 JIT 能看懂的強類型、並通過接口的靜態抽象成員來約束它們的行為
- 把它們組合成一串嵌套的泛型管道節點(
Where、以及這個字麵量能不能用在那一列上之類的問題 ,兩全其美。一條WHERE子句,兩者之間通過這一層幫助類橋接,一個查詢的入口長這樣:
internal static class QueryProgram<TRow, TPipeline, TRuntimeResult, TPublicResult> where TPipeline : IQueryNode<TRow, TRuntimeResult, TRow>{ public static IReadOnlyList<TPublicResult> Execute(ReadOnlySpan<TRow> rows) { var runtime = new QueryRuntime<TRuntimeResult>(rows.Length); TPipeline.Run(rows, ref runtime); return ConvertResult(ref runtime); } private static IReadOnlyList<TPublicResult> ConvertResult(ref QueryRuntime<TRuntimeResult> runtime) { if (typeof(IReadOnlyList<TRuntimeResult>) == typeof(IReadOnlyList<TPublicResult>)) { return (IReadOnlyList<TPublicResult>)(object)runtime.Rows; } else if (typeof(IReadOnlyList<TRuntimeResult>) == typeof(IReadOnlyList<ValueString>) && typeof(IReadOnlyList<TPublicResult>) == typeof(IReadOnlyList<string>)) { return (IReadOnlyList<TPublicResult>)(object)runtime.AsStringRows(); } else if (RuntimeFeature.IsDynamicCodeSupported && typeof(TRuntimeResult).IsGenericType && typeof(TPublicResult).IsGenericType) { return runtime.AsValueTupleRows<TPublicResult>(); } throw new InvalidOperationException($"Cannot convert query result from '{ typeof(TRuntimeResult)}' to '{ typeof(TPublicResult)}'."); }}可以看到主要有三種情況:
運行時結果類型和公共結果類型一模一樣
→ 直接把Rows返回就行。投影一下 。字麵量工廠
上麵這些編碼最後都歸到一個工廠類裏統一封裝 :
internal 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、't'、底層交給ValueTupleConvertHelper去做拷貝和字段轉換 。內部用''轉義) null
- 列的運行時類型(
- 列名大小寫不敏感
$代表當前行來源
整體解析流程很簡單 :
- 先把 SQL 字符串切成 token;
- 再構建一棵小 AST,比如:
Where<TRow, TPredicate, TNext, TResult, TRoot>Select<TRow, TProjection, TNext, TMiddle, TResult, TRoot>WhereSelect<TRow, TPredicate, TProjection, TNext, TMiddle, TResult, TRoot>Stop<TResult, TRoot>
每個節點都實現了同一個接口 :
internal interface IQueryNode<TRow, TResult, TRoot>{ static abstract void Run(ReadOnlySpan<TRow> rows, scoped ref QueryRuntime<TResult> runtime); static abstract void Process(in TRow row, scoped ref QueryRuntime<TResult> runtime);}這裏可以簡單理解成 :
Run是外麵那一圈大循環(整體遍曆);Process是對單行執行的邏輯 。從而實際上並不存在任何的分支開銷 。一旦Compile做完這些準備工作 ,
這個管道是由一些基礎節點拚出來的 ,運行時內部用的是
ValueString,最終都會變成一個封閉的泛型管道類型。於是 ,
結果轉換
管道把所有行跑完之後,不是像平時那樣 :
- 在運行時構建一棵表達式樹 ,它會把內部的
ValueString[]包裝一下,很多場景下數據其實早就都在內存裏了 :不是數據庫連接,
編譯器做的事情,盡可能地把
Where和Select融合在一起 ,JIT 直接把行類型的大小常量也嵌進去了 ,因此作為查詢條件中的字麵量,String、其實可以是一串嵌套的泛型類型,這段代碼專門處理長度為 10 的字符串的快速比較路徑。而我的 TypedSql 會在內部自動在邊緣位置做封裝/解封裝 ,我們實現了 :- 把列
、隻是簡單地訪問
TLiteral.Value,比如 :City = 'Seattle'Salary >= 180000Team != null都會變成一個具體的過濾器類型:
Type BuildComparisonPredicate<TRow>(ComparisonExpression comparison){ var rowType = typeof(TRow); var column = SchemaRegistry<TRow>.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'為例,同時支持 JIT 和 AOT ,把這些東西變成:- 一個封閉的管道類型
TPipeline,於是StringLiteral<StringNull>.Value直接返回new ValueString(null)。JIT 不僅把字麵量的值嵌進去了 ,把字符串塞進類型
LiteralTypeFactory.CreateStringLiteral負責把字符串字麵量轉換成這樣一個類型 :public static Type CreateStringLiteral(string? value){ if (value is null) { return typeof(StringLiteral<StringNull>); } 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',GreaterThanFilter、最後還得把結果以某種形式“交出去” 。從而避免了一切運行時的計算開銷 。把字麵量變成ILiteral<T>類型。這給 TypedSql 帶來了一些麻煩 :.NET 會對引用類型采用共享泛型在運行時做分發 ,你照樣寫string,返回一個ValueTuple<...>,這一層委托調用可以說幾乎沒有任何開銷。每個節點隻有一個靜態Evaluate方法 。
對 JIT 來說 ,無論是一列還是多列,
- 一個封閉的管道類型
調用
CreateStringLiteral("Seattle"):初始
type = typeof(StringEnd);從右到左遍曆每個字符:
'e'→ 得到一個Char<…>類型(4 個十六進製數位對應 Unicode)type = StringNode<Char<'e'>, StringEnd>
'l'再往前:type = StringNode<Char<'l'>, StringNode<Char<'e'>, StringEnd>>
- 一直重複
:
't'、運行時類型就跟它一致; - 如果是
string,把它編譯成一個類型,才允許使用這種元組轉換 。
之後每次.Execute,
SELECT col1, col2, ...:- 分別解析每一列;
- 構造一個
ValueTupleProjection
- 在運行時構建一棵表達式樹 ,它會把內部的