Skip to content

Commit 2856739

Browse files
DJGosnellclaude
andcommitted
Optimize: Make Join interceptors noops in PrebuiltDispatch chains
Add AsJoined<T>() methods to QueryBuilder<T>, JoinedQueryBuilder<T1,T2>, and JoinedQueryBuilder3<T1,T2,T3> that perform type-only conversion without creating JoinClause structs or cloning QueryState via ImmutableArray.Add. Update InterceptorCodeGenerator to emit AsJoined<T>() instead of AddJoinClause<T>() when the chain is PrebuiltDispatch. Closes #6 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent d4039df commit 2856739

5 files changed

Lines changed: 250 additions & 3 deletions

File tree

src/Quarry.Generator/Generation/InterceptorCodeGenerator.Joins.cs

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@ internal static partial class InterceptorCodeGenerator
1717
/// Handles both initial joins (QueryBuilder→JoinedQueryBuilder) and
1818
/// chained joins (JoinedQueryBuilder→JoinedQueryBuilder3, etc.)
1919
/// </summary>
20-
private static void GenerateJoinInterceptor(StringBuilder sb, UsageSiteInfo site, string methodName)
20+
private static void GenerateJoinInterceptor(StringBuilder sb, UsageSiteInfo site, string methodName,
21+
PrebuiltChainInfo? prebuiltChain = null, bool isFirstInChain = false)
2122
{
2223
var entityType = GetShortTypeName(site.EntityTypeName);
2324
var clauseInfo = site.ClauseInfo as JoinClauseInfo;
@@ -50,7 +51,20 @@ private static void GenerateJoinInterceptor(StringBuilder sb, UsageSiteInfo site
5051
var returnTypeArgs = string.Join(", ", allTypes);
5152
var funcTypeArgs = string.Join(", ", allTypes) + ", bool";
5253

53-
if (clauseInfo != null && clauseInfo.IsSuccess)
54+
if (prebuiltChain != null && clauseInfo != null && clauseInfo.IsSuccess)
55+
{
56+
// Prebuilt path: AsJoined<T>() — type conversion only, no state mutation
57+
sb.AppendLine($" public static {returnBuilderName}<{returnTypeArgs}> {methodName}(");
58+
sb.AppendLine($" this {receiverBuilderName}<{receiverTypeArgs}> builder,");
59+
sb.AppendLine($" Expression<Func<{funcTypeArgs}>> _)");
60+
sb.AppendLine($" {{");
61+
sb.AppendLine($" var __b = Unsafe.As<{concreteReceiverBuilderName}<{receiverTypeArgs}>>(builder);");
62+
if (isFirstInChain && prebuiltChain.MaxParameterCount > 0)
63+
sb.AppendLine($" __b.AllocatePrebuiltParams({prebuiltChain.MaxParameterCount});");
64+
sb.AppendLine($" return __b.AsJoined<{joinedType}>();");
65+
sb.AppendLine($" }}");
66+
}
67+
else if (clauseInfo != null && clauseInfo.IsSuccess)
5468
{
5569
var escapedSql = EscapeStringLiteral(clauseInfo.OnConditionSql);
5670
sb.AppendLine($" public static {returnBuilderName}<{returnTypeArgs}> {methodName}(");
@@ -81,6 +95,30 @@ private static void GenerateJoinInterceptor(StringBuilder sb, UsageSiteInfo site
8195
sb.AppendLine($" }}");
8296
}
8397
}
98+
else if (prebuiltChain != null && clauseInfo != null && clauseInfo.IsSuccess)
99+
{
100+
// Prebuilt path: AsJoined<T>() — type conversion only, no state mutation
101+
var joinedEntityName = clauseInfo.JoinedEntityName;
102+
103+
if (site.IsNavigationJoin)
104+
{
105+
sb.AppendLine($" public static IJoinedQueryBuilder<{entityType}, {joinedEntityName}> {methodName}(");
106+
sb.AppendLine($" this {thisType}<{entityType}> builder,");
107+
sb.AppendLine($" Expression<Func<{entityType}, NavigationList<{joinedEntityName}>>> _)");
108+
}
109+
else
110+
{
111+
sb.AppendLine($" public static IJoinedQueryBuilder<{entityType}, {joinedEntityName}> {methodName}(");
112+
sb.AppendLine($" this {thisType}<{entityType}> builder,");
113+
sb.AppendLine($" Expression<Func<{entityType}, {joinedEntityName}, bool>> _)");
114+
}
115+
sb.AppendLine($" {{");
116+
sb.AppendLine($" var __b = Unsafe.As<{concreteThisType}<{entityType}>>(builder);");
117+
if (isFirstInChain && prebuiltChain.MaxParameterCount > 0)
118+
sb.AppendLine($" __b.AllocatePrebuiltParams({prebuiltChain.MaxParameterCount});");
119+
sb.AppendLine($" return __b.AsJoined<{joinedEntityName}>();");
120+
sb.AppendLine($" }}");
121+
}
84122
else if (clauseInfo != null && clauseInfo.IsSuccess)
85123
{
86124
var joinedEntityName = clauseInfo.JoinedEntityName;

src/Quarry.Generator/Generation/InterceptorCodeGenerator.Query.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -187,7 +187,7 @@ or InterceptorKind.ExecuteFetchFirstOrDefault or InterceptorKind.ExecuteFetchSin
187187
case InterceptorKind.Join:
188188
case InterceptorKind.LeftJoin:
189189
case InterceptorKind.RightJoin:
190-
GenerateJoinInterceptor(sb, site, methodName);
190+
GenerateJoinInterceptor(sb, site, methodName, prebuiltClauseChain, isFirstClauseInChain);
191191
break;
192192

193193
case InterceptorKind.ExecuteFetchAll:

src/Quarry.Tests/JoinOperationsTests.cs

Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -780,6 +780,179 @@ public void JoinedQueryBuilder_AddJoinClause_ReturnsBuilder3WithState()
780780

781781
#endregion
782782

783+
#region AsJoined (Prebuilt Path) Tests
784+
785+
[Test]
786+
public void QueryBuilder_AsJoined_ReturnsJoinedBuilderWithSameState()
787+
{
788+
var dialect = SqlDialect.PostgreSQL;
789+
var builder = new QueryBuilder<TestUser>(dialect, "users", null);
790+
791+
var result = builder.AsJoined<TestOrder>();
792+
793+
Assert.That(result, Is.InstanceOf<JoinedQueryBuilder<TestUser, TestOrder>>());
794+
}
795+
796+
[Test]
797+
public void QueryBuilder_AsJoined_DoesNotMutateJoinClauses()
798+
{
799+
var dialect = SqlDialect.PostgreSQL;
800+
var builder = new QueryBuilder<TestUser>(dialect, "users", null);
801+
802+
var result = builder.AsJoined<TestOrder>();
803+
804+
// AsJoined should NOT add JoinClauses or set FromTableAlias
805+
var resultState = result.State;
806+
Assert.Multiple(() =>
807+
{
808+
Assert.That(resultState.JoinClauses.Length, Is.EqualTo(0));
809+
Assert.That(resultState.FromTableAlias, Is.Null);
810+
});
811+
}
812+
813+
[Test]
814+
public void QueryBuilder_AsJoined_PropagatesPrebuiltParams()
815+
{
816+
var dialect = SqlDialect.PostgreSQL;
817+
var builder = new QueryBuilder<TestUser>(dialect, "users", null);
818+
builder.AllocatePrebuiltParams(3);
819+
builder.BindParam(42);
820+
821+
var result = builder.AsJoined<TestOrder>();
822+
823+
Assert.Multiple(() =>
824+
{
825+
Assert.That(result.PrebuiltParams, Is.Not.Null);
826+
Assert.That(result.PrebuiltParams!.Length, Is.EqualTo(3));
827+
Assert.That(result.PrebuiltParams![0], Is.EqualTo(42));
828+
Assert.That(result.PrebuiltParamIndex, Is.EqualTo(1));
829+
});
830+
}
831+
832+
[Test]
833+
public void JoinedQueryBuilder_AsJoined_ReturnsBuilder3WithSameState()
834+
{
835+
var dialect = SqlDialect.PostgreSQL;
836+
var state = new Quarry.Internal.QueryState(dialect, "users", null);
837+
var builder = new JoinedQueryBuilder<TestUser, TestOrder>(state);
838+
839+
var result = builder.AsJoined<TestItem>();
840+
841+
Assert.That(result, Is.InstanceOf<JoinedQueryBuilder3<TestUser, TestOrder, TestItem>>());
842+
}
843+
844+
[Test]
845+
public void JoinedQueryBuilder_AsJoined_DoesNotMutateJoinClauses()
846+
{
847+
var dialect = SqlDialect.PostgreSQL;
848+
var state = new Quarry.Internal.QueryState(dialect, "users", null);
849+
var builder = new JoinedQueryBuilder<TestUser, TestOrder>(state);
850+
851+
var result = builder.AsJoined<TestItem>();
852+
853+
var resultState = result.State;
854+
Assert.Multiple(() =>
855+
{
856+
Assert.That(resultState.JoinClauses.Length, Is.EqualTo(0));
857+
Assert.That(resultState.FromTableAlias, Is.Null);
858+
});
859+
}
860+
861+
[Test]
862+
public void JoinedQueryBuilder_AsJoined_PropagatesPrebuiltParams()
863+
{
864+
var dialect = SqlDialect.PostgreSQL;
865+
var state = new Quarry.Internal.QueryState(dialect, "users", null);
866+
var builder = new JoinedQueryBuilder<TestUser, TestOrder>(state);
867+
builder.AllocatePrebuiltParams(2);
868+
builder.BindParam("hello");
869+
870+
var result = builder.AsJoined<TestItem>();
871+
872+
Assert.Multiple(() =>
873+
{
874+
Assert.That(result.PrebuiltParams, Is.Not.Null);
875+
Assert.That(result.PrebuiltParams!.Length, Is.EqualTo(2));
876+
Assert.That(result.PrebuiltParams![0], Is.EqualTo("hello"));
877+
Assert.That(result.PrebuiltParamIndex, Is.EqualTo(1));
878+
});
879+
}
880+
881+
[Test]
882+
public void JoinedQueryBuilder3_AsJoined_ReturnsBuilder4WithSameState()
883+
{
884+
var dialect = SqlDialect.PostgreSQL;
885+
var state = new Quarry.Internal.QueryState(dialect, "users", null);
886+
var builder = new JoinedQueryBuilder3<TestUser, TestOrder, TestItem>(state);
887+
888+
var result = builder.AsJoined<TestCategory>();
889+
890+
Assert.That(result, Is.InstanceOf<JoinedQueryBuilder4<TestUser, TestOrder, TestItem, TestCategory>>());
891+
}
892+
893+
[Test]
894+
public void JoinedQueryBuilder3_AsJoined_DoesNotMutateJoinClauses()
895+
{
896+
var dialect = SqlDialect.PostgreSQL;
897+
var state = new Quarry.Internal.QueryState(dialect, "users", null);
898+
var builder = new JoinedQueryBuilder3<TestUser, TestOrder, TestItem>(state);
899+
900+
var result = builder.AsJoined<TestCategory>();
901+
902+
var resultState = result.State;
903+
Assert.Multiple(() =>
904+
{
905+
Assert.That(resultState.JoinClauses.Length, Is.EqualTo(0));
906+
Assert.That(resultState.FromTableAlias, Is.Null);
907+
});
908+
}
909+
910+
[Test]
911+
public void JoinedQueryBuilder3_AsJoined_PropagatesPrebuiltParams()
912+
{
913+
var dialect = SqlDialect.PostgreSQL;
914+
var state = new Quarry.Internal.QueryState(dialect, "users", null);
915+
var builder = new JoinedQueryBuilder3<TestUser, TestOrder, TestItem>(state);
916+
builder.AllocatePrebuiltParams(5);
917+
builder.BindParam(100);
918+
builder.BindParam(200);
919+
920+
var result = builder.AsJoined<TestCategory>();
921+
922+
Assert.Multiple(() =>
923+
{
924+
Assert.That(result.PrebuiltParams, Is.Not.Null);
925+
Assert.That(result.PrebuiltParams!.Length, Is.EqualTo(5));
926+
Assert.That(result.PrebuiltParams![0], Is.EqualTo(100));
927+
Assert.That(result.PrebuiltParams![1], Is.EqualTo(200));
928+
Assert.That(result.PrebuiltParamIndex, Is.EqualTo(2));
929+
});
930+
}
931+
932+
[Test]
933+
public void AsJoined_VsAddJoinClause_StateIsDifferent()
934+
{
935+
// AsJoined should NOT add JoinClauses, while AddJoinClause should
936+
var dialect = SqlDialect.PostgreSQL;
937+
var builder = new QueryBuilder<TestUser>(dialect, "users", null);
938+
939+
var asJoinedResult = builder.AsJoined<TestOrder>();
940+
var addJoinResult = builder.AddJoinClause<TestOrder>(CoreJoinKind.Inner, "orders", "cond");
941+
942+
Assert.Multiple(() =>
943+
{
944+
// AsJoined: no join clauses, no alias
945+
Assert.That(asJoinedResult.State.JoinClauses.Length, Is.EqualTo(0));
946+
Assert.That(asJoinedResult.State.FromTableAlias, Is.Null);
947+
948+
// AddJoinClause: has join clause and alias
949+
Assert.That(addJoinResult.State.JoinClauses.Length, Is.EqualTo(1));
950+
Assert.That(addJoinResult.State.FromTableAlias, Is.EqualTo("t0"));
951+
});
952+
}
953+
954+
#endregion
955+
783956
#region Test Entity Classes
784957

785958
private class TestUser

src/Quarry/Query/JoinedQueryBuilder.cs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,18 @@ public JoinedQueryBuilder<T1, T2, TResult> AsProjected<TResult>()
147147
return projected;
148148
}
149149

150+
/// <summary>
151+
/// Creates a 3-table joined builder for the prebuilt path, transferring the PrebuiltParams array.
152+
/// Performs only a type conversion without modifying state (no JoinClause/alias mutation).
153+
/// </summary>
154+
public JoinedQueryBuilder3<T1, T2, T3> AsJoined<T3>() where T3 : class
155+
{
156+
var joined = new JoinedQueryBuilder3<T1, T2, T3>(_state);
157+
joined.PrebuiltParams = PrebuiltParams;
158+
joined.PrebuiltParamIndex = PrebuiltParamIndex;
159+
return joined;
160+
}
161+
150162
#endregion
151163

152164
#endregion
@@ -500,6 +512,18 @@ public JoinedQueryBuilder3<T1, T2, T3, TResult> AsProjected<TResult>()
500512
return projected;
501513
}
502514

515+
/// <summary>
516+
/// Creates a 4-table joined builder for the prebuilt path, transferring the PrebuiltParams array.
517+
/// Performs only a type conversion without modifying state (no JoinClause/alias mutation).
518+
/// </summary>
519+
public JoinedQueryBuilder4<T1, T2, T3, T4> AsJoined<T4>() where T4 : class
520+
{
521+
var joined = new JoinedQueryBuilder4<T1, T2, T3, T4>(_state);
522+
joined.PrebuiltParams = PrebuiltParams;
523+
joined.PrebuiltParamIndex = PrebuiltParamIndex;
524+
return joined;
525+
}
526+
503527
#endregion
504528

505529
#endregion

src/Quarry/Query/QueryBuilder.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -494,6 +494,18 @@ public QueryBuilder<T, TResult> AsProjected<TResult>()
494494
return projected;
495495
}
496496

497+
/// <summary>
498+
/// Creates a joined builder for the prebuilt path, transferring the PrebuiltParams array.
499+
/// Performs only a type conversion without modifying state (no JoinClause/alias mutation).
500+
/// </summary>
501+
public JoinedQueryBuilder<T, TJoined> AsJoined<TJoined>() where TJoined : class
502+
{
503+
var joined = new JoinedQueryBuilder<T, TJoined>(_state);
504+
joined.PrebuiltParams = PrebuiltParams;
505+
joined.PrebuiltParamIndex = PrebuiltParamIndex;
506+
return joined;
507+
}
508+
497509
#endregion
498510

499511
#region Generated Code Methods for Select

0 commit comments

Comments
 (0)