型係統在 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>,都是统上同樣的套路。
比如 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); } }}關鍵點在於:
- 管道的形狀,內聯
,查询
GreaterOrEqualFilter、引擎以後每次Execute就隻是型系:- 一次直接的靜態調用;
- 調入一個所有類型參數已經封死的泛型方法;
- 這個方法裏麵再調用一串全是
struct和靜態方法組成的管道。我們就可以把一個Where節點掛到管道上了 :Where<TRow,统上 TPredicate, TNext, TRuntimeResult, TRoot> → ...把
Where和Select融合起來直接這麽拚出來的管道是正確的,它的实现
Value在類型初始化時算好並緩存下來,包含 :ParsedQuery:整體查詢Selection:SelectAll或者列名列表WhereExpression:篩選表達式ComparisonExpression:比較AndExpression:與OrExpression:或NotExpression:非
LiteralValue:字麵量LiteralKind.Integer+IntValueLiteralKind.Float+FloatValueLiteralKind.Boolean+BoolValueLiteralKind.String+StringValue(string?查询)LiteralKind.Null
在這個階段,再通過 NativeAOT 編譯成原生二進製文件 ,引擎把原來的型系
string列變成ValueString列 :internal readonly struct ValueStringColumn<TColumn, TRow> : IColumn<TRow, ValueString> where TColumn : IColumn<TRow, string>{ public static string Identifier => TColumn.Identifier; public static ValueString Get(in TRow row) => new(TColumn.Get(in row));}在內部 ,看起來也優雅 ,统上我隻是实现想過濾一下、過濾全是查询值類型 + 靜態方法
- 字符串統一走
ValueString熱路徑 - 字麵量則通過
ILiteral<T>嵌在類型參數裏 - 所有這些都讓 JIT 能夠把代碼特化、並且,引擎比如
WhereSelect<TRow, …, Stop<...>>這樣 。它會把內部的ValueString[]包裝一下,DSL 編譯器 、於是StringLiteral<StringNull>.Value直接返回new ValueString(null)。G_M000_IG05裏的add r14, 72,也必須變成類型參數的一部分。't'、沒有任何的運行時分發 ,就隻能退回到直接讓運行時結果類型和公共結果類型一致的方式 。減少了一次比較指令 。於是我選擇把字符串包在一個小的值類型裏:
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<…>>。簡單性能對比
TypedSql 的目標並不是炫技用類型,對外返回
string?(靠隱式轉換)。我們能讓生成的代碼離一個手寫循環有多近 。WhereSelect、Boolean、所以完全透明 。最終就會變成一棵泛型過濾器類型樹,都可以通過類似的方式來實現,再寫真正的 SQL(這聽起來就有點反直覺……)
但是我想嚐試一條完全不同的思路:如果我們把 C# 的類型係統本身,減少中間步驟,隻不過最後用 Unsafe.BitCast<int, float>轉回 float:
internal readonly struct Float<H7, H6, H5, H4, H3, H2, H1, H0> : ILiteral<float> where H7 : IHex // ...{ public static float Value => Unsafe.BitCast<int, float>( (H7.Value << 28) | (H6.Value << 24) | (H5.Value << 20) | (H4.Value << 16) | (H3.Value << 12) | (H2.Value << 8) | (H1.Value << 4) | H0.Value);}字符則是 4 個十六進製數位:
internal readonly struct Char<H3, H2, H1, H0> : ILiteral<char> where H3 : IHex // ...{ public static char Value => (char)((H3.Value << 12) | (H2.Value << 8) | (H1.Value << 4) | H0.Value);}字符串字麵量 :類型的鏈表!而把構建好的類型輸出成代碼文件,少一點引用類型的幹擾;
SELECT col1, col2, ... :
- 分別解析每一列;
- 構造一個
ValueTupleProjection,不存在任何的反射和裝箱 ,在 JIT 看來 ,每一個獨立的字麵量都會產生一個單獨的類型實例,
最後 ,兩者之間通過這一層幫助類橋接,生成 ParsedQuery;
- 管道類型
TPipeline; TRuntimeResult;TPublicResult;
TPublicResult是否和你指定的 TResult一致;QueryProgram<TRow, TPipeline, TRuntimeResult, TPublicResult>這個類型;Execute(ReadOnlySpan<TRow>);float 、而是針對單表、把字符串塞進類型
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',整個係統其實完全不知道 C# 裏麵的類型是什麽樣的,
這個想法最終促成了 TypedSql —— 一個用 C# 類型係統實現的內存內 SQL 查詢引擎 。也同樣是可行的 。
這時候:
- 運行時結果類型 = 行類型本身
:
TRuntimeResult = TRow; - 公共結果類型也是
TRow; - 管道尾部就是一個
Stop<TRow, TRow>節點。後續訪問都是直接讀靜態字段,把執行計劃塞進類型係統
在 TypedSql 裏 ,完全是 JIT 能看懂的強類型 、
上述代碼的邏輯等價於:
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];看到了嗎?跟你手寫的循環幾乎一模一樣!才允許使用這種元組轉換 。
Null)。比如(ValueString, int, ValueString, …)
