├── TypedSql.slnx ├── TypedSql.csproj ├── LICENSE.txt ├── Runtime ├── Schema.cs ├── Pipeline.cs ├── QueryEngine.cs ├── ValueTupleConvertHelper.cs ├── TypeLiterals.cs ├── QueryRuntime.cs ├── SqlParser.cs ├── Expressions.cs └── SqlCompiler.cs ├── .gitattributes ├── Program.cs ├── DemoSchema.cs ├── README.ja.md ├── README.md └── .gitignore /TypedSql.slnx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /TypedSql.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net10.0 6 | enable 7 | enable 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) [year] [fullname] 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Runtime/Schema.cs: -------------------------------------------------------------------------------- 1 | namespace TypedSql.Runtime; 2 | 3 | internal readonly record struct ColumnMetadata(string Name, Type ColumnType, Type ValueType) 4 | { 5 | public Type GetRuntimeValueType() => ValueType != typeof(string) ? ValueType : typeof(ValueString); 6 | 7 | public Type GetRuntimeColumnType(Type rowType) 8 | { 9 | if (ValueType != typeof(string)) 10 | { 11 | return ColumnType; 12 | } 13 | 14 | return typeof(ValueStringColumn<,>).MakeGenericType(ColumnType, rowType); 15 | } 16 | } 17 | 18 | internal static class SchemaRegistry 19 | { 20 | private static IReadOnlyDictionary? _columns; 21 | 22 | public static void Register(IReadOnlyDictionary columns) 23 | { 24 | ArgumentNullException.ThrowIfNull(columns); 25 | _columns = columns; 26 | } 27 | 28 | public static ColumnMetadata ResolveColumn(string identifier) 29 | { 30 | if (_columns is null) 31 | { 32 | throw new InvalidOperationException($"Schema for row type '{typeof(TRow)}' has not been registered."); 33 | } 34 | 35 | if (_columns.TryGetValue(identifier, out var column)) 36 | { 37 | return column; 38 | } 39 | 40 | throw new KeyNotFoundException($"Column '{identifier}' is not registered for row type '{typeof(TRow)}'."); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | ############################################################################### 7 | # Set default behavior for command prompt diff. 8 | # 9 | # This is need for earlier builds of msysgit that does not have it on by 10 | # default for csharp files. 11 | # Note: This is only used by command line 12 | ############################################################################### 13 | #*.cs diff=csharp 14 | 15 | ############################################################################### 16 | # Set the merge driver for project and solution files 17 | # 18 | # Merging from the command prompt will add diff markers to the files if there 19 | # are conflicts (Merging from VS is not affected by the settings below, in VS 20 | # the diff markers are never inserted). Diff markers may cause the following 21 | # file extensions to fail to load in VS. An alternative would be to treat 22 | # these files as binary and thus will always conflict and require user 23 | # intervention with every merge. To do so, just uncomment the entries below 24 | ############################################################################### 25 | #*.sln merge=binary 26 | #*.csproj merge=binary 27 | #*.vbproj merge=binary 28 | #*.vcxproj merge=binary 29 | #*.vcproj merge=binary 30 | #*.dbproj merge=binary 31 | #*.fsproj merge=binary 32 | #*.lsproj merge=binary 33 | #*.wixproj merge=binary 34 | #*.modelproj merge=binary 35 | #*.sqlproj merge=binary 36 | #*.wwaproj merge=binary 37 | 38 | ############################################################################### 39 | # behavior for image files 40 | # 41 | # image files are treated as binary by default. 42 | ############################################################################### 43 | #*.jpg binary 44 | #*.png binary 45 | #*.gif binary 46 | 47 | ############################################################################### 48 | # diff behavior for common document formats 49 | # 50 | # Convert binary document formats to text before diffing them. This feature 51 | # is only available from the command line. Turn it on by uncommenting the 52 | # entries below. 53 | ############################################################################### 54 | #*.doc diff=astextplain 55 | #*.DOC diff=astextplain 56 | #*.docx diff=astextplain 57 | #*.DOCX diff=astextplain 58 | #*.dot diff=astextplain 59 | #*.DOT diff=astextplain 60 | #*.pdf diff=astextplain 61 | #*.PDF diff=astextplain 62 | #*.rtf diff=astextplain 63 | #*.RTF diff=astextplain 64 | -------------------------------------------------------------------------------- /Program.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | using TypedSql; 3 | using TypedSql.Runtime; 4 | 5 | SchemaRegistry.Register(DemoSchema.People); 6 | 7 | var rows = new[] 8 | { 9 | new Person(1, "Ada", 34, "Seattle", 180_000f, "Engineering", true, 6, "US", "Runtime", "Senior"), 10 | new Person(2, "Barbara", 28, "Boston", 150_000f, "Engineering", false, 3, "US", "Compiler", "Mid"), 11 | new Person(3, "Charles", 44, "Helsinki", 210_000f, "Research", true, 15, "FI", "ML", "Principal"), 12 | new Person(4, "David", 31, "Palo Alto", 195_000f, "Product", false, 4, "US", "Runtime", "Senior"), 13 | new Person(5, "Eve", 39, "Seattle", 220_000f, "Product", true, 10, "US", null, "Staff"), 14 | }; 15 | 16 | Console.WriteLine("Input data:"); 17 | foreach (var row in rows) 18 | { 19 | Console.WriteLine(row); 20 | } 21 | 22 | Console.WriteLine(); 23 | 24 | QueryEngine.CompiledQuery Compile(string query) 25 | { 26 | var compiledQuery = QueryEngine.Compile(query, supportsAot: !RuntimeFeature.IsDynamicCodeSupported); 27 | Console.WriteLine($"Compiled query for `{query}`:\n{compiledQuery}"); 28 | return compiledQuery; 29 | } 30 | 31 | // 1) Simple city filter 32 | foreach (var name in Compile("SELECT Name FROM $ WHERE city != 'Seattle'").Execute(rows)) 33 | { 34 | Console.WriteLine($" -> {name}"); 35 | } 36 | 37 | Console.WriteLine(); 38 | 39 | // 2) Senior, well-paid managers in engineering 40 | foreach (var person in Compile( 41 | "SELECT * FROM $ WHERE department = 'Engineering' AND isManager = true AND yearsAtCompany >= 5 AND salary > 170000").Execute(rows)) 42 | { 43 | Console.WriteLine($" -> {person.Name} ({person.City}) [{person.Department}], Years={person.YearsAtCompany}, Level={person.Level}"); 44 | } 45 | 46 | Console.WriteLine(); 47 | 48 | // 3) US-based senior+ ICs on Runtime or ML teams 49 | foreach (var name in Compile( 50 | "SELECT Name FROM $ WHERE country = 'US' AND (team = 'Runtime' OR team = 'ML') AND (level = 'Senior' OR level = 'Staff' OR level = 'Principal')").Execute(rows)) 51 | { 52 | Console.WriteLine($" -> {name}"); 53 | } 54 | 55 | Console.WriteLine(); 56 | 57 | // 4) Non-US employees 58 | foreach (var name in Compile("SELECT Name FROM $ WHERE NOT country = 'US'").Execute(rows)) 59 | { 60 | Console.WriteLine($" -> {name}"); 61 | } 62 | 63 | Console.WriteLine(); 64 | 65 | // 5) Project to a tuple with richer shape 66 | foreach (var (name, city, department, team, level) in Compile( 67 | "SELECT Name, City, Department, Team, Level FROM $ WHERE salary >= 195000 AND Team != null").Execute(rows)) 68 | { 69 | Console.WriteLine($" -> {name} ({city}) - {department}/{team ?? "Unset"} [{level}]"); 70 | } 71 | 72 | Console.WriteLine(); 73 | 74 | // 6) Double negation of predicates 75 | foreach (var name in Compile( 76 | "SELECT Name FROM $ WHERE NOT NOT country = 'US'").Execute(rows)) 77 | { 78 | Console.WriteLine($" -> {name}"); 79 | } 80 | 81 | Console.WriteLine(); 82 | 83 | // 7) De Morgan distribution over OR 84 | foreach (var name in Compile( 85 | "SELECT Name FROM $ WHERE NOT (team = 'Runtime' OR team = 'ML')").Execute(rows)) 86 | { 87 | Console.WriteLine($" -> {name}"); 88 | } 89 | 90 | Console.WriteLine(); 91 | 92 | // 8) Duplicate predicates in AND/OR chains 93 | foreach (var person in Compile( 94 | "SELECT * FROM $ WHERE department = 'Engineering' AND department = 'Engineering' AND (city = 'Seattle' OR city = 'Seattle')").Execute(rows)) 95 | { 96 | Console.WriteLine($" -> {person.Name} ({person.City}) [{person.Department}]"); 97 | } 98 | 99 | Console.WriteLine(); 100 | Console.WriteLine("All queries executed."); -------------------------------------------------------------------------------- /Runtime/Pipeline.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | 3 | namespace TypedSql.Runtime; 4 | 5 | internal static class Where 6 | { 7 | internal const int TRow = 0; 8 | internal const int TPredicate = 1; 9 | internal const int TNext = 2; 10 | internal const int TResult = 3; 11 | internal const int TRoot = 4; 12 | } 13 | 14 | internal static class Select 15 | { 16 | internal const int TRow = 0; 17 | internal const int TProjection = 1; 18 | internal const int TNext = 2; 19 | internal const int TMiddle = 3; 20 | internal const int TResult = 4; 21 | internal const int TRoot = 5; 22 | } 23 | 24 | internal static class WhereSelect 25 | { 26 | internal const int TRow = 0; 27 | internal const int TPredicate = 1; 28 | internal const int TProjection = 2; 29 | internal const int TNext = 3; 30 | internal const int TMiddle = 4; 31 | internal const int TResult = 5; 32 | internal const int TRoot = 6; 33 | } 34 | 35 | internal interface IQueryNode 36 | { 37 | abstract static void Run(ReadOnlySpan rows, scoped ref QueryRuntime runtime); 38 | 39 | abstract static void Process(in TRow row, scoped ref QueryRuntime runtime); 40 | } 41 | 42 | internal readonly struct Where : IQueryNode 43 | where TPredicate : IFilter 44 | where TNext : IQueryNode 45 | { 46 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 47 | public static void Run(ReadOnlySpan rows, scoped ref QueryRuntime runtime) 48 | { 49 | for (var i = 0; i < rows.Length; i++) 50 | { 51 | Process(in rows[i], ref runtime); 52 | } 53 | } 54 | 55 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 56 | public static void Process(in TRow row, scoped ref QueryRuntime runtime) 57 | { 58 | if (TPredicate.Evaluate(in row)) 59 | { 60 | TNext.Process(in row, ref runtime); 61 | } 62 | } 63 | } 64 | 65 | internal readonly struct Select : IQueryNode 66 | where TProjection : IProjection 67 | where TNext : IQueryNode 68 | { 69 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 70 | public static void Run(ReadOnlySpan rows, scoped ref QueryRuntime runtime) 71 | { 72 | for (var i = 0; i < rows.Length; i++) 73 | { 74 | Process(in rows[i], ref runtime); 75 | } 76 | } 77 | 78 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 79 | public static void Process(in TRow row, scoped ref QueryRuntime runtime) 80 | { 81 | var projected = TProjection.Project(in row); 82 | TNext.Process(in projected, ref runtime); 83 | } 84 | } 85 | 86 | internal readonly struct WhereSelect : IQueryNode 87 | where TPredicate : IFilter 88 | where TProjection : IProjection 89 | where TNext : IQueryNode 90 | { 91 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 92 | public static void Run(ReadOnlySpan rows, scoped ref QueryRuntime runtime) 93 | { 94 | for (var i = 0; i < rows.Length; i++) 95 | { 96 | Process(in rows[i], ref runtime); 97 | } 98 | } 99 | 100 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 101 | public static void Process(in TRow row, scoped ref QueryRuntime runtime) 102 | { 103 | if (TPredicate.Evaluate(in row)) 104 | { 105 | var projected = TProjection.Project(in row); 106 | TNext.Process(in projected, ref runtime); 107 | } 108 | } 109 | } 110 | 111 | internal readonly struct Stop : IQueryNode 112 | { 113 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 114 | public static void Run(ReadOnlySpan rows, scoped ref QueryRuntime runtime) 115 | { 116 | runtime.AddRange(rows); 117 | } 118 | 119 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 120 | public static void Process(in TResult row, scoped ref QueryRuntime runtime) 121 | { 122 | runtime.Add(in row); 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /DemoSchema.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | using TypedSql.Runtime; 3 | 4 | namespace TypedSql; 5 | 6 | internal readonly struct PersonIdColumn : IColumn 7 | { 8 | public static string Identifier => "id"; 9 | 10 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 11 | public static int Get(in Person row) => row.Id; 12 | } 13 | 14 | internal readonly struct PersonNameColumn : IColumn 15 | { 16 | public static string Identifier => "name"; 17 | 18 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 19 | public static string Get(in Person row) => row.Name; 20 | } 21 | 22 | internal readonly struct PersonAgeColumn : IColumn 23 | { 24 | public static string Identifier => "age"; 25 | 26 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 27 | public static int Get(in Person row) => row.Age; 28 | } 29 | 30 | internal readonly struct PersonCityColumn : IColumn 31 | { 32 | public static string Identifier => "city"; 33 | 34 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 35 | public static string Get(in Person row) => row.City; 36 | } 37 | 38 | internal readonly struct PersonSalaryColumn : IColumn 39 | { 40 | public static string Identifier => "salary"; 41 | 42 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 43 | public static float Get(in Person row) => row.Salary; 44 | } 45 | 46 | internal readonly struct PersonDepartmentColumn : IColumn 47 | { 48 | public static string Identifier => "department"; 49 | 50 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 51 | public static string Get(in Person row) => row.Department; 52 | } 53 | 54 | internal readonly struct PersonIsManagerColumn : IColumn 55 | { 56 | public static string Identifier => "isManager"; 57 | 58 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 59 | public static bool Get(in Person row) => row.IsManager; 60 | } 61 | 62 | internal readonly struct PersonYearsAtCompanyColumn : IColumn 63 | { 64 | public static string Identifier => "yearsAtCompany"; 65 | 66 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 67 | public static int Get(in Person row) => row.YearsAtCompany; 68 | } 69 | 70 | internal readonly struct PersonCountryColumn : IColumn 71 | { 72 | public static string Identifier => "country"; 73 | 74 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 75 | public static string Get(in Person row) => row.Country; 76 | } 77 | 78 | internal readonly struct PersonTeamColumn : IColumn 79 | { 80 | public static string Identifier => "team"; 81 | 82 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 83 | public static string? Get(in Person row) => row.Team; 84 | } 85 | 86 | internal readonly struct PersonLevelColumn : IColumn 87 | { 88 | public static string Identifier => "level"; 89 | 90 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 91 | public static string Get(in Person row) => row.Level; 92 | } 93 | 94 | 95 | internal static class DemoSchema 96 | { 97 | public static readonly IReadOnlyDictionary People = CreatePeople(); 98 | 99 | private static IReadOnlyDictionary CreatePeople() 100 | { 101 | return new Dictionary(StringComparer.OrdinalIgnoreCase) 102 | { 103 | [PersonIdColumn.Identifier] = new(PersonIdColumn.Identifier, typeof(PersonIdColumn), typeof(int)), 104 | [PersonNameColumn.Identifier] = new(PersonNameColumn.Identifier, typeof(PersonNameColumn), typeof(string)), 105 | [PersonAgeColumn.Identifier] = new(PersonAgeColumn.Identifier, typeof(PersonAgeColumn), typeof(int)), 106 | [PersonCityColumn.Identifier] = new(PersonCityColumn.Identifier, typeof(PersonCityColumn), typeof(string)), 107 | [PersonSalaryColumn.Identifier] = new(PersonSalaryColumn.Identifier, typeof(PersonSalaryColumn), typeof(float)), 108 | [PersonDepartmentColumn.Identifier] = new(PersonDepartmentColumn.Identifier, typeof(PersonDepartmentColumn), typeof(string)), 109 | [PersonIsManagerColumn.Identifier] = new(PersonIsManagerColumn.Identifier, typeof(PersonIsManagerColumn), typeof(bool)), 110 | [PersonYearsAtCompanyColumn.Identifier] = new(PersonYearsAtCompanyColumn.Identifier, typeof(PersonYearsAtCompanyColumn), typeof(int)), 111 | [PersonCountryColumn.Identifier] = new(PersonCountryColumn.Identifier, typeof(PersonCountryColumn), typeof(string)), 112 | [PersonTeamColumn.Identifier] = new(PersonTeamColumn.Identifier, typeof(PersonTeamColumn), typeof(string)), 113 | [PersonLevelColumn.Identifier] = new(PersonLevelColumn.Identifier, typeof(PersonLevelColumn), typeof(string)), 114 | }; 115 | } 116 | } 117 | 118 | public record struct Person( 119 | int Id, 120 | string Name, 121 | int Age, 122 | string City, 123 | float Salary, 124 | string Department, 125 | bool IsManager, 126 | int YearsAtCompany, 127 | string Country, 128 | string? Team, 129 | string Level); 130 | -------------------------------------------------------------------------------- /Runtime/QueryEngine.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics.CodeAnalysis; 2 | using System.Reflection; 3 | using System.Runtime.CompilerServices; 4 | using System.Text; 5 | 6 | namespace TypedSql.Runtime; 7 | 8 | public static class QueryEngine 9 | { 10 | public readonly struct CompiledQuery(MethodInfo executeMethod) 11 | { 12 | private readonly Func, IReadOnlyList> _entryPoint = executeMethod.CreateDelegate, IReadOnlyList>>(); 13 | 14 | public Type QueryPlan => executeMethod.DeclaringType!; 15 | 16 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 17 | public IReadOnlyList Execute(ReadOnlySpan rows) 18 | { 19 | return _entryPoint(rows); 20 | } 21 | 22 | public override string ToString() 23 | { 24 | return Visualize(QueryPlan, friendly: true); 25 | } 26 | } 27 | 28 | public static CompiledQuery Compile([StringSyntax("sql")] string sql, bool supportsAot = false) 29 | { 30 | var parsed = SqlParser.Parse(sql); 31 | var (pipelineType, runtimeResultType, publicResultType) = SqlCompiler.Compile(parsed, supportsAot); 32 | if (publicResultType != typeof(TResult)) 33 | { 34 | throw new InvalidOperationException($"Query result type '{publicResultType}' does not match '{typeof(TResult)}'."); 35 | } 36 | 37 | var programType = typeof(QueryProgram<,,,>).MakeGenericType(typeof(TRow), pipelineType, runtimeResultType, publicResultType); 38 | var executeMethod = programType.GetMethod("Execute", BindingFlags.Public | BindingFlags.Static) 39 | ?? throw new InvalidOperationException("Query program entry point is missing."); 40 | return CreateRunner(executeMethod); 41 | } 42 | 43 | public static IReadOnlyList Execute([StringSyntax("sql")] string sql, ReadOnlySpan rows) 44 | { 45 | return Compile(sql).Execute(rows); 46 | } 47 | 48 | private static CompiledQuery CreateRunner(MethodInfo executeMethod) 49 | { 50 | return new CompiledQuery(executeMethod); 51 | } 52 | 53 | internal static string Visualize(Type t, bool friendly = false) 54 | { 55 | if (!t.IsGenericType) return t.Name; 56 | if (friendly && t.GetInterfaces().Any(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(ILiteral<>))) return GetLiteralValue(t); 57 | var sb = new StringBuilder(); 58 | sb.Append($"{t.Name.AsSpan(0, t.Name.IndexOf('`'))}<"); 59 | var cnt = 0; 60 | foreach (var arg in t.GetGenericArguments()) 61 | { 62 | if (cnt > 0) sb.Append(", "); 63 | sb.Append(Visualize(arg, friendly)); 64 | cnt++; 65 | } 66 | return sb.Append('>').ToString(); 67 | } 68 | 69 | private static string GetLiteralValue(Type t) 70 | { 71 | if (t.GetInterfaces().Any(i => i == typeof(ILiteral))) 72 | { 73 | var literalValue = (ValueString)t.GetProperty(nameof(ILiteral<>.Value))!.GetValue(null)!; 74 | return literalValue.Value is null ? "null" : $"'{literalValue.Value.Replace("'", "''")}'"; 75 | } 76 | 77 | var hex = t.GetGenericArguments(); 78 | var num = 0; 79 | for (int i = 0; i < hex.Length; i++) 80 | { 81 | num <<= 4; 82 | num |= (int)hex[i].GetProperty(nameof(ILiteral<>.Value))!.GetValue(null)!; 83 | } 84 | var targetType = t.GetInterfaces().First(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(ILiteral<>)).GetGenericArguments()[0]; 85 | var converted = typeof(Unsafe).GetMethod(nameof(Unsafe.As), 2, BindingFlags.Public | BindingFlags.Static, [Type.MakeGenericMethodParameter(0).MakeByRefType()])!.MakeGenericMethod(typeof(int), targetType).Invoke(null, [num])!; 86 | return converted!.ToString()!; 87 | } 88 | 89 | } 90 | 91 | internal static class QueryProgram 92 | where TPipeline : IQueryNode 93 | { 94 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 95 | public static IReadOnlyList Execute(ReadOnlySpan rows) 96 | { 97 | var runtime = new QueryRuntime(rows.Length); 98 | TPipeline.Run(rows, ref runtime); 99 | 100 | return ConvertResult(ref runtime); 101 | } 102 | 103 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 104 | private static IReadOnlyList ConvertResult(ref QueryRuntime runtime) 105 | { 106 | if (typeof(IReadOnlyList) == typeof(IReadOnlyList)) 107 | { 108 | return (IReadOnlyList)(object)runtime.Rows; 109 | } 110 | else if (typeof(IReadOnlyList) == typeof(IReadOnlyList) && typeof(IReadOnlyList) == typeof(IReadOnlyList)) 111 | { 112 | return (IReadOnlyList)(object)runtime.AsStringRows(); 113 | } 114 | else if (RuntimeFeature.IsDynamicCodeSupported && typeof(TRuntimeResult).IsGenericType && typeof(TPublicResult).IsGenericType) 115 | { 116 | return runtime.AsValueTupleRows(); 117 | } 118 | return Throw(); 119 | } 120 | 121 | [MethodImpl(MethodImplOptions.NoInlining)] 122 | private static IReadOnlyList Throw() => throw new InvalidOperationException($"Cannot convert query result from '{typeof(TRuntimeResult)}' to '{typeof(TPublicResult)}'."); 123 | } 124 | -------------------------------------------------------------------------------- /README.ja.md: -------------------------------------------------------------------------------- 1 | # TypedSql 2 | 3 | TypedSql は、C# の型システムそのものを実行計画として使う、小さな実験的な SQL ライククエリエンジンです。各クエリは `Where` / `Select` / `Stop` などのノードからなる閉じたジェネリック型に変換され、ホットパスでは仮想呼び出しや式木インタープリタを挟まず、静的メソッドの呼び出しだけでさくっと実行されます。 4 | 5 | ## なぜ TypedSql か 6 | 7 | - **型レベルでの実行計画**: クエリの形状をジェネリック型としてエンコードすることで、JIT から見ると「普通のループ」に近いストレートなコードになります。 8 | - **カラムアクセスの構造体化**: 各カラムは `IColumn` 構造体として定義されます (`DemoSchema.cs` を参照)。フィールドアクセスがインライン展開されやすく、ボクシングも発生しません。 9 | - **リテラルの型持ち上げ**: `WHERE` 句内のリテラルは `Runtime/TypeLiterals.cs` 経由で `ILiteral` 型に持ち上げられ、実行時のパースを省きつつ、コンパイル時に値が見える形にします。 10 | - **ValueString による文字列の最適化**: ホットパスでは参照型ジェネリックを避けるため、内部的には文字列を `ValueString` に正規化しつつ、外からは薄いアダプタ経由で通常の `string` として扱えます。 11 | 12 | ## クイックスタート 13 | 14 | スキーマを一度登録し、クエリをコンパイルしてから、配列に対して実行します: 15 | 16 | ```csharp 17 | using TypedSql; 18 | using TypedSql.Runtime; 19 | 20 | SchemaRegistry.Register(DemoSchema.People); 21 | 22 | var rows = new[] 23 | { 24 | new Person(1, "Ada", 34, "Seattle", 180_000f, "Engineering", true, 6, "US", "Runtime", "Senior"), 25 | new Person(2, "Barbara", 28, "Boston", 150_000f, "Engineering", false, 3, "US", "Compiler", "Mid"), 26 | new Person(3, "Charles", 44, "Helsinki", 210_000f, "Research", true, 15, "FI", "ML", "Principal"), 27 | new Person(4, "David", 31, "Palo Alto", 195_000f, "Product", false, 4, "US", "Runtime", "Senior"), 28 | new Person(5, "Eve", 39, "Seattle", 220_000f, "Product", true, 10, "US", "ML", "Staff"), 29 | }; 30 | 31 | // よくある「社員テーブル」の少し複雑なクエリ例 (US 拠点のエンジニアリングマネージャ) 32 | var query = QueryEngine.Compile( 33 | "SELECT * FROM $ WHERE department = 'Engineering' AND isManager = true AND yearsAtCompany >= 5 AND salary > 170000 AND country = 'US'"); 34 | 35 | foreach (var person in query.Execute(rows)) 36 | { 37 | Console.WriteLine($" -> {person.Name} ({person.City}) [{person.Department}/{person.Team}] {person.Level}, Years={person.YearsAtCompany}, Manager={person.IsManager}"); 38 | } 39 | ``` 40 | 41 | ## アーキテクチャ概要 42 | 43 | TypedSql は、簡易な SQL をパースしてジェネリックなパイプライン型にコンパイルし、そのパイプラインをプレーンなインメモリ行に対して実行します。 44 | 45 | - **スキーマ**: 行型に対してどのカラムが存在するかを表します。 46 | - **パーサー / コンパイラ**: SQL 文字列を具体的なパイプライン型に変換します。 47 | - **ランタイム**: 結果バッファを管理し、パイプラインの静的メソッドを呼び出します。 48 | 49 | より詳しい仕組みは、後述の「Deep dive」セクションをのぞいてみてください。 50 | 51 | ## 対応している SQL サーフェス 52 | 53 | - `SELECT * FROM $` : 元の行型 (`Person`) を返す。 54 | - `SELECT FROM $` : 単一カラムを返す (`string` / 数値など)。 55 | - `SELECT col1, col2, ... FROM $` : 複数カラムを C# のタプル `(T1, T2, ...)` として返す。 56 | - `WHERE ` : `=`, `!=`, `>`, `<`, `>=`, `<=`。 57 | - ブール演算子: `AND`, `OR`, `NOT`、および `()` によるグルーピング。 58 | - リテラル: 整数 (`42`)、浮動小数 (`123.45`)、ブール値 (`true` / `false`)、シングルクォート文字列 (`'Seattle'`、エスケープは `''`)、ヌル文字列 (`null`)。 59 | - カラム識別子は大文字小文字を区別しません。 60 | - 新しい式、演算子、リテラル、カラムを簡単に拡張できます。 61 | 62 | `Program.cs` では、上記の演算子を一通り使ったクエリ例をいくつも実行しています。 63 | 64 | ## デモ実行 65 | 66 | ```pwsh 67 | dotnet run -c Release 68 | ``` 69 | 70 | ## ベンチマーク 71 | 72 | BenchmarkDotNet で TypedSql のクエリと LINQ や手書きループによる同等の処理を、同じインメモリデータに対して比べて、「`City == "Seattle"` の行を除外し、該当する `Id` を集める」という処理は以下のような結果が出ました: 73 | 74 | | Method | Mean | Error | StdDev | Ratio | RatioSD | Gen0 | Code Size | Allocated | Alloc Ratio | 75 | |--------- |----------:|----------:|----------:|------:|--------:|-------:|----------:|----------:|------------:| 76 | | TypedSql | 10.093 ns | 0.2519 ns | 0.3185 ns | 1.21 | 0.05 | 0.0046 | 666 B | 72 B | 1.00 | 77 | | Linq | 27.449 ns | 0.5885 ns | 0.7442 ns | 3.28 | 0.12 | 0.0127 | 3,769 B | 200 B | 2.78 | 78 | | Foreach | 8.364 ns | 0.2126 ns | 0.2274 ns | 1.00 | 0.04 | 0.0046 | 409 B | 72 B | 1.00 | 79 | 80 | TypedSql と手書きの `foreach` ループは、だいたい同じくらいの速さとメモリ割り当てになっているのに対して、LINQ は少し重めで、実行時間もメモリも多めにかかっています。 81 | 82 | ## スキーマを拡張するには 83 | 84 | `DemoSchema.cs` に新しい `IColumn` 実装を追加し、`People` 辞書に登録したうえで: 85 | 86 | ```csharp 87 | SchemaRegistry.Register(DemoSchema.People); 88 | ``` 89 | 90 | を維持したままクエリ文字列に新しいカラム名を使えば、エンジン側の変更なしでそのまま利用できます。 91 | 92 | ## Deep dive 93 | 94 | ### スキーマ登録 95 | 96 | 行は `Person` のような単純な record/struct です。各カラムは `IColumn` を実装し、一意な `Identifier` を公開します。`SchemaRegistry` は `ColumnMetadata` の大文字小文字を区別しない辞書を持ち、コンパイル時にカラム名から具体的なカラム型とゲッターを引き当てます。 97 | 98 | `DemoSchema.cs` では、次のようなカラムが定義されています。 99 | 100 | - `PersonIdColumn` / `PersonNameColumn` / `PersonAgeColumn` / `PersonCityColumn` / `PersonSalaryColumn` 101 | - `PersonDepartmentColumn` / `PersonIsManagerColumn` / `PersonYearsAtCompanyColumn` 102 | - `PersonCountryColumn` / `PersonTeamColumn` / `PersonLevelColumn` 103 | 104 | これらを `SchemaRegistry.Register(DemoSchema.People);` で一括登録します。 105 | 106 | ### パースとコンパイル 107 | 108 | 1. `Runtime/SqlParser.cs` が簡易 SQL (`SELECT`, `FROM $`, `WHERE` など) をトークナイズ・構文解析します。 109 | 2. `Runtime/SqlCompiler.cs` がスキーマ情報を参照しつつリテラル型を組み立て、`Runtime/Pipeline.cs` のノードからなる型レベルパイプラインを構築します。 110 | 3. 可能な場合、連続する `Where` と `Select` は `WhereSelect` にまとめられ、データ走査回数を減らします。 111 | 4. 最終的なパイプライン型は `QueryProgram` に差し込まれ、その `Execute` が実行エントリポイントになります。 112 | 113 | ### パイプラインノード 114 | 115 | - `Where`: `IFilter` を実装した述語型を評価します。 116 | - `Select`: `IProjection` 実装 (例: `ColumnProjection`) を用いて次のシェイプを生成します。 117 | - `WhereSelect`: 直後の `Select` と結合されたフィルタ兼射影ノードです。 118 | - `Stop`: 終端ノードで、`QueryRuntime` のバッファに結果を積み上げます。 119 | 120 | 各ノードは静的な `Run` / `Process` メソッドを公開しており、一度 `QueryProgram` が組み上がると、クエリ実行はネストしたジェネリック型上の静的メソッド呼び出しの組み合わせとして表現されます。 121 | 122 | ### ランタイム実行 123 | 124 | - `Runtime/QueryRuntime.cs` が結果バッファを管理します。 125 | - `QueryProgram.Execute` がランタイムを生成し、パイプラインの `Run` を呼び出し、公開結果型 (元の行/プリミティブ/文字列/タプルなど) への最小限の変換を行います。 126 | - `Runtime/QueryEngine.cs` は一度だけ反射で `Execute` メソッドを取得し、delegate* に変換したうえで以降は直接呼び出します。 127 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TypedSql 2 | 3 | TypedSql is a small experimental SQL-like query engine that leans on the C# type system as its execution plan. Each query turns into a closed generic type built from `Where` / `Select` / `Stop` nodes and runs entirely through static methods, so there’s no virtual dispatch or expression-tree interpretation sitting in the hot path. 4 | 5 | ## Why TypedSql 6 | 7 | - **Execution plan in types**: The query shape is encoded in generic types, so from the JIT’s point of view execution looks like an ordinary straight-line loop instead of a dynamic interpreter. 8 | - **Struct-based column access**: Columns are implemented as `IColumn` structs (see `DemoSchema.cs`), which makes inlining predictable and avoids boxing when reading fields. 9 | - **Lifted literals**: Literals in `WHERE` clauses are turned into `ILiteral` types via `Runtime/TypeLiterals.cs`, so there is no runtime parsing and the JIT can see literal values at compile time. 10 | - **ValueString for strings**: Hot paths avoid reference-type generics by normalizing strings to `ValueString` internally, while callers still see ordinary `string` values through a thin adapter. 11 | 12 | ## Quick start 13 | 14 | Register the schema once, compile a query, and then run it over an array of rows: 15 | 16 | ```csharp 17 | using TypedSql; 18 | using TypedSql.Runtime; 19 | 20 | SchemaRegistry.Register(DemoSchema.People); 21 | 22 | var rows = new[] 23 | { 24 | new Person(1, "Ada", 34, "Seattle", 180_000f, "Engineering", true, 6, "US", "Runtime", "Senior"), 25 | new Person(2, "Barbara", 28, "Boston", 150_000f, "Engineering", false, 3, "US", "Compiler", "Mid"), 26 | new Person(3, "Charles", 44, "Helsinki", 210_000f, "Research", true, 15, "FI", "ML", "Principal"), 27 | new Person(4, "David", 31, "Palo Alto", 195_000f, "Product", false, 4, "US", "Runtime", "Senior"), 28 | new Person(5, "Eve", 39, "Seattle", 220_000f, "Product", true, 10, "US", "ML", "Staff"), 29 | }; 30 | 31 | // Find well‑paid senior engineering managers in the US 32 | var query = QueryEngine.Compile( 33 | "SELECT * FROM $ WHERE department = 'Engineering' AND isManager = true AND yearsAtCompany >= 5 AND salary > 170000 AND country = 'US'"); 34 | 35 | foreach (var person in query.Execute(rows)) 36 | { 37 | Console.WriteLine($" -> {person.Name} ({person.City}) [{person.Department}/{person.Team}] {person.Level}, Years={person.YearsAtCompany}, Manager={person.IsManager}"); 38 | } 39 | ``` 40 | 41 | ## Architecture Overview 42 | At a high level, TypedSql parses a small SQL subset, compiles it into a chain of generic pipeline nodes, and then runs that pipeline over plain in-memory rows. 43 | 44 | - A **schema** describes which columns exist for a given row type. 45 | - The **parser/compiler** translate SQL into a concrete generic pipeline type. 46 | - The **runtime** owns the result buffer and calls the pipeline’s static methods. 47 | 48 | If you want to peek under the hood in more detail, check out the **Deep dive** section below. 49 | 50 | ## Supported SQL Surface 51 | 52 | - `SELECT * FROM $`: returns the original row type (e.g., `Person`). 53 | - `SELECT FROM $`: returns a single column (`string`, numeric, etc.). 54 | - `SELECT col1, col2, ... FROM $`: returns multiple columns as a C# tuple `(T1, T2, ...)`. 55 | - `WHERE `: comparison operators `=`, `!=`, `>`, `<`, `>=`, `<=`. 56 | - Boolean operators: `AND`, `OR`, `NOT`, and grouping with `()`. 57 | - Literals: integers (`42`), floats (`123.45`), booleans (`true` / `false`), single-quoted strings (`'Seattle'` with `''` as the escape) and `null` strings. 58 | - Column identifiers are case-insensitive. 59 | - Easily extensible for new expressions, operators, literals, and columns. 60 | 61 | `Program.cs` shows many of these operators in action with a bunch of small example queries. 62 | 63 | ## Running the demo 64 | 65 | ```pwsh 66 | dotnet run -c Release 67 | ``` 68 | 69 | ## Benchmarks 70 | Compare a simple TypedSql query against equivalent LINQ and handwritten loops over the same in-memory data. Filtering out rows where `City == "Seattle"` and returning the matching `Id` values, produced numbers like these: 71 | 72 | | Method | Mean | Error | StdDev | Gen0 | Code Size | Allocated | 73 | |--------- |----------:|----------:|----------:|-------:|----------:|----------:| 74 | | TypedSql | 10.953 ns | 0.0250 ns | 0.0195 ns | 0.0051 | 111 B | 80 B | 75 | | Linq | 27.030 ns | 0.1277 ns | 0.1067 ns | 0.0148 | 3,943 B | 232 B | 76 | | Foreach | 9.429 ns | 0.0417 ns | 0.0326 ns | 0.0046 | 407 B | 72 B | 77 | 78 | TypedSql and the handwritten `foreach` loop end up with very similar throughput and allocation, while the LINQ query is noticeably slower and allocates more. 79 | 80 | ## Extending the schema 81 | 82 | To extend the schema, add new `IColumn` implementations to `DemoSchema.cs`, register them in the `People` dictionary, and keep: 83 | 84 | ```csharp 85 | SchemaRegistry.Register(DemoSchema.People); 86 | ``` 87 | 88 | in place. Once registered, you can use the new column names in your SQL strings without modifying the engine itself. 89 | 90 | ## Deep dive 91 | 92 | ### Schema registration 93 | 94 | Rows are simple records/structs such as `Person`. Each column implements `IColumn` and exposes a stable `Identifier`. `SchemaRegistry` holds a case-insensitive dictionary of `ColumnMetadata` and uses it to resolve column names to concrete column types and getters at compile time. 95 | 96 | `DemoSchema.cs` defines the following columns: 97 | 98 | - `PersonIdColumn` / `PersonNameColumn` / `PersonAgeColumn` / `PersonCityColumn` / `PersonSalaryColumn` 99 | - `PersonDepartmentColumn` / `PersonIsManagerColumn` / `PersonYearsAtCompanyColumn` 100 | - `PersonCountryColumn` / `PersonTeamColumn` / `PersonLevelColumn` 101 | 102 | They are registered with: 103 | 104 | ```csharp 105 | SchemaRegistry.Register(DemoSchema.People); 106 | ``` 107 | 108 | ### Parsing and compilation 109 | 110 | 1. `Runtime/SqlParser.cs` tokenizes and parses a small SQL subset (`SELECT`, `FROM $`, `WHERE`, etc.). 111 | 2. `Runtime/SqlCompiler.cs` looks up schema information, constructs literal types, and builds a type-level pipeline using nodes from `Runtime/Pipeline.cs`. 112 | 3. When possible, consecutive `Where` and `Select` nodes are fused into a single `WhereSelect` node to reduce passes over the data. 113 | 4. The final pipeline type is plugged into `QueryProgram`, whose `Execute` method becomes the single entry point. 114 | 115 | ### Pipeline nodes 116 | 117 | - `Where`: evaluates predicates implemented as `IFilter`. 118 | - `Select`: uses `IProjection` implementations (for example, `ColumnProjection`) to produce the next shape in the pipeline. 119 | - `WhereSelect`: a combined filter+projection node used when a `Where` is directly followed by a `Select`. 120 | - `Stop`: the terminal node, which pushes results into the `QueryRuntime` buffer. 121 | 122 | Each node exposes static `Run` / `Process` methods. Once a `QueryProgram` is assembled, running a query is essentially a set of static method calls on nested generic types. 123 | 124 | ### Runtime execution 125 | 126 | - `Runtime/QueryRuntime.cs` owns and manages the result buffer. 127 | - `QueryProgram.Execute` creates the runtime, calls the pipeline’s `Run`, and performs minimal conversion to the public result type (original rows, primitives, strings, or tuples). 128 | - `Runtime/QueryEngine.cs` locates `Execute` via reflection once, converts it to a function pointer (`delegate*`), and subsequent executions call it directly. 129 | -------------------------------------------------------------------------------- /Runtime/ValueTupleConvertHelper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Reflection.Emit; 4 | using System.Runtime.CompilerServices; 5 | using System.Text; 6 | 7 | namespace TypedSql.Runtime; 8 | 9 | internal static class ValueTupleConvertHelper 10 | { 11 | private delegate void CopyDelegate(ref TPublicResult dest, ref readonly TRuntimeResult source); 12 | 13 | private static readonly CopyDelegate _helper = default!; 14 | 15 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 16 | public static void Copy(ref TPublicResult dest, ref readonly TRuntimeResult source) 17 | { 18 | if (typeof(TPublicResult) == typeof(TRuntimeResult)) 19 | { 20 | dest = Unsafe.As(ref Unsafe.AsRef(in source)); 21 | } 22 | else 23 | { 24 | _helper.Invoke(ref dest, in source); 25 | } 26 | } 27 | 28 | static ValueTupleConvertHelper() 29 | { 30 | DynamicMethod dm = new($"ValueTupleConvertHelper+{typeof(TPublicResult)}_{typeof(TRuntimeResult)}", typeof(void), [typeof(TPublicResult).MakeByRefType(), typeof(TRuntimeResult).MakeByRefType()], true); 31 | ILGenerator ilGen = dm.GetILGenerator(); 32 | if (typeof(TPublicResult) == typeof(TRuntimeResult)) 33 | { 34 | ilGen.Emit(OpCodes.Ldarg_0); 35 | ilGen.Emit(OpCodes.Ldarg_1); 36 | ilGen.Emit(OpCodes.Cpobj, typeof(TPublicResult)); 37 | ilGen.Emit(OpCodes.Ret); 38 | return; 39 | } 40 | var tmp1 = ilGen.DeclareLocal(typeof(byte).MakeByRefType()); 41 | var tmp2 = ilGen.DeclareLocal(typeof(byte).MakeByRefType()); 42 | ilGen.Emit(OpCodes.Ldarg_0); 43 | ilGen.Emit(OpCodes.Stloc, tmp1); 44 | ilGen.Emit(OpCodes.Ldarg_1); 45 | ilGen.Emit(OpCodes.Stloc, tmp2); 46 | TypeHelper(typeof(TPublicResult), typeof(TRuntimeResult)); 47 | void TypeHelper(Type t1, Type t2) 48 | { 49 | var gDefn = t1.GetGenericTypeDefinition(); 50 | if (gDefn != t2.GetGenericTypeDefinition() || !gDefn.IsGenericType) 51 | { 52 | throw new InvalidOperationException($"Cannot convert between types '{typeof(TPublicResult)}' and '{typeof(TRuntimeResult)}'."); 53 | } 54 | if (gDefn == typeof(ValueTuple<>)) 55 | { 56 | FieldHelper(t1, t2, nameof(ValueTuple<>.Item1)); 57 | } 58 | else if (gDefn == typeof(ValueTuple<,>)) 59 | { 60 | FieldHelper(t1, t2, nameof(ValueTuple<,>.Item1)); 61 | FieldHelper(t1, t2, nameof(ValueTuple<,>.Item2)); 62 | } 63 | else if (gDefn == typeof(ValueTuple<,,>)) 64 | { 65 | FieldHelper(t1, t2, nameof(ValueTuple<,,>.Item1)); 66 | FieldHelper(t1, t2, nameof(ValueTuple<,,>.Item2)); 67 | FieldHelper(t1, t2, nameof(ValueTuple<,,>.Item3)); 68 | } 69 | else if (gDefn == typeof(ValueTuple<,,,>)) 70 | { 71 | FieldHelper(t1, t2, nameof(ValueTuple<,,,>.Item1)); 72 | FieldHelper(t1, t2, nameof(ValueTuple<,,,>.Item2)); 73 | FieldHelper(t1, t2, nameof(ValueTuple<,,,>.Item3)); 74 | FieldHelper(t1, t2, nameof(ValueTuple<,,,>.Item4)); 75 | } 76 | else if (gDefn == typeof(ValueTuple<,,,,>)) 77 | { 78 | FieldHelper(t1, t2, nameof(ValueTuple<,,,,>.Item1)); 79 | FieldHelper(t1, t2, nameof(ValueTuple<,,,,>.Item2)); 80 | FieldHelper(t1, t2, nameof(ValueTuple<,,,,>.Item3)); 81 | FieldHelper(t1, t2, nameof(ValueTuple<,,,,>.Item4)); 82 | FieldHelper(t1, t2, nameof(ValueTuple<,,,,>.Item5)); 83 | } 84 | else if (gDefn == typeof(ValueTuple<,,,,,>)) 85 | { 86 | FieldHelper(t1, t2, nameof(ValueTuple<,,,,,>.Item1)); 87 | FieldHelper(t1, t2, nameof(ValueTuple<,,,,,>.Item2)); 88 | FieldHelper(t1, t2, nameof(ValueTuple<,,,,,>.Item3)); 89 | FieldHelper(t1, t2, nameof(ValueTuple<,,,,,>.Item4)); 90 | FieldHelper(t1, t2, nameof(ValueTuple<,,,,,>.Item5)); 91 | FieldHelper(t1, t2, nameof(ValueTuple<,,,,,>.Item6)); 92 | } 93 | else if (gDefn == typeof(ValueTuple<,,,,,,>)) 94 | { 95 | FieldHelper(t1, t2, nameof(ValueTuple<,,,,,,>.Item1)); 96 | FieldHelper(t1, t2, nameof(ValueTuple<,,,,,,>.Item2)); 97 | FieldHelper(t1, t2, nameof(ValueTuple<,,,,,,>.Item3)); 98 | FieldHelper(t1, t2, nameof(ValueTuple<,,,,,,>.Item4)); 99 | FieldHelper(t1, t2, nameof(ValueTuple<,,,,,,>.Item5)); 100 | FieldHelper(t1, t2, nameof(ValueTuple<,,,,,,>.Item6)); 101 | FieldHelper(t1, t2, nameof(ValueTuple<,,,,,,>.Item7)); 102 | } 103 | else if (gDefn == typeof(ValueTuple<,,,,,,,>)) 104 | { 105 | FieldHelper(t1, t2, nameof(ValueTuple<,,,,,,,>.Item1)); 106 | FieldHelper(t1, t2, nameof(ValueTuple<,,,,,,,>.Item2)); 107 | FieldHelper(t1, t2, nameof(ValueTuple<,,,,,,,>.Item3)); 108 | FieldHelper(t1, t2, nameof(ValueTuple<,,,,,,,>.Item4)); 109 | FieldHelper(t1, t2, nameof(ValueTuple<,,,,,,,>.Item5)); 110 | FieldHelper(t1, t2, nameof(ValueTuple<,,,,,,,>.Item6)); 111 | FieldHelper(t1, t2, nameof(ValueTuple<,,,,,,,>.Item7)); 112 | var rest1 = t1.GetField(nameof(ValueTuple<,,,,,,,>.Rest))!; 113 | var rest2 = t2.GetField(nameof(ValueTuple<,,,,,,,>.Rest))!; 114 | ilGen.Emit(OpCodes.Ldloc, tmp1); 115 | ilGen.Emit(OpCodes.Ldflda, rest1); 116 | ilGen.Emit(OpCodes.Stloc, tmp1); 117 | ilGen.Emit(OpCodes.Ldloc, tmp2); 118 | ilGen.Emit(OpCodes.Ldflda, rest2); 119 | ilGen.Emit(OpCodes.Stloc, tmp2); 120 | TypeHelper(rest1.FieldType, rest2.FieldType); 121 | } 122 | else 123 | { 124 | throw new InvalidOperationException($"Cannot convert between types '{typeof(TPublicResult)}' and '{typeof(TRuntimeResult)}'."); 125 | } 126 | } 127 | 128 | void FieldHelper(Type t1, Type t2, string fieldName) 129 | { 130 | var f1 = t1.GetField(fieldName)!; 131 | var f2 = t2.GetField(fieldName)!; 132 | if (f1.FieldType == f2.FieldType) 133 | { 134 | ilGen.Emit(OpCodes.Ldloc, tmp1); 135 | ilGen.Emit(OpCodes.Ldloc, tmp2); 136 | ilGen.Emit(OpCodes.Ldfld, f2); 137 | ilGen.Emit(OpCodes.Stfld, f1); 138 | } 139 | else if (f1.FieldType == typeof(string) && f2.FieldType == typeof(ValueString)) 140 | { 141 | ilGen.Emit(OpCodes.Ldloc, tmp1); 142 | ilGen.Emit(OpCodes.Ldloc, tmp2); 143 | ilGen.Emit(OpCodes.Ldflda, f2); 144 | ilGen.Emit(OpCodes.Ldfld, typeof(ValueString).GetField(nameof(ValueString.Value))!); 145 | ilGen.Emit(OpCodes.Stfld, f1); 146 | } 147 | else if (f1.FieldType == typeof(ValueString) && f2.FieldType == typeof(string)) 148 | { 149 | ilGen.Emit(OpCodes.Ldloc, tmp1); 150 | ilGen.Emit(OpCodes.Ldloc, tmp2); 151 | ilGen.Emit(OpCodes.Ldfld, f2); 152 | ilGen.Emit(OpCodes.Newobj, typeof(ValueString).GetConstructor([typeof(string)])!); 153 | ilGen.Emit(OpCodes.Stfld, f1); 154 | } 155 | } 156 | 157 | ilGen.Emit(OpCodes.Ret); 158 | _helper = dm.CreateDelegate(); 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.rsuser 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Mono auto generated files 17 | mono_crash.* 18 | 19 | # Build results 20 | [Dd]ebug/ 21 | [Dd]ebugPublic/ 22 | [Rr]elease/ 23 | [Rr]eleases/ 24 | x64/ 25 | x86/ 26 | [Ww][Ii][Nn]32/ 27 | [Aa][Rr][Mm]/ 28 | [Aa][Rr][Mm]64/ 29 | bld/ 30 | [Bb]in/ 31 | [Oo]bj/ 32 | [Oo]ut/ 33 | [Ll]og/ 34 | [Ll]ogs/ 35 | 36 | # Visual Studio 2015/2017 cache/options directory 37 | .vs/ 38 | # Uncomment if you have tasks that create the project's static files in wwwroot 39 | #wwwroot/ 40 | 41 | # Visual Studio 2017 auto generated files 42 | Generated\ Files/ 43 | 44 | # MSTest test Results 45 | [Tt]est[Rr]esult*/ 46 | [Bb]uild[Ll]og.* 47 | 48 | # NUnit 49 | *.VisualState.xml 50 | TestResult.xml 51 | nunit-*.xml 52 | 53 | # Build Results of an ATL Project 54 | [Dd]ebugPS/ 55 | [Rr]eleasePS/ 56 | dlldata.c 57 | 58 | # Benchmark Results 59 | BenchmarkDotNet.Artifacts/ 60 | 61 | # .NET Core 62 | project.lock.json 63 | project.fragment.lock.json 64 | artifacts/ 65 | 66 | # ASP.NET Scaffolding 67 | ScaffoldingReadMe.txt 68 | 69 | # StyleCop 70 | StyleCopReport.xml 71 | 72 | # Files built by Visual Studio 73 | *_i.c 74 | *_p.c 75 | *_h.h 76 | *.ilk 77 | *.meta 78 | *.obj 79 | *.iobj 80 | *.pch 81 | *.pdb 82 | *.ipdb 83 | *.pgc 84 | *.pgd 85 | *.rsp 86 | *.sbr 87 | *.tlb 88 | *.tli 89 | *.tlh 90 | *.tmp 91 | *.tmp_proj 92 | *_wpftmp.csproj 93 | *.log 94 | *.vspscc 95 | *.vssscc 96 | .builds 97 | *.pidb 98 | *.svclog 99 | *.scc 100 | 101 | # Chutzpah Test files 102 | _Chutzpah* 103 | 104 | # Visual C++ cache files 105 | ipch/ 106 | *.aps 107 | *.ncb 108 | *.opendb 109 | *.opensdf 110 | *.sdf 111 | *.cachefile 112 | *.VC.db 113 | *.VC.VC.opendb 114 | 115 | # Visual Studio profiler 116 | *.psess 117 | *.vsp 118 | *.vspx 119 | *.sap 120 | 121 | # Visual Studio Trace Files 122 | *.e2e 123 | 124 | # TFS 2012 Local Workspace 125 | $tf/ 126 | 127 | # Guidance Automation Toolkit 128 | *.gpState 129 | 130 | # ReSharper is a .NET coding add-in 131 | _ReSharper*/ 132 | *.[Rr]e[Ss]harper 133 | *.DotSettings.user 134 | 135 | # TeamCity is a build add-in 136 | _TeamCity* 137 | 138 | # DotCover is a Code Coverage Tool 139 | *.dotCover 140 | 141 | # AxoCover is a Code Coverage Tool 142 | .axoCover/* 143 | !.axoCover/settings.json 144 | 145 | # Coverlet is a free, cross platform Code Coverage Tool 146 | coverage*.json 147 | coverage*.xml 148 | coverage*.info 149 | 150 | # Visual Studio code coverage results 151 | *.coverage 152 | *.coveragexml 153 | 154 | # NCrunch 155 | _NCrunch_* 156 | .*crunch*.local.xml 157 | nCrunchTemp_* 158 | 159 | # MightyMoose 160 | *.mm.* 161 | AutoTest.Net/ 162 | 163 | # Web workbench (sass) 164 | .sass-cache/ 165 | 166 | # Installshield output folder 167 | [Ee]xpress/ 168 | 169 | # DocProject is a documentation generator add-in 170 | DocProject/buildhelp/ 171 | DocProject/Help/*.HxT 172 | DocProject/Help/*.HxC 173 | DocProject/Help/*.hhc 174 | DocProject/Help/*.hhk 175 | DocProject/Help/*.hhp 176 | DocProject/Help/Html2 177 | DocProject/Help/html 178 | 179 | # Click-Once directory 180 | publish/ 181 | 182 | # Publish Web Output 183 | *.[Pp]ublish.xml 184 | *.azurePubxml 185 | # Note: Comment the next line if you want to checkin your web deploy settings, 186 | # but database connection strings (with potential passwords) will be unencrypted 187 | *.pubxml 188 | *.publishproj 189 | 190 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 191 | # checkin your Azure Web App publish settings, but sensitive information contained 192 | # in these scripts will be unencrypted 193 | PublishScripts/ 194 | 195 | # NuGet Packages 196 | *.nupkg 197 | # NuGet Symbol Packages 198 | *.snupkg 199 | # The packages folder can be ignored because of Package Restore 200 | **/[Pp]ackages/* 201 | # except build/, which is used as an MSBuild target. 202 | !**/[Pp]ackages/build/ 203 | # Uncomment if necessary however generally it will be regenerated when needed 204 | #!**/[Pp]ackages/repositories.config 205 | # NuGet v3's project.json files produces more ignorable files 206 | *.nuget.props 207 | *.nuget.targets 208 | 209 | # Microsoft Azure Build Output 210 | csx/ 211 | *.build.csdef 212 | 213 | # Microsoft Azure Emulator 214 | ecf/ 215 | rcf/ 216 | 217 | # Windows Store app package directories and files 218 | AppPackages/ 219 | BundleArtifacts/ 220 | Package.StoreAssociation.xml 221 | _pkginfo.txt 222 | *.appx 223 | *.appxbundle 224 | *.appxupload 225 | 226 | # Visual Studio cache files 227 | # files ending in .cache can be ignored 228 | *.[Cc]ache 229 | # but keep track of directories ending in .cache 230 | !?*.[Cc]ache/ 231 | 232 | # Others 233 | ClientBin/ 234 | ~$* 235 | *~ 236 | *.dbmdl 237 | *.dbproj.schemaview 238 | *.jfm 239 | *.pfx 240 | *.publishsettings 241 | orleans.codegen.cs 242 | 243 | # Including strong name files can present a security risk 244 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 245 | #*.snk 246 | 247 | # Since there are multiple workflows, uncomment next line to ignore bower_components 248 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 249 | #bower_components/ 250 | 251 | # RIA/Silverlight projects 252 | Generated_Code/ 253 | 254 | # Backup & report files from converting an old project file 255 | # to a newer Visual Studio version. Backup files are not needed, 256 | # because we have git ;-) 257 | _UpgradeReport_Files/ 258 | Backup*/ 259 | UpgradeLog*.XML 260 | UpgradeLog*.htm 261 | ServiceFabricBackup/ 262 | *.rptproj.bak 263 | 264 | # SQL Server files 265 | *.mdf 266 | *.ldf 267 | *.ndf 268 | 269 | # Business Intelligence projects 270 | *.rdl.data 271 | *.bim.layout 272 | *.bim_*.settings 273 | *.rptproj.rsuser 274 | *- [Bb]ackup.rdl 275 | *- [Bb]ackup ([0-9]).rdl 276 | *- [Bb]ackup ([0-9][0-9]).rdl 277 | 278 | # Microsoft Fakes 279 | FakesAssemblies/ 280 | 281 | # GhostDoc plugin setting file 282 | *.GhostDoc.xml 283 | 284 | # Node.js Tools for Visual Studio 285 | .ntvs_analysis.dat 286 | node_modules/ 287 | 288 | # Visual Studio 6 build log 289 | *.plg 290 | 291 | # Visual Studio 6 workspace options file 292 | *.opt 293 | 294 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 295 | *.vbw 296 | 297 | # Visual Studio LightSwitch build output 298 | **/*.HTMLClient/GeneratedArtifacts 299 | **/*.DesktopClient/GeneratedArtifacts 300 | **/*.DesktopClient/ModelManifest.xml 301 | **/*.Server/GeneratedArtifacts 302 | **/*.Server/ModelManifest.xml 303 | _Pvt_Extensions 304 | 305 | # Paket dependency manager 306 | .paket/paket.exe 307 | paket-files/ 308 | 309 | # FAKE - F# Make 310 | .fake/ 311 | 312 | # CodeRush personal settings 313 | .cr/personal 314 | 315 | # Python Tools for Visual Studio (PTVS) 316 | __pycache__/ 317 | *.pyc 318 | 319 | # Cake - Uncomment if you are using it 320 | # tools/** 321 | # !tools/packages.config 322 | 323 | # Tabs Studio 324 | *.tss 325 | 326 | # Telerik's JustMock configuration file 327 | *.jmconfig 328 | 329 | # BizTalk build output 330 | *.btp.cs 331 | *.btm.cs 332 | *.odx.cs 333 | *.xsd.cs 334 | 335 | # OpenCover UI analysis results 336 | OpenCover/ 337 | 338 | # Azure Stream Analytics local run output 339 | ASALocalRun/ 340 | 341 | # MSBuild Binary and Structured Log 342 | *.binlog 343 | 344 | # NVidia Nsight GPU debugger configuration file 345 | *.nvuser 346 | 347 | # MFractors (Xamarin productivity tool) working folder 348 | .mfractor/ 349 | 350 | # Local History for Visual Studio 351 | .localhistory/ 352 | 353 | # BeatPulse healthcheck temp database 354 | healthchecksdb 355 | 356 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 357 | MigrationBackup/ 358 | 359 | # Ionide (cross platform F# VS Code tools) working folder 360 | .ionide/ 361 | 362 | # Fody - auto-generated XML schema 363 | FodyWeavers.xsd -------------------------------------------------------------------------------- /Runtime/TypeLiterals.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | 3 | namespace TypedSql.Runtime; 4 | 5 | internal interface IHex 6 | { 7 | abstract static int Value { get; } 8 | } 9 | 10 | internal readonly struct Hex0 : IHex { public static int Value => 0; } 11 | internal readonly struct Hex1 : IHex { public static int Value => 1; } 12 | internal readonly struct Hex2 : IHex { public static int Value => 2; } 13 | internal readonly struct Hex3 : IHex { public static int Value => 3; } 14 | internal readonly struct Hex4 : IHex { public static int Value => 4; } 15 | internal readonly struct Hex5 : IHex { public static int Value => 5; } 16 | internal readonly struct Hex6 : IHex { public static int Value => 6; } 17 | internal readonly struct Hex7 : IHex { public static int Value => 7; } 18 | internal readonly struct Hex8 : IHex { public static int Value => 8; } 19 | internal readonly struct Hex9 : IHex { public static int Value => 9; } 20 | internal readonly struct HexA : IHex { public static int Value => 10; } 21 | internal readonly struct HexB : IHex { public static int Value => 11; } 22 | internal readonly struct HexC : IHex { public static int Value => 12; } 23 | internal readonly struct HexD : IHex { public static int Value => 13; } 24 | internal readonly struct HexE : IHex { public static int Value => 14; } 25 | internal readonly struct HexF : IHex { public static int Value => 15; } 26 | 27 | internal interface ILiteral 28 | where T : IEquatable, IComparable 29 | { 30 | abstract static T Value { get; } 31 | } 32 | 33 | internal readonly struct Int : ILiteral 34 | where H7 : IHex 35 | where H6 : IHex 36 | where H5 : IHex 37 | where H4 : IHex 38 | where H3 : IHex 39 | where H2 : IHex 40 | where H1 : IHex 41 | where H0 : IHex 42 | { 43 | public static int Value 44 | { 45 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 46 | get => (H7.Value << 28) 47 | | (H6.Value << 24) 48 | | (H5.Value << 20) 49 | | (H4.Value << 16) 50 | | (H3.Value << 12) 51 | | (H2.Value << 8) 52 | | (H1.Value << 4) 53 | | H0.Value; 54 | } 55 | } 56 | 57 | internal readonly struct Float : ILiteral 58 | where H7 : IHex 59 | where H6 : IHex 60 | where H5 : IHex 61 | where H4 : IHex 62 | where H3 : IHex 63 | where H2 : IHex 64 | where H1 : IHex 65 | where H0 : IHex 66 | { 67 | public static float Value 68 | { 69 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 70 | get => Unsafe.BitCast((H7.Value << 28) 71 | | (H6.Value << 24) 72 | | (H5.Value << 20) 73 | | (H4.Value << 16) 74 | | (H3.Value << 12) 75 | | (H2.Value << 8) 76 | | (H1.Value << 4) 77 | | H0.Value); 78 | } 79 | } 80 | 81 | internal readonly struct Char : ILiteral 82 | where H3 : IHex 83 | where H2 : IHex 84 | where H1 : IHex 85 | where H0 : IHex 86 | { 87 | public static char Value 88 | { 89 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 90 | get => (char)((H3.Value << 12) 91 | | (H2.Value << 8) 92 | | (H1.Value << 4) 93 | | H0.Value); 94 | } 95 | } 96 | 97 | internal interface IStringNode 98 | { 99 | abstract static int Length { get; } 100 | 101 | abstract static void Write(Span destination, int index); 102 | } 103 | 104 | internal readonly struct StringEnd : IStringNode 105 | { 106 | public static int Length => 0; 107 | 108 | public static void Write(Span destination, int index) 109 | { 110 | } 111 | } 112 | 113 | internal readonly struct StringNull : IStringNode 114 | { 115 | public static int Length => -1; 116 | public static void Write(Span destination, int index) 117 | { 118 | } 119 | } 120 | 121 | internal readonly struct StringNode : IStringNode 122 | where TChar : ILiteral 123 | where TNext : IStringNode 124 | { 125 | public static int Length => 1 + TNext.Length; 126 | 127 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 128 | public static void Write(Span destination, int index) 129 | { 130 | destination[index] = TChar.Value; 131 | TNext.Write(destination, index + 1); 132 | } 133 | } 134 | 135 | internal readonly struct StringLiteral : ILiteral 136 | where TString : IStringNode 137 | { 138 | public static ValueString Value => Cache.Value; 139 | 140 | private static class Cache 141 | { 142 | public static readonly ValueString Value = Build(); 143 | 144 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 145 | private static ValueString Build() 146 | { 147 | var length = TString.Length; 148 | if (length < 0) 149 | { 150 | return new ValueString(null); 151 | } 152 | 153 | if (length == 0) 154 | { 155 | return new ValueString(string.Empty); 156 | } 157 | 158 | var chars = new char[length]; 159 | TString.Write(chars.AsSpan(), 0); 160 | return new string(chars, 0, length); 161 | } 162 | } 163 | } 164 | 165 | internal readonly struct TrueLiteral : ILiteral 166 | { 167 | public static bool Value => true; 168 | } 169 | 170 | internal readonly struct FalseLiteral : ILiteral 171 | { 172 | public static bool Value => false; 173 | } 174 | 175 | internal static class LiteralTypeFactory 176 | { 177 | private static readonly Type[] HexTypes = 178 | [ 179 | typeof(Hex0), typeof(Hex1), typeof(Hex2), typeof(Hex3), 180 | typeof(Hex4), typeof(Hex5), typeof(Hex6), typeof(Hex7), 181 | typeof(Hex8), typeof(Hex9), typeof(HexA), typeof(HexB), 182 | typeof(HexC), typeof(HexD), typeof(HexE), typeof(HexF) 183 | ]; 184 | 185 | public static Type CreateIntLiteral(int value) 186 | { 187 | var typeArgs = new Type[8]; 188 | var unsigned = unchecked((uint)value); 189 | for (var i = 0; i < 8; i++) 190 | { 191 | var shift = (7 - i) * 4; 192 | var nibble = (int)((unsigned >>> shift) & 0xF); 193 | typeArgs[i] = HexTypes[nibble]; 194 | } 195 | 196 | return typeof(Int<,,,,,,,>).MakeGenericType(typeArgs); 197 | } 198 | 199 | public static Type CreateStringLiteral(string? value) 200 | { 201 | if (value is null) 202 | { 203 | return typeof(StringLiteral); 204 | } 205 | 206 | var type = typeof(StringEnd); 207 | for (var i = value.Length - 1; i >= 0; i--) 208 | { 209 | var charType = CreateCharType(value[i]); 210 | type = typeof(StringNode<,>).MakeGenericType(charType, type); 211 | } 212 | 213 | return typeof(StringLiteral<>).MakeGenericType(type); 214 | } 215 | 216 | public static Type CreateFloatLiteral(float value) 217 | { 218 | var typeArgs = new Type[8]; 219 | var unsigned = Unsafe.BitCast(value); 220 | for (var i = 0; i < 8; i++) 221 | { 222 | var shift = (7 - i) * 4; 223 | var nibble = (unsigned >>> shift) & 0xF; 224 | typeArgs[i] = HexTypes[nibble]; 225 | } 226 | 227 | return typeof(Float<,,,,,,,>).MakeGenericType(typeArgs); 228 | } 229 | 230 | public static Type CreateBoolLiteral(bool value) 231 | { 232 | return value 233 | ? typeof(TrueLiteral) 234 | : typeof(FalseLiteral); 235 | } 236 | 237 | private static Type CreateCharType(char value) 238 | { 239 | var typeArgs = new Type[4]; 240 | var unsigned = (ushort)value; 241 | for (var i = 0; i < 4; i++) 242 | { 243 | var shift = (3 - i) * 4; 244 | var nibble = (unsigned >>> shift) & 0xF; 245 | typeArgs[i] = HexTypes[nibble]; 246 | } 247 | 248 | return typeof(Char<,,,>).MakeGenericType(typeArgs); 249 | } 250 | } 251 | -------------------------------------------------------------------------------- /Runtime/QueryRuntime.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using System.Runtime.CompilerServices; 5 | using System.Runtime.InteropServices; 6 | 7 | namespace TypedSql.Runtime; 8 | 9 | internal readonly struct ValueString(string? value) : IEquatable, IComparable 10 | { 11 | public readonly string? Value = value; 12 | 13 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 14 | public int CompareTo(ValueString other) 15 | => string.Compare(Value, other.Value, StringComparison.Ordinal); 16 | 17 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 18 | public bool Equals(ValueString other) 19 | { 20 | return string.Equals(Value, other.Value, StringComparison.Ordinal); 21 | } 22 | 23 | public override string? ToString() => Value; 24 | 25 | public static implicit operator ValueString(string value) => new(value); 26 | 27 | public static implicit operator string?(ValueString value) => value.Value; 28 | 29 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 30 | public override bool Equals(object? obj) 31 | { 32 | return obj is ValueString str && Equals(str); 33 | } 34 | 35 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 36 | public override int GetHashCode() 37 | { 38 | return Value?.GetHashCode() ?? 0; 39 | } 40 | } 41 | 42 | [method: MethodImpl(MethodImplOptions.AggressiveInlining)] 43 | internal ref struct QueryRuntime(int expectedCount) 44 | { 45 | private readonly TResult[] _buffer = new TResult[expectedCount]; 46 | private int _count = 0; 47 | 48 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 49 | public void Add(in TResult value) 50 | { 51 | // The resulted rows are never larger than the input rows, so no resizing is needed. 52 | Unsafe.Add(ref MemoryMarshal.GetArrayDataReference(_buffer), _count++) = value; 53 | } 54 | 55 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 56 | public void AddRange(ReadOnlySpan values) 57 | { 58 | if (values.Length == 0) 59 | { 60 | return; 61 | } 62 | // The resulted rows are never larger than the input rows, so no resizing is needed. 63 | values.CopyTo(MemoryMarshal.CreateSpan(ref Unsafe.Add(ref MemoryMarshal.GetArrayDataReference(_buffer), _count), values.Length)); 64 | _count += values.Length; 65 | } 66 | 67 | public readonly IReadOnlyList Rows 68 | { 69 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 70 | get 71 | { 72 | return new QueryResult(_buffer, _count); 73 | } 74 | } 75 | 76 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 77 | public readonly IReadOnlyList AsStringRows() 78 | { 79 | var buffer = (ValueString[])(object)_buffer; 80 | return new ValueStringQueryResult(buffer, _count); 81 | } 82 | 83 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 84 | public readonly IReadOnlyList AsValueTupleRows() 85 | { 86 | return new ValueTupleQueryResult(_buffer, _count); 87 | } 88 | 89 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 90 | public readonly ReadOnlySpan AsSpan() 91 | { 92 | return MemoryMarshal.CreateSpan(ref MemoryMarshal.GetArrayDataReference(_buffer), _count); 93 | } 94 | } 95 | 96 | internal readonly struct ValueStringQueryResult : IReadOnlyList 97 | { 98 | private readonly ValueString[] _buffer; 99 | 100 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 101 | internal ValueStringQueryResult(ValueString[] buffer, int count) 102 | { 103 | _buffer = buffer; 104 | Count = count; 105 | } 106 | 107 | public string? this[int index] 108 | { 109 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 110 | get 111 | { 112 | ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(index, Count); 113 | return Unsafe.Add(ref MemoryMarshal.GetArrayDataReference(_buffer), index).Value; 114 | } 115 | } 116 | 117 | public int Count { get; } 118 | 119 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 120 | public Enumerator GetEnumerator() => new(_buffer, Count); 121 | 122 | IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); 123 | 124 | IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); 125 | 126 | internal struct Enumerator : IEnumerator 127 | { 128 | private readonly ValueString[] _buffer; 129 | private readonly int _count; 130 | private int _index; 131 | 132 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 133 | internal Enumerator(ValueString[] buffer, int count) 134 | { 135 | _buffer = buffer; 136 | _count = count; 137 | _index = -1; 138 | } 139 | 140 | public readonly string? Current 141 | { 142 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 143 | get 144 | { 145 | return Unsafe.Add(ref MemoryMarshal.GetArrayDataReference(_buffer), _index).Value; 146 | } 147 | } 148 | 149 | readonly object? IEnumerator.Current => Current; 150 | 151 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 152 | public bool MoveNext() => ++_index < _count; 153 | 154 | public void Reset() => _index = -1; 155 | 156 | public readonly void Dispose() { } 157 | } 158 | } 159 | 160 | internal readonly struct QueryResult : IReadOnlyList 161 | { 162 | private readonly TResult[] _buffer; 163 | 164 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 165 | internal QueryResult(TResult[] buffer, int count) 166 | { 167 | _buffer = buffer; 168 | Count = count; 169 | } 170 | 171 | public TResult this[int index] 172 | { 173 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 174 | get 175 | { 176 | ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(index, Count); 177 | return Unsafe.Add(ref MemoryMarshal.GetArrayDataReference(_buffer), index); 178 | } 179 | } 180 | 181 | public int Count { get; } 182 | 183 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 184 | public Enumerator GetEnumerator() => new(_buffer, Count); 185 | 186 | IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); 187 | 188 | IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); 189 | 190 | internal struct Enumerator : IEnumerator 191 | { 192 | private readonly TResult[] _buffer; 193 | private readonly int _count; 194 | private int _index; 195 | 196 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 197 | internal Enumerator(TResult[] buffer, int count) 198 | { 199 | _buffer = buffer; 200 | _count = count; 201 | _index = -1; 202 | } 203 | 204 | public readonly TResult Current 205 | { 206 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 207 | get 208 | { 209 | return Unsafe.Add(ref MemoryMarshal.GetArrayDataReference(_buffer), _index); 210 | } 211 | } 212 | 213 | readonly object? IEnumerator.Current => Current; 214 | 215 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 216 | public bool MoveNext() => ++_index < _count; 217 | 218 | public void Reset() => _index = -1; 219 | 220 | public readonly void Dispose() { } 221 | } 222 | } 223 | 224 | internal readonly struct ValueTupleQueryResult : IReadOnlyList 225 | { 226 | private readonly TRuntimeResult[] _buffer; 227 | 228 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 229 | internal ValueTupleQueryResult(TRuntimeResult[] buffer, int count) 230 | { 231 | _buffer = buffer; 232 | Count = count; 233 | } 234 | 235 | public TPublicResult this[int index] 236 | { 237 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 238 | get 239 | { 240 | ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(index, Count); 241 | TPublicResult result = default!; 242 | ValueTupleConvertHelper.Copy(ref result, ref Unsafe.Add(ref MemoryMarshal.GetArrayDataReference(_buffer), index)); 243 | return result; 244 | } 245 | } 246 | 247 | public int Count { get; } 248 | 249 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 250 | public Enumerator GetEnumerator() => new(_buffer, Count); 251 | 252 | IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); 253 | 254 | IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); 255 | 256 | internal struct Enumerator : IEnumerator 257 | { 258 | private readonly TRuntimeResult[] _buffer; 259 | private readonly int _count; 260 | private int _index; 261 | 262 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 263 | internal Enumerator(TRuntimeResult[] buffer, int count) 264 | { 265 | _buffer = buffer; 266 | _count = count; 267 | _index = -1; 268 | } 269 | 270 | public readonly TPublicResult Current 271 | { 272 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 273 | get 274 | { 275 | TPublicResult result = default!; 276 | ValueTupleConvertHelper.Copy(ref result, ref Unsafe.Add(ref MemoryMarshal.GetArrayDataReference(_buffer), _index)); 277 | return result; 278 | } 279 | } 280 | 281 | readonly object IEnumerator.Current => Current!; 282 | 283 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 284 | public bool MoveNext() => ++_index < _count; 285 | 286 | public void Reset() => _index = -1; 287 | 288 | public readonly void Dispose() { } 289 | } 290 | } 291 | -------------------------------------------------------------------------------- /Runtime/SqlParser.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Globalization; 3 | using System.Text; 4 | 5 | namespace TypedSql.Runtime; 6 | 7 | internal enum ComparisonOperator 8 | { 9 | Equals, 10 | GreaterThan, 11 | LessThan, 12 | GreaterOrEqual, 13 | LessOrEqual, 14 | NotEqual 15 | } 16 | 17 | internal sealed record SelectionClause(bool SelectAll, IReadOnlyList ColumnIdentifiers); 18 | 19 | internal enum LiteralKind 20 | { 21 | Integer, 22 | String, 23 | Float, 24 | Boolean 25 | } 26 | 27 | internal readonly record struct LiteralValue(LiteralKind Kind, int IntValue, string? StringValue, float FloatValue, bool BoolValue) 28 | { 29 | public static LiteralValue FromInt(int value) => new(LiteralKind.Integer, value, null, 0f, false); 30 | 31 | public static LiteralValue FromString(string? value) => new(LiteralKind.String, 0, value, 0f, false); 32 | 33 | public static LiteralValue FromFloat(float value) => new(LiteralKind.Float, 0, null, value, false); 34 | 35 | public static LiteralValue FromBool(bool value) => new(LiteralKind.Boolean, 0, null, 0f, value); 36 | 37 | public override string? ToString() 38 | { 39 | return Kind switch 40 | { 41 | LiteralKind.Integer => IntValue.ToString(CultureInfo.InvariantCulture), 42 | LiteralKind.String => StringValue is null ? "null" : $"'{StringValue.Replace("'", "\\'")}'", 43 | LiteralKind.Float => FloatValue.ToString(CultureInfo.InvariantCulture), 44 | LiteralKind.Boolean => BoolValue ? "True" : "False", 45 | _ => throw new InvalidOperationException("Unknown literal kind.") 46 | }; 47 | } 48 | } 49 | 50 | internal abstract record WhereExpression; 51 | 52 | internal sealed record ComparisonExpression(string ColumnIdentifier, ComparisonOperator Operator, LiteralValue Literal) : WhereExpression 53 | { 54 | public override string ToString() 55 | { 56 | var opString = Operator switch 57 | { 58 | ComparisonOperator.Equals => "=", 59 | ComparisonOperator.GreaterThan => ">", 60 | ComparisonOperator.LessThan => "<", 61 | ComparisonOperator.GreaterOrEqual => ">=", 62 | ComparisonOperator.LessOrEqual => "<=", 63 | ComparisonOperator.NotEqual => "!=", 64 | _ => throw new InvalidOperationException("Unknown comparison operator.") 65 | }; 66 | return $"{ColumnIdentifier} {opString} {Literal}"; 67 | } 68 | } 69 | 70 | internal sealed record AndExpression(WhereExpression Left, WhereExpression Right) : WhereExpression 71 | { 72 | public override string ToString() 73 | { 74 | return $"({Left}) && ({Right})"; 75 | } 76 | } 77 | 78 | internal sealed record OrExpression(WhereExpression Left, WhereExpression Right) : WhereExpression 79 | { 80 | public override string ToString() 81 | { 82 | return $"({Left}) || ({Right})"; 83 | } 84 | } 85 | 86 | internal sealed record NotExpression(WhereExpression Expression) : WhereExpression 87 | { 88 | public override string ToString() 89 | { 90 | return $"!({Expression})"; 91 | } 92 | } 93 | 94 | internal sealed record ParsedQuery(SelectionClause Selection, WhereExpression? Where); 95 | 96 | internal static class SqlParser 97 | { 98 | public static ParsedQuery Parse(string sql) 99 | { 100 | var tokens = Tokenize(sql); 101 | var index = 0; 102 | 103 | Expect(tokens, ref index, "SELECT"); 104 | if (index >= tokens.Count) 105 | { 106 | throw new InvalidOperationException("Expected selection list after SELECT."); 107 | } 108 | 109 | SelectionClause selection; 110 | if (tokens[index] == "*") 111 | { 112 | index++; 113 | selection = new SelectionClause(true, Array.Empty()); 114 | } 115 | else 116 | { 117 | var columns = new List(); 118 | while (index < tokens.Count && !tokens[index].Equals("FROM", StringComparison.OrdinalIgnoreCase)) 119 | { 120 | columns.Add(tokens[index++]); 121 | } 122 | 123 | if (columns.Count == 0) 124 | { 125 | throw new InvalidOperationException("Expected at least one column in SELECT clause."); 126 | } 127 | 128 | selection = new SelectionClause(false, columns); 129 | } 130 | 131 | Expect(tokens, ref index, "FROM"); 132 | if (index >= tokens.Count) 133 | { 134 | throw new InvalidOperationException("Expected table name after FROM."); 135 | } 136 | 137 | var source = tokens[index++]; 138 | if (!source.Equals("$")) 139 | { 140 | throw new InvalidOperationException("Queries must select FROM $ to reference the provided rows."); 141 | } 142 | WhereExpression? where = null; 143 | 144 | if (index < tokens.Count && tokens[index].Equals("WHERE", StringComparison.OrdinalIgnoreCase)) 145 | { 146 | index++; 147 | where = ParseWhereExpression(tokens, ref index); 148 | } 149 | 150 | if (index < tokens.Count) 151 | { 152 | throw new InvalidOperationException($"Unexpected token '{tokens[index]}'."); 153 | } 154 | 155 | return new ParsedQuery(selection, where); 156 | } 157 | 158 | private static LiteralValue ParseLiteral(string token) 159 | { 160 | if (token.Equals("true", StringComparison.OrdinalIgnoreCase)) 161 | { 162 | return LiteralValue.FromBool(true); 163 | } 164 | 165 | if (token.Equals("false", StringComparison.OrdinalIgnoreCase)) 166 | { 167 | return LiteralValue.FromBool(false); 168 | } 169 | 170 | if (token.Equals("null", StringComparison.OrdinalIgnoreCase)) 171 | { 172 | return LiteralValue.FromString(null); 173 | } 174 | 175 | if (token.Length >= 2 && token[0] == '\'' && token[^1] == '\'') 176 | { 177 | var sb = new StringBuilder(token.Length - 2); 178 | for (var i = 1; i < token.Length - 1; i++) 179 | { 180 | var ch = token[i]; 181 | if (ch == '\'' && i + 1 < token.Length - 1 && token[i + 1] == '\'') 182 | { 183 | sb.Append('\''); 184 | i++; 185 | } 186 | else 187 | { 188 | sb.Append(ch); 189 | } 190 | } 191 | return LiteralValue.FromString(sb.ToString()); 192 | } 193 | 194 | if (int.TryParse(token, NumberStyles.Integer, CultureInfo.InvariantCulture, out var number)) 195 | { 196 | return LiteralValue.FromInt(number); 197 | } 198 | 199 | if (float.TryParse(token, NumberStyles.Float | NumberStyles.AllowThousands, CultureInfo.InvariantCulture, out var floatValue)) 200 | { 201 | return LiteralValue.FromFloat(floatValue); 202 | } 203 | 204 | throw new InvalidOperationException($"Literal '{token}' is not a supported type."); 205 | } 206 | 207 | private static ComparisonOperator ParseOperator(string token) 208 | { 209 | return token switch 210 | { 211 | "=" => ComparisonOperator.Equals, 212 | ">" => ComparisonOperator.GreaterThan, 213 | "<" => ComparisonOperator.LessThan, 214 | ">=" => ComparisonOperator.GreaterOrEqual, 215 | "<=" => ComparisonOperator.LessOrEqual, 216 | "!=" => ComparisonOperator.NotEqual, 217 | _ => throw new InvalidOperationException($"Unsupported comparison operator '{token}'.") 218 | }; 219 | } 220 | 221 | private static WhereExpression ParseWhereExpression(IReadOnlyList tokens, ref int index) 222 | { 223 | if (index >= tokens.Count) 224 | { 225 | throw new InvalidOperationException("Expected predicate after WHERE."); 226 | } 227 | 228 | return ParseOrExpression(tokens, ref index); 229 | } 230 | 231 | private static WhereExpression ParseOrExpression(IReadOnlyList tokens, ref int index) 232 | { 233 | var left = ParseAndExpression(tokens, ref index); 234 | while (index < tokens.Count && tokens[index].Equals("OR", StringComparison.OrdinalIgnoreCase)) 235 | { 236 | index++; 237 | var right = ParseAndExpression(tokens, ref index); 238 | left = new OrExpression(left, right); 239 | } 240 | 241 | return left; 242 | } 243 | 244 | private static WhereExpression ParseAndExpression(IReadOnlyList tokens, ref int index) 245 | { 246 | var left = ParseUnaryExpression(tokens, ref index); 247 | while (index < tokens.Count && tokens[index].Equals("AND", StringComparison.OrdinalIgnoreCase)) 248 | { 249 | index++; 250 | var right = ParseUnaryExpression(tokens, ref index); 251 | left = new AndExpression(left, right); 252 | } 253 | 254 | return left; 255 | } 256 | 257 | private static WhereExpression ParseUnaryExpression(IReadOnlyList tokens, ref int index) 258 | { 259 | if (index >= tokens.Count) 260 | { 261 | throw new InvalidOperationException("Unexpected end of WHERE clause."); 262 | } 263 | 264 | if (tokens[index].Equals("NOT", StringComparison.OrdinalIgnoreCase)) 265 | { 266 | index++; 267 | var expression = ParseUnaryExpression(tokens, ref index); 268 | return new NotExpression(expression); 269 | } 270 | 271 | if (tokens[index] == "(") 272 | { 273 | index++; 274 | var expression = ParseOrExpression(tokens, ref index); 275 | if (index >= tokens.Count || tokens[index] != ")") 276 | { 277 | throw new InvalidOperationException("Unclosed parenthesis in WHERE clause."); 278 | } 279 | 280 | index++; 281 | return expression; 282 | } 283 | 284 | return ParseComparisonExpression(tokens, ref index); 285 | } 286 | 287 | private static WhereExpression ParseComparisonExpression(IReadOnlyList tokens, ref int index) 288 | { 289 | if (index + 2 >= tokens.Count) 290 | { 291 | throw new InvalidOperationException("Malformed predicate in WHERE clause."); 292 | } 293 | 294 | var column = tokens[index++]; 295 | var opToken = tokens[index++]; 296 | var literalToken = tokens[index++]; 297 | var op = ParseOperator(opToken); 298 | var literal = ParseLiteral(literalToken); 299 | return new ComparisonExpression(column, op, literal); 300 | } 301 | 302 | private static void Expect(IReadOnlyList tokens, ref int index, string keyword) 303 | { 304 | if (index >= tokens.Count || !tokens[index].Equals(keyword, StringComparison.OrdinalIgnoreCase)) 305 | { 306 | throw new InvalidOperationException($"Expected keyword '{keyword}'."); 307 | } 308 | 309 | index++; 310 | } 311 | 312 | private static List Tokenize(string sql) 313 | { 314 | var tokens = new List(); 315 | var builder = new StringBuilder(); 316 | 317 | for (var i = 0; i < sql.Length; i++) 318 | { 319 | var ch = sql[i]; 320 | if (char.IsWhiteSpace(ch) || ch == ',' || ch == ';') 321 | { 322 | Flush(); 323 | continue; 324 | } 325 | 326 | if (ch == '(' || ch == ')') 327 | { 328 | Flush(); 329 | tokens.Add(ch.ToString()); 330 | continue; 331 | } 332 | 333 | if (ch == '\'') 334 | { 335 | Flush(); 336 | var literal = new StringBuilder(); 337 | literal.Append(ch); 338 | i++; 339 | var closed = false; 340 | for (; i < sql.Length; i++) 341 | { 342 | var current = sql[i]; 343 | literal.Append(current); 344 | if (current == '\'') 345 | { 346 | if (i + 1 < sql.Length && sql[i + 1] == '\'') 347 | { 348 | literal.Append('\''); 349 | i++; 350 | continue; 351 | } 352 | 353 | closed = true; 354 | break; 355 | } 356 | } 357 | 358 | if (!closed) 359 | { 360 | throw new InvalidOperationException("Unterminated string literal."); 361 | } 362 | 363 | tokens.Add(literal.ToString()); 364 | continue; 365 | } 366 | 367 | if (ch is '!' or '>' or '<' or '=') 368 | { 369 | Flush(); 370 | if ((ch == '!' || ch == '>' || ch == '<') && i + 1 < sql.Length && sql[i + 1] == '=') 371 | { 372 | tokens.Add(new string([ch, '='])); 373 | i++; 374 | } 375 | else 376 | { 377 | tokens.Add(ch.ToString()); 378 | } 379 | continue; 380 | } 381 | 382 | builder.Append(ch); 383 | } 384 | 385 | Flush(); 386 | return tokens; 387 | 388 | void Flush() 389 | { 390 | if (builder.Length == 0) 391 | { 392 | return; 393 | } 394 | 395 | tokens.Add(builder.ToString()); 396 | builder.Clear(); 397 | } 398 | } 399 | } 400 | -------------------------------------------------------------------------------- /Runtime/Expressions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.CompilerServices; 3 | 4 | namespace TypedSql.Runtime; 5 | 6 | internal interface IColumn 7 | { 8 | abstract static string Identifier { get; } 9 | 10 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 11 | abstract static TValue Get(in TRow row); 12 | } 13 | 14 | internal interface IProjection 15 | { 16 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 17 | abstract static TResult Project(in TRow row); 18 | } 19 | 20 | internal readonly struct IdentityProjection : IProjection 21 | { 22 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 23 | public static TRow Project(in TRow row) => row; 24 | } 25 | 26 | internal readonly struct ColumnProjection : IProjection 27 | where TColumn : IColumn 28 | { 29 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 30 | public static TValue Project(in TRow row) => TColumn.Get(row); 31 | } 32 | 33 | internal readonly struct ValueTupleProjection : IProjection> 34 | where TColumn1 : IColumn 35 | { 36 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 37 | public static ValueTuple Project(in TRow row) 38 | => new(TColumn1.Get(row)); 39 | } 40 | 41 | internal readonly struct ValueTupleProjection : IProjection> 42 | where TColumn1 : IColumn 43 | where TColumn2 : IColumn 44 | { 45 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 46 | public static ValueTuple Project(in TRow row) 47 | => new(TColumn1.Get(row), TColumn2.Get(row)); 48 | } 49 | 50 | internal readonly struct ValueTupleProjection : IProjection> 51 | where TColumn1 : IColumn 52 | where TColumn2 : IColumn 53 | where TColumn3 : IColumn 54 | { 55 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 56 | public static ValueTuple Project(in TRow row) 57 | => new(TColumn1.Get(row), TColumn2.Get(row), TColumn3.Get(row)); 58 | } 59 | 60 | internal readonly struct ValueTupleProjection : IProjection> 61 | where TColumn1 : IColumn 62 | where TColumn2 : IColumn 63 | where TColumn3 : IColumn 64 | where TColumn4 : IColumn 65 | { 66 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 67 | public static ValueTuple Project(in TRow row) 68 | => new(TColumn1.Get(row), TColumn2.Get(row), TColumn3.Get(row), TColumn4.Get(row)); 69 | } 70 | 71 | internal readonly struct ValueTupleProjection : IProjection> 72 | where TColumn1 : IColumn 73 | where TColumn2 : IColumn 74 | where TColumn3 : IColumn 75 | where TColumn4 : IColumn 76 | where TColumn5 : IColumn 77 | { 78 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 79 | public static ValueTuple Project(in TRow row) 80 | => new(TColumn1.Get(row), TColumn2.Get(row), TColumn3.Get(row), TColumn4.Get(row), TColumn5.Get(row)); 81 | } 82 | 83 | internal readonly struct ValueTupleProjection : IProjection> 84 | where TColumn1 : IColumn 85 | where TColumn2 : IColumn 86 | where TColumn3 : IColumn 87 | where TColumn4 : IColumn 88 | where TColumn5 : IColumn 89 | where TColumn6 : IColumn 90 | { 91 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 92 | public static ValueTuple Project(in TRow row) 93 | => new(TColumn1.Get(row), TColumn2.Get(row), TColumn3.Get(row), TColumn4.Get(row), TColumn5.Get(row), TColumn6.Get(row)); 94 | } 95 | 96 | internal readonly struct ValueTupleProjection : IProjection> 97 | where TColumn1 : IColumn 98 | where TColumn2 : IColumn 99 | where TColumn3 : IColumn 100 | where TColumn4 : IColumn 101 | where TColumn5 : IColumn 102 | where TColumn6 : IColumn 103 | where TColumn7 : IColumn 104 | { 105 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 106 | public static ValueTuple Project(in TRow row) 107 | => new( 108 | TColumn1.Get(row), 109 | TColumn2.Get(row), 110 | TColumn3.Get(row), 111 | TColumn4.Get(row), 112 | TColumn5.Get(row), 113 | TColumn6.Get(row), 114 | TColumn7.Get(row)); 115 | } 116 | 117 | internal readonly struct ValueTupleProjection 118 | : IProjection> 119 | where TColumn1 : IColumn 120 | where TColumn2 : IColumn 121 | where TColumn3 : IColumn 122 | where TColumn4 : IColumn 123 | where TColumn5 : IColumn 124 | where TColumn6 : IColumn 125 | where TColumn7 : IColumn 126 | where TNextProjection : IProjection 127 | where TRest : struct 128 | { 129 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 130 | public static ValueTuple Project(in TRow row) 131 | => new( 132 | TColumn1.Get(row), 133 | TColumn2.Get(row), 134 | TColumn3.Get(row), 135 | TColumn4.Get(row), 136 | TColumn5.Get(row), 137 | TColumn6.Get(row), 138 | TColumn7.Get(row), 139 | TNextProjection.Project(in row)); 140 | } 141 | 142 | internal readonly struct ValueStringColumn : IColumn 143 | where TColumn : IColumn 144 | { 145 | public static string Identifier => TColumn.Identifier; 146 | 147 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 148 | public static ValueString Get(in TRow row) => new(TColumn.Get(in row)); 149 | } 150 | 151 | internal interface IFilter 152 | { 153 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 154 | abstract static bool Evaluate(in TRow row); 155 | } 156 | 157 | internal readonly struct EqualsFilter : IFilter 158 | where TColumn : IColumn 159 | where TLiteral : ILiteral 160 | where TValue : IEquatable, IComparable 161 | { 162 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 163 | public static bool Evaluate(in TRow row) 164 | { 165 | if (typeof(TValue).IsValueType) 166 | { 167 | return TColumn.Get(row).Equals(TLiteral.Value); 168 | } 169 | else 170 | { 171 | var left = TColumn.Get(row); 172 | var right = TLiteral.Value; 173 | if (left is null && right is null) return true; 174 | if (left is null || right is null) return false; 175 | return left.Equals(right); 176 | } 177 | } 178 | } 179 | 180 | internal readonly struct GreaterThanFilter : IFilter 181 | where TColumn : IColumn 182 | where TLiteral : ILiteral 183 | where TValue : IEquatable, IComparable 184 | { 185 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 186 | public static bool Evaluate(in TRow row) 187 | { 188 | if (typeof(TValue).IsValueType) 189 | { 190 | return TColumn.Get(row).CompareTo(TLiteral.Value) > 0; 191 | } 192 | else 193 | { 194 | var left = TColumn.Get(row); 195 | var right = TLiteral.Value; 196 | if (left is null && right is null) return false; 197 | if (left is null) return false; 198 | if (right is null) return true; 199 | return left.CompareTo(right) > 0; 200 | } 201 | } 202 | } 203 | 204 | internal readonly struct LessThanFilter : IFilter 205 | where TColumn : IColumn 206 | where TLiteral : ILiteral 207 | where TValue : IEquatable, IComparable 208 | { 209 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 210 | public static bool Evaluate(in TRow row) 211 | { 212 | if (typeof(TValue).IsValueType) 213 | { 214 | return TColumn.Get(row).CompareTo(TLiteral.Value) < 0; 215 | } 216 | else 217 | { 218 | var left = TColumn.Get(row); 219 | var right = TLiteral.Value; 220 | if (left is null && right is null) return false; 221 | if (left is null) return true; 222 | if (right is null) return false; 223 | return left.CompareTo(right) < 0; 224 | } 225 | } 226 | } 227 | 228 | internal readonly struct GreaterOrEqualFilter : IFilter 229 | where TColumn : IColumn 230 | where TLiteral : ILiteral 231 | where TValue : IEquatable, IComparable 232 | { 233 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 234 | public static bool Evaluate(in TRow row) 235 | { 236 | if (typeof(TValue).IsValueType) 237 | { 238 | return TColumn.Get(row).CompareTo(TLiteral.Value) >= 0; 239 | } 240 | else 241 | { 242 | var left = TColumn.Get(row); 243 | var right = TLiteral.Value; 244 | if (left is null && right is null) return true; 245 | if (left is null) return false; 246 | if (right is null) return true; 247 | return left.CompareTo(right) >= 0; 248 | } 249 | } 250 | } 251 | 252 | internal readonly struct LessOrEqualFilter : IFilter 253 | where TColumn : IColumn 254 | where TLiteral : ILiteral 255 | where TValue : IEquatable, IComparable 256 | { 257 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 258 | public static bool Evaluate(in TRow row) 259 | { 260 | if (typeof(TValue).IsValueType) 261 | { 262 | return TColumn.Get(row).CompareTo(TLiteral.Value) <= 0; 263 | } 264 | else 265 | { 266 | var left = TColumn.Get(row); 267 | var right = TLiteral.Value; 268 | if (left is null && right is null) return true; 269 | if (left is null) return true; 270 | if (right is null) return false; 271 | return left.CompareTo(right) <= 0; 272 | } 273 | } 274 | } 275 | 276 | internal readonly struct NotEqualFilter : IFilter 277 | where TColumn : IColumn 278 | where TLiteral : ILiteral 279 | where TValue : IEquatable, IComparable 280 | { 281 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 282 | public static bool Evaluate(in TRow row) 283 | { 284 | if (typeof(TValue).IsValueType) 285 | { 286 | return !TColumn.Get(row).Equals(TLiteral.Value); 287 | } 288 | else 289 | { 290 | var left = TColumn.Get(row); 291 | var right = TLiteral.Value; 292 | if (left is null && right is null) return false; 293 | if (left is null || right is null) return true; 294 | return !left.Equals(right); 295 | } 296 | } 297 | } 298 | 299 | internal readonly struct AndFilter : IFilter 300 | where TLeftPredicate : IFilter 301 | where TRightPredicate : IFilter 302 | { 303 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 304 | public static bool Evaluate(in TRow row) 305 | { 306 | if (!TLeftPredicate.Evaluate(in row)) 307 | { 308 | return false; 309 | } 310 | 311 | return TRightPredicate.Evaluate(in row); 312 | } 313 | } 314 | 315 | internal readonly struct OrFilter : IFilter 316 | where TLeftPredicate : IFilter 317 | where TRightPredicate : IFilter 318 | { 319 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 320 | public static bool Evaluate(in TRow row) 321 | { 322 | if (TLeftPredicate.Evaluate(in row)) 323 | { 324 | return true; 325 | } 326 | 327 | return TRightPredicate.Evaluate(in row); 328 | } 329 | } 330 | 331 | internal readonly struct NotFilter : IFilter 332 | where TPredicate : IFilter 333 | { 334 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 335 | public static bool Evaluate(in TRow row) => !TPredicate.Evaluate(in row); 336 | } -------------------------------------------------------------------------------- /Runtime/SqlCompiler.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Runtime.CompilerServices; 4 | 5 | namespace TypedSql.Runtime; 6 | 7 | internal static class SqlCompiler 8 | { 9 | public static (Type PipelineType, Type RuntimeResultType, Type PublicResultType) Compile(ParsedQuery query, bool supportsAot = false) 10 | { 11 | var rowType = typeof(TRow); 12 | Type runtimeResultType; 13 | Type publicResultType; 14 | Type pipeline; 15 | 16 | if (query.Selection.SelectAll) 17 | { 18 | runtimeResultType = rowType; 19 | publicResultType = rowType; 20 | pipeline = typeof(Stop<,>).MakeGenericType(runtimeResultType, rowType); 21 | } 22 | else 23 | { 24 | var (projectionType, runtimeType, publicType) = BuildSelectionProjection(query.Selection.ColumnIdentifiers, supportsAot); 25 | runtimeResultType = runtimeType; 26 | publicResultType = publicType; 27 | 28 | var stopNode = typeof(Stop<,>).MakeGenericType(runtimeResultType, rowType); 29 | pipeline = typeof(Select<,,,,,>).MakeGenericType( 30 | rowType, 31 | projectionType, 32 | stopNode, 33 | runtimeResultType, 34 | runtimeResultType, 35 | rowType); 36 | } 37 | 38 | if (query.Where is { } whereExpression) 39 | { 40 | var simplifiedWhere = Simplify(whereExpression); 41 | var predicateType = BuildPredicate(simplifiedWhere); 42 | pipeline = typeof(Where<,,,,>).MakeGenericType(rowType, predicateType, pipeline, runtimeResultType, rowType); 43 | } 44 | 45 | pipeline = Optimize(pipeline); 46 | 47 | return (pipeline, runtimeResultType, publicResultType); 48 | } 49 | 50 | private static (Type ProjectionType, Type RuntimeResultType, Type PublicResultType) BuildSelectionProjection(IReadOnlyList columnIdentifiers, bool supportsAot) 51 | { 52 | if (columnIdentifiers is null || columnIdentifiers.Count == 0) 53 | { 54 | throw new InvalidOperationException("At least one column must be specified in SELECT clause."); 55 | } 56 | 57 | var rowType = typeof(TRow); 58 | 59 | if (columnIdentifiers.Count == 1) 60 | { 61 | var column = SchemaRegistry.ResolveColumn(columnIdentifiers[0]); 62 | var runtimeColumnType = column.GetRuntimeColumnType(rowType); 63 | var runtimeValueType = column.GetRuntimeValueType(); 64 | return (typeof(ColumnProjection<,,>).MakeGenericType(runtimeColumnType, rowType, runtimeValueType), runtimeValueType, column.ValueType); 65 | } 66 | 67 | var columns = new ColumnMetadata[columnIdentifiers.Count]; 68 | for (var i = 0; i < columnIdentifiers.Count; i++) 69 | { 70 | columns[i] = SchemaRegistry.ResolveColumn(columnIdentifiers[i]); 71 | } 72 | 73 | var tupleInfo = BuildTupleSelection(rowType, columns, 0, supportsAot); 74 | return (tupleInfo.ProjectionType, tupleInfo.RuntimeType, tupleInfo.PublicType); 75 | } 76 | 77 | private static TupleSelectionInfo BuildTupleSelection(Type rowType, ColumnMetadata[] columns, int offset, bool supportsAot) 78 | { 79 | var remaining = columns.Length - offset; 80 | if (remaining <= 0) 81 | { 82 | throw new InvalidOperationException("At least one column must be specified in SELECT clause."); 83 | } 84 | 85 | if (remaining <= 7) 86 | { 87 | var runtimeColumnTypes = new Type[remaining]; 88 | var runtimeValueTypes = new Type[remaining]; 89 | var publicValueTypes = new Type[remaining]; 90 | 91 | for (var i = 0; i < remaining; i++) 92 | { 93 | var column = columns[offset + i]; 94 | if (!supportsAot) 95 | { 96 | runtimeColumnTypes[i] = column.GetRuntimeColumnType(rowType); 97 | runtimeValueTypes[i] = column.GetRuntimeValueType(); 98 | } 99 | else 100 | { 101 | runtimeColumnTypes[i] = column.ColumnType; 102 | runtimeValueTypes[i] = column.ValueType; 103 | } 104 | publicValueTypes[i] = column.ValueType; 105 | } 106 | 107 | var leafProjectionType = CreateLeafTupleProjectionType(rowType, runtimeColumnTypes, runtimeValueTypes); 108 | var leafRuntimeTupleType = CreateSmallValueTupleType(runtimeValueTypes); 109 | var leafPublicTupleType = CreateSmallValueTupleType(publicValueTypes); 110 | return new(leafProjectionType, leafRuntimeTupleType, leafPublicTupleType); 111 | } 112 | 113 | var headRuntimeColumns = new Type[7]; 114 | var headRuntimeValues = new Type[7]; 115 | var headPublicValues = new Type[7]; 116 | 117 | for (var i = 0; i < 7; i++) 118 | { 119 | var column = columns[offset + i]; 120 | if (!supportsAot) 121 | { 122 | headRuntimeColumns[i] = column.GetRuntimeColumnType(rowType); 123 | headRuntimeValues[i] = column.GetRuntimeValueType(); 124 | } 125 | else 126 | { 127 | headRuntimeColumns[i] = column.ColumnType; 128 | headRuntimeValues[i] = column.ValueType; 129 | } 130 | headPublicValues[i] = column.ValueType; 131 | } 132 | 133 | var rest = BuildTupleSelection(rowType, columns, offset + 7, supportsAot); 134 | var projectionArgs = new Type[17]; 135 | projectionArgs[0] = rowType; 136 | for (var i = 0; i < 7; i++) 137 | { 138 | projectionArgs[i + 1] = headRuntimeColumns[i]; 139 | projectionArgs[i + 9] = headRuntimeValues[i]; 140 | } 141 | 142 | projectionArgs[8] = rest.ProjectionType; 143 | projectionArgs[16] = rest.RuntimeType; 144 | var projectionType = typeof(ValueTupleProjection<,,,,,,,,,,,,,,,,>).MakeGenericType(projectionArgs); 145 | 146 | var runtimeTupleType = typeof(ValueTuple<,,,,,,,>).MakeGenericType( 147 | headRuntimeValues[0], 148 | headRuntimeValues[1], 149 | headRuntimeValues[2], 150 | headRuntimeValues[3], 151 | headRuntimeValues[4], 152 | headRuntimeValues[5], 153 | headRuntimeValues[6], 154 | rest.RuntimeType); 155 | 156 | var publicTupleType = typeof(ValueTuple<,,,,,,,>).MakeGenericType( 157 | headPublicValues[0], 158 | headPublicValues[1], 159 | headPublicValues[2], 160 | headPublicValues[3], 161 | headPublicValues[4], 162 | headPublicValues[5], 163 | headPublicValues[6], 164 | rest.PublicType); 165 | 166 | return new(projectionType, runtimeTupleType, publicTupleType); 167 | } 168 | 169 | private static Type CreateLeafTupleProjectionType(Type rowType, ReadOnlySpan runtimeColumnTypes, ReadOnlySpan runtimeValueTypes) 170 | { 171 | return runtimeColumnTypes.Length switch 172 | { 173 | 1 => typeof(ValueTupleProjection<,,>).MakeGenericType(rowType, runtimeColumnTypes[0], runtimeValueTypes[0]), 174 | 2 => typeof(ValueTupleProjection<,,,,>).MakeGenericType(rowType, runtimeColumnTypes[0], runtimeColumnTypes[1], runtimeValueTypes[0], runtimeValueTypes[1]), 175 | 3 => typeof(ValueTupleProjection<,,,,,,>).MakeGenericType(rowType, runtimeColumnTypes[0], runtimeColumnTypes[1], runtimeColumnTypes[2], runtimeValueTypes[0], runtimeValueTypes[1], runtimeValueTypes[2]), 176 | 4 => typeof(ValueTupleProjection<,,,,,,,,>).MakeGenericType(rowType, runtimeColumnTypes[0], runtimeColumnTypes[1], runtimeColumnTypes[2], runtimeColumnTypes[3], runtimeValueTypes[0], runtimeValueTypes[1], runtimeValueTypes[2], runtimeValueTypes[3]), 177 | 5 => typeof(ValueTupleProjection<,,,,,,,,,,>).MakeGenericType(rowType, runtimeColumnTypes[0], runtimeColumnTypes[1], runtimeColumnTypes[2], runtimeColumnTypes[3], runtimeColumnTypes[4], runtimeValueTypes[0], runtimeValueTypes[1], runtimeValueTypes[2], runtimeValueTypes[3], runtimeValueTypes[4]), 178 | 6 => typeof(ValueTupleProjection<,,,,,,,,,,,,>).MakeGenericType(rowType, runtimeColumnTypes[0], runtimeColumnTypes[1], runtimeColumnTypes[2], runtimeColumnTypes[3], runtimeColumnTypes[4], runtimeColumnTypes[5], runtimeValueTypes[0], runtimeValueTypes[1], runtimeValueTypes[2], runtimeValueTypes[3], runtimeValueTypes[4], runtimeValueTypes[5]), 179 | 7 => typeof(ValueTupleProjection<,,,,,,,,,,,,,,>).MakeGenericType(rowType, runtimeColumnTypes[0], runtimeColumnTypes[1], runtimeColumnTypes[2], runtimeColumnTypes[3], runtimeColumnTypes[4], runtimeColumnTypes[5], runtimeColumnTypes[6], runtimeValueTypes[0], runtimeValueTypes[1], runtimeValueTypes[2], runtimeValueTypes[3], runtimeValueTypes[4], runtimeValueTypes[5], runtimeValueTypes[6]), 180 | _ => throw new InvalidOperationException("ValueTuple projection arity is not supported."), 181 | }; 182 | } 183 | 184 | private static Type CreateSmallValueTupleType(ReadOnlySpan elementTypes) 185 | { 186 | return elementTypes.Length switch 187 | { 188 | 1 => typeof(ValueTuple<>).MakeGenericType(elementTypes[0]), 189 | 2 => typeof(ValueTuple<,>).MakeGenericType(elementTypes[0], elementTypes[1]), 190 | 3 => typeof(ValueTuple<,,>).MakeGenericType(elementTypes[0], elementTypes[1], elementTypes[2]), 191 | 4 => typeof(ValueTuple<,,,>).MakeGenericType(elementTypes[0], elementTypes[1], elementTypes[2], elementTypes[3]), 192 | 5 => typeof(ValueTuple<,,,,>).MakeGenericType(elementTypes[0], elementTypes[1], elementTypes[2], elementTypes[3], elementTypes[4]), 193 | 6 => typeof(ValueTuple<,,,,,>).MakeGenericType(elementTypes[0], elementTypes[1], elementTypes[2], elementTypes[3], elementTypes[4], elementTypes[5]), 194 | 7 => typeof(ValueTuple<,,,,,,>).MakeGenericType(elementTypes[0], elementTypes[1], elementTypes[2], elementTypes[3], elementTypes[4], elementTypes[5], elementTypes[6]), 195 | _ => throw new InvalidOperationException("ValueTuple arity is not supported."), 196 | }; 197 | } 198 | 199 | private readonly record struct TupleSelectionInfo(Type ProjectionType, Type RuntimeType, Type PublicType); 200 | 201 | private static Type BuildPredicate(WhereExpression expression) 202 | { 203 | return expression switch 204 | { 205 | ComparisonExpression comparison => BuildComparisonPredicate(comparison), 206 | AndExpression andExpression => typeof(AndFilter<,,>).MakeGenericType( 207 | typeof(TRow), 208 | BuildPredicate(andExpression.Left), 209 | BuildPredicate(andExpression.Right)), 210 | OrExpression orExpression => typeof(OrFilter<,,>).MakeGenericType( 211 | typeof(TRow), 212 | BuildPredicate(orExpression.Left), 213 | BuildPredicate(orExpression.Right)), 214 | NotExpression notExpression => typeof(NotFilter<,>).MakeGenericType( 215 | typeof(TRow), 216 | BuildPredicate(notExpression.Expression)), 217 | _ => throw new InvalidOperationException($"Unsupported WHERE expression '{expression}'."), 218 | }; 219 | } 220 | 221 | private static WhereExpression Simplify(WhereExpression expression) 222 | { 223 | return expression switch 224 | { 225 | AndExpression andExpression => SimplifyAndExpression(andExpression), 226 | OrExpression orExpression => SimplifyOrExpression(orExpression), 227 | NotExpression notExpression => SimplifyNotExpression(notExpression), 228 | _ => expression, 229 | }; 230 | } 231 | 232 | private static WhereExpression SimplifyAndExpression(AndExpression expression) 233 | { 234 | var left = Simplify(expression.Left); 235 | var right = Simplify(expression.Right); 236 | 237 | if (left == right) 238 | { 239 | return left; 240 | } 241 | 242 | if (left == expression.Left && right == expression.Right) 243 | { 244 | return expression; 245 | } 246 | 247 | return new AndExpression(left, right); 248 | } 249 | 250 | private static WhereExpression SimplifyOrExpression(OrExpression expression) 251 | { 252 | var left = Simplify(expression.Left); 253 | var right = Simplify(expression.Right); 254 | 255 | if (left == right) 256 | { 257 | return left; 258 | } 259 | 260 | if (left == expression.Left && right == expression.Right) 261 | { 262 | return expression; 263 | } 264 | 265 | return new OrExpression(left, right); 266 | } 267 | 268 | private static WhereExpression SimplifyNotExpression(NotExpression expression) 269 | { 270 | var inner = Simplify(expression.Expression); 271 | 272 | return inner switch 273 | { 274 | ComparisonExpression comparison => comparison with { Operator = Negate(comparison.Operator) }, 275 | NotExpression nested => Simplify(nested.Expression), 276 | AndExpression andExpression => Simplify(new OrExpression(new NotExpression(andExpression.Left), new NotExpression(andExpression.Right))), 277 | OrExpression orExpression => Simplify(new AndExpression(new NotExpression(orExpression.Left), new NotExpression(orExpression.Right))), 278 | _ when inner == expression.Expression => expression, 279 | _ => new NotExpression(inner), 280 | }; 281 | } 282 | 283 | private static ComparisonOperator Negate(ComparisonOperator op) 284 | { 285 | return op switch 286 | { 287 | ComparisonOperator.Equals => ComparisonOperator.NotEqual, 288 | ComparisonOperator.NotEqual => ComparisonOperator.Equals, 289 | ComparisonOperator.GreaterThan => ComparisonOperator.LessOrEqual, 290 | ComparisonOperator.LessThan => ComparisonOperator.GreaterOrEqual, 291 | ComparisonOperator.GreaterOrEqual => ComparisonOperator.LessThan, 292 | ComparisonOperator.LessOrEqual => ComparisonOperator.GreaterThan, 293 | _ => throw new InvalidOperationException($"Unsupported comparison operator '{op}'."), 294 | }; 295 | } 296 | 297 | private static Type BuildComparisonPredicate(ComparisonExpression comparison) 298 | { 299 | var rowType = typeof(TRow); 300 | var column = SchemaRegistry.ResolveColumn(comparison.ColumnIdentifier); 301 | var runtimeColumnType = column.GetRuntimeColumnType(rowType); 302 | var runtimeColumnValueType = column.GetRuntimeValueType(); 303 | var literalType = CreateLiteralType(runtimeColumnValueType, comparison.Literal); 304 | var filterDefinition = comparison.Operator switch 305 | { 306 | ComparisonOperator.Equals => typeof(EqualsFilter<,,,>), 307 | ComparisonOperator.GreaterThan => typeof(GreaterThanFilter<,,,>), 308 | ComparisonOperator.LessThan => typeof(LessThanFilter<,,,>), 309 | ComparisonOperator.GreaterOrEqual => typeof(GreaterOrEqualFilter<,,,>), 310 | ComparisonOperator.LessOrEqual => typeof(LessOrEqualFilter<,,,>), 311 | ComparisonOperator.NotEqual => typeof(NotEqualFilter<,,,>), 312 | _ => throw new InvalidOperationException($"Unsupported operator '{comparison.Operator}'."), 313 | }; 314 | 315 | return filterDefinition.MakeGenericType(rowType, runtimeColumnType, literalType, runtimeColumnValueType); 316 | } 317 | 318 | private static Type Optimize(Type pipeline) 319 | { 320 | if (pipeline is null) 321 | { 322 | throw new ArgumentNullException(nameof(pipeline)); 323 | } 324 | 325 | if (!pipeline.IsGenericType) 326 | { 327 | return pipeline; 328 | } 329 | 330 | var definition = pipeline.GetGenericTypeDefinition(); 331 | 332 | if (definition == typeof(Where<,,,,>)) 333 | { 334 | var args = pipeline.GetGenericArguments(); 335 | var rowType = args[Where.TRow]; 336 | var predicateType = args[Where.TPredicate]; 337 | var next = args[Where.TNext]; 338 | var resultType = args[Where.TResult]; 339 | var rootType = args[Where.TRoot]; 340 | 341 | if (next.IsGenericType && next.GetGenericTypeDefinition() == typeof(Select<,,,,,>)) 342 | { 343 | var selectArgs = next.GetGenericArguments(); 344 | if (selectArgs[Select.TRow] == rowType && selectArgs[Select.TResult] == resultType && selectArgs[Select.TRoot] == rootType) 345 | { 346 | var projectionType = selectArgs[Select.TProjection]; 347 | var selectNext = Optimize(selectArgs[Select.TNext]); 348 | var middleType = selectArgs[Select.TMiddle]; 349 | return typeof(WhereSelect<,,,,,,>).MakeGenericType(rowType, predicateType, projectionType, selectNext, middleType, resultType, rootType); 350 | } 351 | } 352 | 353 | var optimizedNext = Optimize(next); 354 | return typeof(Where<,,,,>).MakeGenericType(rowType, predicateType, optimizedNext, resultType, rootType); 355 | } 356 | 357 | if (definition == typeof(Select<,,,,,>)) 358 | { 359 | var args = pipeline.GetGenericArguments(); 360 | var rowType = args[Select.TRow]; 361 | var projectionType = args[Select.TProjection]; 362 | var next = args[Select.TNext]; 363 | var middleType = args[Select.TMiddle]; 364 | var resultType = args[Select.TResult]; 365 | var rootType = args[Select.TRoot]; 366 | 367 | if (next.IsGenericType && next.GetGenericTypeDefinition() == typeof(Where<,,,,>)) 368 | { 369 | var whereArgs = next.GetGenericArguments(); 370 | if (whereArgs[Where.TRow] == middleType && whereArgs[Where.TResult] == resultType && whereArgs[Where.TRoot] == rootType) 371 | { 372 | var predicateType = whereArgs[Where.TPredicate]; 373 | var whereNext = Optimize(whereArgs[Where.TNext]); 374 | return typeof(WhereSelect<,,,,,,>).MakeGenericType(rowType, predicateType, projectionType, whereNext, middleType, resultType, rootType); 375 | } 376 | } 377 | 378 | var optimizedNext = Optimize(next); 379 | return typeof(Select<,,,,,>).MakeGenericType(rowType, projectionType, optimizedNext, middleType, resultType, rootType); 380 | } 381 | 382 | if (definition == typeof(WhereSelect<,,,,,,>)) 383 | { 384 | var args = pipeline.GetGenericArguments(); 385 | var next = Optimize(args[WhereSelect.TNext]); 386 | return typeof(WhereSelect<,,,,,,>).MakeGenericType( 387 | args[WhereSelect.TRow], 388 | args[WhereSelect.TPredicate], 389 | args[WhereSelect.TProjection], 390 | next, 391 | args[WhereSelect.TMiddle], 392 | args[WhereSelect.TResult], 393 | args[WhereSelect.TRoot]); 394 | } 395 | 396 | return pipeline; 397 | } 398 | 399 | private static Type CreateLiteralType(Type columnType, LiteralValue literal) 400 | { 401 | if (columnType == typeof(int)) 402 | { 403 | if (literal.Kind != LiteralKind.Integer) 404 | { 405 | throw new InvalidOperationException("Expected an integer literal for this column."); 406 | } 407 | 408 | return LiteralTypeFactory.CreateIntLiteral(literal.IntValue); 409 | } 410 | 411 | if (columnType == typeof(ValueString)) 412 | { 413 | if (literal.Kind != LiteralKind.String) 414 | { 415 | throw new InvalidOperationException("Expected a string literal for this column."); 416 | } 417 | 418 | return LiteralTypeFactory.CreateStringLiteral(literal.StringValue); 419 | } 420 | 421 | if (columnType == typeof(float)) 422 | { 423 | if (literal.Kind == LiteralKind.Integer) 424 | { 425 | return LiteralTypeFactory.CreateFloatLiteral(literal.IntValue); 426 | } 427 | 428 | if (literal.Kind == LiteralKind.Float) 429 | { 430 | return LiteralTypeFactory.CreateFloatLiteral(literal.FloatValue); 431 | } 432 | 433 | throw new InvalidOperationException("Expected a numeric literal for this column."); 434 | } 435 | 436 | if (columnType == typeof(bool)) 437 | { 438 | if (literal.Kind != LiteralKind.Boolean) 439 | { 440 | throw new InvalidOperationException("Expected a boolean literal for this column."); 441 | } 442 | 443 | return LiteralTypeFactory.CreateBoolLiteral(literal.BoolValue); 444 | } 445 | 446 | throw new InvalidOperationException($"Column type '{columnType}' is not supported in WHERE clauses."); 447 | } 448 | } 449 | --------------------------------------------------------------------------------