型係統在 C上實現 類一個 引擎 查詢
值類型特化版字符串
:ValueString
在 .NET 裏,实现外麵希望看到 string
→ 調用 AsStringRows,查询
列和投影
查詢總得運行在某種行類型 TRow上 ,引擎
SQL 編譯器接下來要做的型系就是 ,
這時候:
- 運行時結果類型 = 行類型本身:
TRuntimeResult = TRow; - 公共結果類型也是统上
TRow; - 管道尾部就是一個
Stop<TRow, TRow>節點。對外返回string?实现(靠隱式轉換) 。並且借助 JIT 編譯器的查询強大優化能力,所以隻需要計算一次 ,引擎委托帶來的型系那點開銷; - 要麽幹脆極端一點:把數據塞進數據庫 ,JIT 直接把我們的统上字符串字麵量的長度常量嵌進了機器碼裏;進一步當長度匹配時 ,
NotEqualFilter等等 ,实现這時候,查询
對 JIT 來說
,引擎Select、也同樣是可行的。內部包 string?)
數值字麵量
數值字麵量的編碼方式很直接 :用 16 進製和位運算拚出來 。整個流程大致是:
解析階段讀到
'Seattle',展望未來的應用 ,
布爾結構
給定一個解析後的
WhereExpression樹 :A AND B→AndFilter<TRow, TA, TB>;A OR B→OrFilter<TRow, TA, TB>;NOT A→NotFilter<TRow, TA>。隻需要簡單地把泛型參數取出來重新帶入到新的融合類型即可 ,也可以返回元組 :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 、
編譯
SELECT先看選擇部分。運行時內部可以用一個對自己更舒服的元組類型 ,
把字麵量變成類型 —— 包括字符串
在這裏 ,不是像平時那樣:
- 在運行時構建一棵表達式樹,我隻是想過濾一下 、用聲明的 CLR 類型(如
string)。一個整型字麵量長這樣:internal readonly struct Int<H7, H6, H5, H4, H3, H2, H1, H0> : ILiteral<int> where H7 : IHex // ... where H0 : IHex{ public static int Value => (H7.Value << 28) | (H6.Value << 24) | (H5.Value << 20) | (H4.Value << 16) | (H3.Value << 12) | (H2.Value << 8) | (H1.Value << 4) | H0.Value;}浮點數也是一樣的 8 個十六進製數位,並且,我們的引擎是完全支持來自外部的動態輸入的,其實可以是一串嵌套的泛型類型,隻要利用好 C# 的泛型和靜態成員 ,
GreaterThanFilter、也可以把它輸出到代碼裏然後通過 NativeAOT 編譯成原生二進製文件,兩全其美。Null)。最終都會變成一個封閉的泛型管道類型。則是通過CreateStringLiteral("Seattle")得到的某個StringLiteral<SomeStringNode<…>>。達到了性能和易用性的平衡。生成一個LiteralValue:Kind == LiteralKind.StringStringValue == "Seattle"
編譯階段根據列的類型判斷:這是個字符串列,
搭好整個管道類型
到目前為止,再通過 NativeAOT 編譯成原生二進製文件,我們的抽象完全被 JIT 優化的一幹二淨!
Float、LessOrEqualFilter、諸如查詢引擎、返回一個ValueTuple<...>,'t'、我們能讓生成的代碼離一個手寫循環有多近 。而外麵看到的則是(string, int, string, …),這個想法最終促成了 TypedSql —— 一個用 C# 類型係統實現的內存內 SQL 查詢引擎。避免了運行時的計算;而
dec esi更是直接把遞增的循環優化成了遞減,生成非常高效的代碼。從而在保持靈活性的同時 ,因此答案是肯定的:.NET 的類型係統完全可以用來表達圖靈完備的邏輯,一旦
Compile做完這些準備工作,而這並不需要複雜的優化算法 ,雖然這點開銷不大,可以這麽寫:internal readonly struct ColumnProjection<TColumn, TRow, TValue> : IProjection<TRow, TValue> where TColumn : IColumn<TRow, TValue>{ public static TValue Project(in TRow row) => TColumn.Get(row);}多列選擇時,隻是簡單地訪問
TLiteral.Value,然後通過一個“Rest”再遞歸掛一個 IProjection還是同樣的模式:全是
struct,內部用''轉義)null
- 列名大小寫不敏感
$代表當前行來源- 先把 SQL 字符串切成 token;
- 再構建一棵小 AST,並通過接口的靜態抽象成員來約束它們的行為
- 把它們組合成一串嵌套的泛型管道節點(
Where、於是,
'a'、會自然落到一套具體的設計上 。 // 遇到 Rest 字段時遞歸。也不是某個遠程服務的結果 ,大概是對這棵樹一層層往下調自己的方法:Type BuildPredicate<TRow>(WhereExpression expr){ return expr switch { ComparisonExpression cmpExpr => BuildComparisonPredicate<TRow>(cmpExpr), AndExpression andExpr => typeof(AndFilter<,,>).MakeGenericType(typeof(TRow), BuildPredicate<TRow>(andExpr.Left), BuildPredicate<TRow>(andExpr.Right)), OrExpression orExpr => typeof(OrFilter<,,>).MakeGenericType(typeof(TRow), BuildPredicate<TRow>(orExpr.Left), BuildPredicate<TRow>(orExpr.Right)), NotExpression notExpr => typeof(NotFilter<,>).MakeGenericType(typeof(TRow), BuildPredicate<TRow>(notExpr.Expression)), _ => throw … };}比較表達式
每一個葉子比較表達式,我想針對每一個 SQL 語句都生成一份獨特的類型,設計了一個很小的 SQL 方言 :
支持這些語句 :
SELECT * FROM $SELECT col FROM $SELECT col1, col2, ... FROM $WHERE支持:- 比較
:
=,!=,>,<,>=,<= - 布爾
:
AND,OR,NOT - 括號
- 比較
:
- 字麵量支持
:
- 整數(如
42) - 浮點數(如
123.45) - 布爾(
true/false) - 單引號字符串(
'Seattle',所以我想盡量把熱路徑裏涉及的類型都做成值類型 。確保隻有在支持動態代碼的環境下 ,你照樣寫string,這給 TypedSql 帶來了一些麻煩:.NET 會對引用類型采用共享泛型在運行時做分發 ,它實現IQueryNode<TRow, TRuntimeResult, TRoot>; - 一個運行時結果類型
TRuntimeResult; - 一個對外公開的結果類型
TPublicResult。如果那一列是字符串列 ,提升性能。再把結果轉交給Stop.Process處理。而不是為string泛型實例化一個具體類型 ,不存在任何的反射和裝箱 ,TypedSql 會構造專門的投影,ValueString); - 字麵量的種類(
Integer、在 TypeSql 中,.NET 的 JIT 能夠識別這種模式 ,再注意看循環計數器的更新部分 ,展開 、從而避免了一切運行時的計算開銷 。 }}
這樣,這一塊用到了動態代碼生成,
順著這個想法 ,這裏的
72就是sizeof(Person),先來一組
IHex接口和Hex0–HexFstruct:internal interface IHex { static abstract int Value { get; } }internal readonly struct Hex0 : IHex { public static int Value => 0; }// ...internal readonly struct HexF : IHex { public static int Value => 15; }然後 ,並且為值類型和引用類型分別特化並生成不同的代碼路徑,
不過需要注意的是,

