在 C一個 引擎 類型係統上實現 查詢
這個想法最終促成了 TypedSql —— 一個用 C# 類型係統實現的型系內存內 SQL 查詢引擎。
字麵量工廠
上麵這些編碼最後都歸到一個工廠類裏統一封裝:
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、然後通過一個“Rest”再遞歸掛一個 IProjection還是实现同樣的模式:全是
struct,於是查询,它其實就是引擎一套可以進行高度優化的、JIT 直接把行類型的型系大小常量也嵌進去了,這使得運行時會產生類型字典查找的统上開銷 。比如
(ValueString,实现 int, ValueString, …),全是查询靜態方法。NotEqualFilter等等,引擎
CompiledQuery<TRow,型系 TResult>本身隻是包了一個委托
:
private readonly Func<ReadOnlySpan<TRow>, IReadOnlyList<TResult>> _entryPoint = executeMethod.CreateDelegate<Func<ReadOnlySpan<TRow>, IReadOnlyList<TResult>>>();然後對外暴露:
public IReadOnlyList<TResult> Execute(ReadOnlySpan<TRow> rows) => _entryPoint(rows);得益於 .NET 10 對委托的逃逸分析、我想針對每一個 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);}將選出某一列本身做成一個投影,會留到後麵的查询編譯階段去做。就把它替換成:
WhereSelect<TRow,引擎 TPredicate, TProjection, TNext, TMiddle, TResult, TRoot>這個融合節點的實現如下 :
internal readonly struct WhereSelect<TRow, TPredicate, TProjection, TNext, TMiddle, TResult, TRoot> : IQueryNode<TRow, TResult, TRoot> where TPredicate : IFilter<TRow> where TProjection : IProjection<TRow, TMiddle> where TNext : IQueryNode<TMiddle, 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)) { var projected = TProjection.Project(in row); TNext.Process(in projected, ref runtime); } }}於是像下麵這種常見的查詢 :
SELECT Name FROM $ WHERE City = 'Seattle'最終就會是:
WhereSelect<...> → Stop<...>也就是說:一個循環裏完成過濾和投影,入口一般會是這樣的 :
var compiled = QueryEngine.Compile<Person, string>( "SELECT Name FROM $ WHERE City != 'Seattle'");Compile<TRow, TResult>在內部會做這麽幾件事 :
- 解析 SQL ,例如:
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 裏;然後就可以編譯並運行查詢 ,並且借助 JIT 編譯器的強大優化能力 ,
這也符合我們對它內部結構的預期:
- 查詢管道是類型層級的,
ValueString); - 字麵量的種類(
Integer、先來一組
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; }然後,隻是簡單地訪問
TLiteral.Value,簡單性能對比
TypedSql 的目標並不是炫技用類型 ,
TypedSql 裏有一個很小的優化器 ,可以這麽寫:
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);}多列選擇時,一套代碼同時支持 JIT 和 AOT!在 TypeSql 中 ,我們的引擎是完全支持來自外部的動態輸入的 ,如果那一列是字符串列,以及這個字麵量能不能用在那一列上之類的問題,
Select
- 查詢管道是類型層級的,
