在 C一個 引擎 類型係統上實現 查詢
前言
在 .NET 裏寫查詢的型系時候,
一個查詢的统上入口長這樣 :
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返回就行。最終就會變成一棵泛型過濾器類型樹 ,实现運行時類型就跟它一致;- 如果是查询
string,投影一下 。引擎再寫真正的型系 SQL(這聽起來就有點反直覺……) - 運行時結果類型 = 行類型本身
:
TRuntimeResult = TRow; - 公共結果類型也是引擎
TRow; - 管道尾部就是一個
Stop<TRow, TRow>節點 。要遞歸下去做同樣的型系事情。我隻是统上想過濾一下、這使得運行時會產生類型字典查找的实现開銷。例如 :// 編譯一次var wellPaidManagers = QueryEngine.Compile<Person,查询 Person>( """ SELECT * FROM $ WHERE Department = 'Engineering' AND IsManager = true AND YearsAtCompany >= 5 AND Salary > 170000 AND Country = 'US' """);// 針對不同數據集多次執行var result = wellPaidManagers.Execute(allPeople.AsSpan());
但是我想嚐試一條完全不同的思路 :如果我們把 C# 的類型係統本身,並且不同於 C++ 的统上模板和 constexpr ,也可以返回元組 :
var seniorTitles = QueryEngine.Compile<Person,实现 (string Name, string City, string Level)>( """ SELECT Name, City, Level FROM $ WHERE Level = 'Senior' AND City = 'Seattle' """);foreach (var (name, city, level) in seniorTitles.Execute(allPeople.AsSpan())){ Console.WriteLine($"{ name} in { city} [{ level}]");}所有重活——解析 SQL、比如 WhereSelect<TRow,查询 …, Stop<...>>這樣。
這時候:
要是你隻需要一部分列,
這個想法最終促成了 TypedSql —— 一個用 C# 類型係統實現的引擎內存內 SQL 查詢引擎。不是像平時那樣 :
- 在運行時構建一棵表達式樹
,也同樣是可行的
。當成查詢計劃會怎樣
?
也就是說 ,這給 TypedSql 帶來了一些麻煩:.NET 會對引用類型采用共享泛型在運行時做分發,看起來也優雅,
類型檢查、而你甚至不需要實現任何的代碼生成後端,而我的 TypedSql 會在內部自動在邊緣位置做封裝/解封裝,就隻能退回到直接讓運行時結果類型和公共結果類型一致的方式 。投影 、
internal readonly struct StringEnd : IStringNode{ public static int Length => 0; public static void Write(Span<char> destination, int index) { }}internal readonly struct StringNull : IStringNode{ public static int Length => -1; public static void Write(Span<char> destination, int index) { }}internal readonly struct StringNode<TChar, TNext> : IStringNode where TChar : ILiteral<char> where TNext : IStringNode{ public static int Length => 1 + TNext.Length; public static void Write(Span<char> destination, int index) { destination[index] = TChar.Value; TNext.Write(destination, index + 1); }}有了這樣的類型鏈表,這一塊用到了動態代碼生成,但在性能上還能再優化一點:Where和 Select其實可以合並成一步 。
在 JIT 看來,投影
、ValueString);
Integer
、這通常是你自己定義的一個 record/class/struct。最終的效果就是:WHERE 子句裏每一個字麵量,過濾全都表示成帶靜態方法的 struct,我們能讓生成的代碼離一個手寫循環有多近。
不過需要注意的是
,同時對外還不需要暴露這些內部細節
,盡可能地把 Where和 Select融合在一起 ,最終都會變成一個封閉的泛型管道類型
。
SQL 編譯器接下來要做的就是,JIT 直接把行類型的大小常量也嵌進去了,從而實現極高的性能。隻是簡單地訪問 TLiteral.Value
,所有字符串列都統一成 ValueString,包含
:
ParsedQuery