把查詢變成嵌套的泛型類型
TypedSql 的核心想法看上去非常簡單 :一個查詢,它會把內部的
ValueString[]包裝一下 ,從而實際上並不存在任何的分支開銷 。
大致邏輯如下 :
TRuntimeResult = typeof(TRow);TPublicResult = typeof(TRow);TPipelineTail = typeof(Stop<,>).MakeGenericType(TRuntimeResult, typeof(TRow));SELECT col/SELECT col1, col2, ...當有明確列投影時 ,DSL 編譯器 、
而過濾器在需要值的時候 ,而我的 TypedSql 會在內部自動在邊緣位置做封裝/解封裝 ,因此作為查詢條件中的字麵量 ,當成查詢計劃會怎樣?
也就是說 ,這一層委托調用可以說幾乎沒有任何開銷。
- 整數(如
它在類型初始化時 ,不需要再分兩趟 。兩者之間通過這一層幫助類橋接,
調用
CreateStringLiteral("Seattle"):初始
type = typeof(StringEnd);從右到左遍曆每個字符:
'e'→ 得到一個Char<…>類型(4 個十六進製數位對應 Unicode)type = StringNode<Char<'e'>, StringEnd>
'l'再往前 :type = StringNode<Char<'l'>, StringNode<Char<'e'>, StringEnd>>
- 一直重複 :
't'
整體解析流程很簡單 :
