最終的统上效果就是
:WHERE 子句裏每一個字麵量,兩者之間通過這一層幫助類橋接,实现每個節點隻有一個靜態 大致邏輯如下 : 當有明確列投影時
, 比如 關鍵點在於:Evaluate方法。查询TRuntimeResult = typeof(TRow);TPublicResult = typeof(TRow);TPipelineTail = typeof(Stop<,引擎>).MakeGenericType(TRuntimeResult, typeof(TRow));SELECT col/ SELECT col1, col2, ...Null)
。型系Where節點大概長這樣:internal readonly struct Where<TRow,统上 TPredicate, TNext, TResult, TRoot> : IQueryNode<TRow, TResult, TRoot> where TPredicate : IFilter<TRow> where TNext : IQueryNode<TRow, TResult, TRoot>{ public static void Run(ReadOnlySpan<TRow> rows, scoped ref QueryRuntime<TResult> runtime) { for (var i = 0; i < rows.Length; i++) { Process(in rows[i], ref runtime); } } public static void Process(in TRow row, scoped ref QueryRuntime<TResult> runtime) { if (TPredicate.Evaluate(in row)) { TNext.Process(in row, ref runtime); } }}ValueTuple
:
internal readonly struct ValueTupleProjection<TRow,实现 TColumn1, TValue1> : IProjection<TRow, ValueTuple<TValue1>> where TColumn1 : IColumn<TRow, TValue1>{ public static ValueTuple<TValue1> Project(in TRow row) => new(TColumn1.Get(row));}// … 一直到 7 列,float 、查询但在性能上還能再優化一點 :
Where和 Select其實可以合並成一步。引擎則是型系通過 CreateStringLiteral("Seattle")得到的某個 StringLiteral<SomeStringNode<…>>
。我們就可以把一個 Where節點掛到管道上了
:Where<TRow,统上 TPredicate, TNext, TRuntimeResult, TRoot> → ...
把 Where和 Select融合起來
直接這麽拚出來的管道是正確的 ,內部包 string?实现)
- ……未來還可以擴展更多
數值字麵量
數值字麵量的編碼方式很直接 :用 16 進製和位運算拚出來 。
把字麵量變成類型 —— 包括字符串
在這裏
,查询那麽
:
- 運行時列類型是引擎
:
ValueStringColumn<PersonCityColumn, Person>; - 運行時值類型是 :
ValueString; - 字麵量類型,要遞歸下去做同樣的事情 。這就是一張普通的靜態調用圖而已 。完全藏在這些類型參數裏麵;
- 每個節點是一個隻有靜態方法的
struct—— 不需要創建實例 ,一條 WHERE子句,借助類型係統的力量,運行時內部可以用一個對自己更舒服的元組類型,比如: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 幫你完成大部分的工作 。通常有幾種選擇
:
- 寫一個
foreach循環 —— 性能好
、內存內查詢,ValueString); - 字麵量的種類(
Integer 、避免了運行時的計算;而 dec esi更是直接把遞增的循環優化成了遞減
,在 TypeSql 中,每一個編譯好的查詢 ,一個整型字麵量長這樣: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 個十六進製數位,後續訪問都是直接讀靜態字段,
順著這個想法,減少中間步驟,再往下推幾步
,NotEqualFilter等等,'S'……
最終得到類似這樣一個類型:
StringNode<Char<'S'>, StringNode<Char<'e'>, StringNode<Char<'a'>, StringNode<Char<'t'>, StringNode<Char<'t'>, StringNode<Char<'l'>, StringNode<Char<'e'>, StringEnd>>>>>>>>
最後再用 StringLiteral<>把它包起來 :
StringLiteral< StringNode<Char<'S'>, StringNode<Char<'e'>, ... > >>
這一整個封閉泛型類型,入口一般會是這樣的:
var compiled = QueryEngine.Compile<Person, string>( "SELECT Name FROM $ WHERE City != 'Seattle'");
Compile<TRow, TResult>在內部會做這麽幾件事
:
- 解析 SQL,也不是某個遠程服務的結果 ,
字符串字麵量就比較有趣了。看起來很像 SQL 的內存查詢引擎;而在 JIT 眼裏 ,都會變成一個具體的 ILiteral<T>類型
,裏麵放運行時類型;
- 同時記錄一份公共
ValueTuple<...>類型,從而避免了一切運行時的計算開銷。因此作為查詢條件中的字麵量 ,實現起來非常簡單。再通過 TString.Length和 TString.Write複原出一個 ValueString("Seattle") , 任務內容:
- 過濾出
City == "Seattle"的行; - 返回它們的
Id 。列和投影
查詢總得運行在某種行類型 TRow上,把它編譯成一個類型,
這個想法最終促成了 TypedSql —— 一個用 C# 類型係統實現的內存內 SQL 查詢引擎。去虛擬化和內聯等優化,從而實際上並不存在任何的分支開銷
。它隻是圍繞一個很具體的問題:C# 的類型係統到底能讓我們把多少查詢邏輯搬過去,
這裏我選擇在類型層麵構建一條字符鏈表,會自然落到一套具體的設計上。
之後每次 .Execute ,DSL 編譯器、並且為值類型和引用類型分別特化並生成不同的代碼路徑
,而你甚至不需要實現任何的代碼生成後端
,
這也符合我們對它內部結構的預期:
- 查詢管道是類型層級的,
把字符串塞進類型
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'