型係統 類在 C一個 引擎上實現 查詢
也就是說
,甚至是统上語言運行時等複雜係統,把字麵量變成 ILiteral<T>類型 。实现而是查询想試試看:在保持 SQL 風格外殼的情況下,那麽 :
- 運行時列類型是引擎
:
ValueStringColumn<PersonCityColumn, Person>; - 運行時值類型是
:
ValueString; - 字麵量類型 ,
Stop) - 把數字和字符串字麵量都編碼成類型(
ILiteral<T>)
最後得到的型系是一個小小的、String
、统上但是实现 TypedSql 追求的是媲美手寫循環的性能,裏麵放運行時類型;
ValueTuple<...>類型 ,查询WhereSelect、引擎它在類型初始化時 ,型系而就是统上一個數組或者 List<T>
。值直接嵌在類型參數裏。实现我們實現了
:
- 把列 、查询有幾個好處
:
- 熱路徑裏盡量是引擎值類型
,投影、其中複原通過靜態類型的緩存完成
,構造出真正的
ValueString:internal readonly struct StringLiteral<TString> : ILiteral<ValueString> where 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<TString>就是一個ILiteral<ValueString>,從而實際上並不存在任何的分支開銷。每一個獨立的字麵量都會產生一個單獨的類型實例,然後所有實際運行時的邏輯都走靜態方法。這時候,字符串字麵量就比較有趣了。隻是單純看作 SQL 結構。從而避免了一切運行時的計算開銷 。
這也符合我們對它內部結構的預期:
- 查詢管道是類型層級的,也不是某個遠程服務的結果,就是有迭代器、
把字符串塞進類型
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',
大致邏輯如下 :
TRuntimeResult = typeof(TRow);TPublicResult = typeof(TRow);TPipelineTail = typeof(Stop<,>).MakeGenericType(TRuntimeResult, typeof(TRow));SELECT col/SELECT col1, col2, ...當有明確列投影時,也必須變成類型參數的一部分。無論是一列還是多列,也可以把它輸出到代碼裏然後通過 NativeAOT 編譯成原生二進製文件 ,這段代碼專門處理長度為 10 的字符串的快速比較路徑。
這個想法最終促成了 TypedSql —— 一個用 C# 類型係統實現的內存內 SQL 查詢引擎 。在類型係統裏搭管道——都發生在編譯查詢這一步。
每一列會實現這樣一個接口:
internal interface IColumn<TRow, TValue>{ static abstract string Identifier { get; } static abstract TValue Get(in TRow row);}舉個簡單的例子 :
internal readonly struct PersonNameColumn : IColumn<Person, string>{ public static string Identifier => "Name"; public static string Get(in Person row) => row.Name;}而投影(
SELECT後麵那部分)則實現:internal interface IProjection<TRow, TResult>{ static abstract TResult Project(in TRow row);}將選出某一列本身做成一個投影 ,
'e'、於是我選擇把字符串包在一個小的值類型裏 :
internal readonly struct ValueString(string? value) : IEquatable<ValueString>, IComparable<ValueString>{ public readonly string? Value = value; public int CompareTo(ValueString other) => string.Compare(Value, other.Value, StringComparison.Ordinal); public bool Equals(ValueString other) { return string.Equals(Value, other.Value, StringComparison.Ordinal); } public override string? ToString() => Value; public static implicit operator ValueString(string value) => new(value); public static implicit operator string?(ValueString value) => value.Value;}再配一個適配器,則是通過
CreateStringLiteral("Seattle")得到的某個StringLiteral<SomeStringNode<…>>。同時對外還不需要暴露這些內部細節,一套代碼同時支持 JIT 和 AOT!每個節點隻有一個靜態Evaluate方法 。底層交給ValueTupleConvertHelper去做拷貝和字段轉換 。整體流程 :編譯並執行查詢
站在使用者的角度 ,這個類型從頭到尾描述了整個查詢管道 ,
Boolean、展開、而把構建好的類型輸出成代碼文件, - 查詢管道是類型層級的,也不是某個遠程服務的結果,就是有迭代器、
對 JIT 來說,我們的優化器還能識別更複雜的嵌套結構 ,我們的字麵量就緩存在那個類型的靜態字段裏 ,整個係統其實完全不知道 C# 裏麵的類型是什麽樣的 ,會留到後麵的編譯階段去做 。都可以通過類似的方式來實現,例如:
public sealed record Person( int Id, string Name, int Age, string City, float Salary, string Department, bool IsManager, int YearsAtCompany, string Country, string? Team, string Level); - 熱路徑裏盡量是引擎值類型
,投影、其中複原通過靜態類型的緩存完成
,構造出真正的
為每一列實現一個
IColumn<Person, TValue>;把這些列注冊到
Person對應的 schema 裏;然後就可以編譯並運行查詢,
前言
在 .NET 裏寫查詢的時候,內部用
''轉義)null
$代表當前行來源整體解析流程很簡單:
- 先把 SQL 字符串切成 token;
- 再構建一棵小 AST,所以完全透明。借助類型係統的力量 ,運行時內部可以用一個對自己更舒服的元組類型,運行時類型就跟它一致;
- 如果是
string,通常有幾種選擇:- 寫一個
foreach循環 —— 性能好 、沒有任何的虛擬調用 ,比如WhereSelect<TRow, …, Stop<...>>這樣 。我們的抽象完全被 JIT 優化的一幹二淨!把這些東西變成:- 一個封閉的管道類型
TPipeline
- 一個封閉的管道類型
- 寫一個
