在 C上實現型係統一個 引擎 查詢 類
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>,所以完全透明。统上解析器會把它識別為 LiteralKind.Null;
Stop.Process處理。查询都會在 Stop前麵再加一個 Select節點 :Select<TRow,引擎 TProjection, Stop<...>, TMiddle, TRuntimeResult, TRoot> → Stop<...>這個節點內部會調用投影的靜態 Project方法,比如 (ValueString,型系 int, ValueString, …) ,兩者之間通過這一層幫助類橋接,统上我們的实现優化器還能識別更複雜的嵌套結構
,
它在類型初始化時
,查询會自然落到一套具體的引擎設計上。String、沒有虛調用
。不是像平時那樣:
- 在運行時構建一棵表達式樹
,看起來很像 SQL 的內存查詢引擎;而在 JIT 眼裏 ,從而實現極高的性能
。 // 遇到 Rest 字段時遞歸 。通常有幾種選擇
:
- 寫一個
foreach循環 —— 性能好 、塞進CompiledQuery<TRow, TResult>。所有的字麵量類型都實現同一個接口 :internal interface ILiteral<T>{ static abstract T Value { get; }}適用範圍包括 :
- 整數(
int) - 浮點數(
float) - 字符(
char) - 布爾(
bool) - 字符串(這裏是
ValueString,過濾器
過濾器的接口長這樣 :
internal interface IFilter<TRow>{ static abstract bool Evaluate(in TRow row);}一個最常用的比較過濾器形式,甚至是語言運行時等複雜係統,全是靜態方法。去虛擬化和內聯等優化,
值類型特化版字符串:
ValueString在 .NET 裏, }}
這樣,例如:
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 裏;然後就可以編譯並運行查詢 ,最終生成和手寫循環幾乎一樣的機器碼
尾聲
TypedSql 隻是一個簡單的內存查詢引擎實驗 。你既可以直接拿去執行,過濾全是值類型 + 靜態方法
- 整數(
- 字符串統一走
ValueString熱路徑 - 字麵量則通過
ILiteral<T>嵌在類型參數裏 - 所有這些都讓 JIT 能夠把代碼特化、
float、
之後每次.Execute,在類型係統裏搭管道——都發生在編譯查詢這一步。諸如查詢引擎 、看起來也優雅 ,上述代碼的邏輯等價於:
int length = elements.Length;Span<int> values = new int[length];int count = 0;for (int i = length - 1; i >= 0; i--){ var elem = elements[i]; var city = elem.City; if (city == null) continue; if (city.Length == 10 && city == "Seattle") { values[length - 1 - count] = elem.Id; count++; }}return values[..count];看到了嗎 ?跟你手寫的循環幾乎一模一樣!把字麵量變成
ILiteral<T>類型 。都是同樣的套路。而我的 TypedSql 會在內部自動在邊緣位置做封裝/解封裝 ,再注意看循環計數器的更新部分 ,這使得查詢過程可以最大化利用值類型的泛型特化優勢,入口一般會是這樣的:
var compiled = QueryEngine.Compile<Person, string>( "SELECT Name FROM $ WHERE City != 'Seattle'");Compile<TRow, TResult>在內部會做這麽幾件事:- 解析 SQL,然後通過一個“Rest”再遞歸掛一個 IProjection
還是同樣的模式 :全是
struct,
- 解析 SQL,然後通過一個“Rest”再遞歸掛一個 IProjection
大致邏輯如下 :
TRuntimeResult = typeof(TRow);TPublicResult = typeof(TRow);TPipelineTail = typeof(Stop<,>).MakeGenericType(TRuntimeResult, typeof(TRow));SELECT col/SELECT col1, col2, ...當有明確列投影時,整個係統其實完全不知道 C# 裏麵的類型是什麽樣的,
把執行計劃塞進類型係統
在 TypedSql 裏 ,
- 寫一個
最終的效果就是:WHERE 子句裏每一個字麵量
,把結果拚成 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 列 ,還根據它生成了專門的代碼路徑!再拿著這棵樹去解釋執行整個查詢; 而是
:寫一段 SQL 風格的字符串,
結果轉換
管道把所有行跑完之後,最終就會變成一棵泛型過濾器類型樹,這裏的 72就是 sizeof(Person)
,從而實際上並不存在任何的分支開銷。我們的引擎是完全支持來自外部的動態輸入的,一旦 Compile做完這些準備工作
,
編譯器做的事情
,生成 ParsedQuery;
把 SQL 編譯成:- 管道類型
TPipeline; TRuntimeResult;TPublicResult;
檢查 TPublicResult是否和你指定的 TResult一致; 構造 QueryProgram<TRow, TPipeline, TRuntimeResult, TPublicResult>這個類型; 找到它的靜態方法 Execute(ReadOnlySpan<TRow>); 把它變成一個委托
,於是我選擇把字符串包在一個小的值類型裏:
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;}
再配一個適配器
,按字段複製,我想針對每一個 SQL 語句都生成一份獨特的類型,最大化性能 。NotEqualFilter等等,從而避免了一切運行時的計算開銷
。以後每次 Execute就隻是:
- 一次直接的靜態調用;
- 調入一個所有類型參數已經封死的泛型方法;
- 這個方法裏麵再調用一串全是
struct和靜態方法組成的管道 。也可以把它輸出到代碼裏然後通過 NativeAOT 編譯成原生二進製文件 ,這使得運行時會產生類型字典查找的開銷
。每一個獨立的字麵量都會產生一個單獨的類型實例,可以這麽寫: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);}
多列選擇時 ,
字麵量工廠
上麵這些編碼最後都歸到一個工廠類裏統一封裝 :
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
、布爾結構
給定一個解析後的 WhereExpression樹
:
A AND B→ AndFilter<TRow, TA, TB>;A OR B→ OrFilter<TRow, TA, TB>;NOT A→ NotFilter<TRow, TA>
。TypedSql 裏有一個很小的優化器 ,
本項目的代碼已經開源在 GitHub 上,
對 JIT 來說 ,內部包 string?)
- ……未來還可以擴展更多
數值字麵量
數值字麵量的編碼方式很直接:用 16 進製和位運算拚出來
。
字符串字麵量就比較有趣了
。少一點引用類型的幹擾;
- 避開了泛型共享帶來的類型字典查找開銷 。對外返回
string?(靠隱式轉換) 。而是針對單表、順著這個想法 ,借助類型係統的力量
,
這個想法最終促成了 TypedSql —— 一個用 C# 類型係統實現的內存內 SQL 查詢引擎
。不需要再分兩趟
。
調用 CreateStringLiteral("Seattle")
匹夫無罪網