Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 40 additions & 2 deletions src/Quarry.Generator/Generation/InterceptorCodeGenerator.Joins.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ internal static partial class InterceptorCodeGenerator
/// Handles both initial joins (QueryBuilder→JoinedQueryBuilder) and
/// chained joins (JoinedQueryBuilder→JoinedQueryBuilder3, etc.)
/// </summary>
private static void GenerateJoinInterceptor(StringBuilder sb, UsageSiteInfo site, string methodName)
private static void GenerateJoinInterceptor(StringBuilder sb, UsageSiteInfo site, string methodName,
PrebuiltChainInfo? prebuiltChain = null, bool isFirstInChain = false)
{
var entityType = GetShortTypeName(site.EntityTypeName);
var clauseInfo = site.ClauseInfo as JoinClauseInfo;
Expand Down Expand Up @@ -50,7 +51,20 @@ private static void GenerateJoinInterceptor(StringBuilder sb, UsageSiteInfo site
var returnTypeArgs = string.Join(", ", allTypes);
var funcTypeArgs = string.Join(", ", allTypes) + ", bool";

if (clauseInfo != null && clauseInfo.IsSuccess)
if (prebuiltChain != null && clauseInfo != null && clauseInfo.IsSuccess)
{
// Prebuilt path: AsJoined<T>() — type conversion only, no state mutation
sb.AppendLine($" public static {returnBuilderName}<{returnTypeArgs}> {methodName}(");
sb.AppendLine($" this {receiverBuilderName}<{receiverTypeArgs}> builder,");
sb.AppendLine($" Expression<Func<{funcTypeArgs}>> _)");
sb.AppendLine($" {{");
sb.AppendLine($" var __b = Unsafe.As<{concreteReceiverBuilderName}<{receiverTypeArgs}>>(builder);");
if (isFirstInChain && prebuiltChain.MaxParameterCount > 0)
sb.AppendLine($" __b.AllocatePrebuiltParams({prebuiltChain.MaxParameterCount});");
sb.AppendLine($" return __b.AsJoined<{joinedType}>();");
sb.AppendLine($" }}");
}
else if (clauseInfo != null && clauseInfo.IsSuccess)
{
var escapedSql = EscapeStringLiteral(clauseInfo.OnConditionSql);
sb.AppendLine($" public static {returnBuilderName}<{returnTypeArgs}> {methodName}(");
Expand Down Expand Up @@ -81,6 +95,30 @@ private static void GenerateJoinInterceptor(StringBuilder sb, UsageSiteInfo site
sb.AppendLine($" }}");
}
}
else if (prebuiltChain != null && clauseInfo != null && clauseInfo.IsSuccess)
{
// Prebuilt path: AsJoined<T>() — type conversion only, no state mutation
var joinedEntityName = clauseInfo.JoinedEntityName;

if (site.IsNavigationJoin)
{
sb.AppendLine($" public static IJoinedQueryBuilder<{entityType}, {joinedEntityName}> {methodName}(");
sb.AppendLine($" this {thisType}<{entityType}> builder,");
sb.AppendLine($" Expression<Func<{entityType}, NavigationList<{joinedEntityName}>>> _)");
}
else
{
sb.AppendLine($" public static IJoinedQueryBuilder<{entityType}, {joinedEntityName}> {methodName}(");
sb.AppendLine($" this {thisType}<{entityType}> builder,");
sb.AppendLine($" Expression<Func<{entityType}, {joinedEntityName}, bool>> _)");
}
sb.AppendLine($" {{");
sb.AppendLine($" var __b = Unsafe.As<{concreteThisType}<{entityType}>>(builder);");
if (isFirstInChain && prebuiltChain.MaxParameterCount > 0)
sb.AppendLine($" __b.AllocatePrebuiltParams({prebuiltChain.MaxParameterCount});");
sb.AppendLine($" return __b.AsJoined<{joinedEntityName}>();");
sb.AppendLine($" }}");
}
else if (clauseInfo != null && clauseInfo.IsSuccess)
{
var joinedEntityName = clauseInfo.JoinedEntityName;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,7 @@ or InterceptorKind.ExecuteFetchFirstOrDefault or InterceptorKind.ExecuteFetchSin
case InterceptorKind.Join:
case InterceptorKind.LeftJoin:
case InterceptorKind.RightJoin:
GenerateJoinInterceptor(sb, site, methodName);
GenerateJoinInterceptor(sb, site, methodName, prebuiltClauseChain, isFirstClauseInChain);
break;

case InterceptorKind.ExecuteFetchAll:
Expand Down
173 changes: 173 additions & 0 deletions src/Quarry.Tests/JoinOperationsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -780,6 +780,179 @@ public void JoinedQueryBuilder_AddJoinClause_ReturnsBuilder3WithState()

#endregion

#region AsJoined (Prebuilt Path) Tests

[Test]
public void QueryBuilder_AsJoined_ReturnsJoinedBuilderWithSameState()
{
var dialect = SqlDialect.PostgreSQL;
var builder = new QueryBuilder<TestUser>(dialect, "users", null);

var result = builder.AsJoined<TestOrder>();

Assert.That(result, Is.InstanceOf<JoinedQueryBuilder<TestUser, TestOrder>>());
}

[Test]
public void QueryBuilder_AsJoined_DoesNotMutateJoinClauses()
{
var dialect = SqlDialect.PostgreSQL;
var builder = new QueryBuilder<TestUser>(dialect, "users", null);

var result = builder.AsJoined<TestOrder>();

// AsJoined should NOT add JoinClauses or set FromTableAlias
var resultState = result.State;
Assert.Multiple(() =>
{
Assert.That(resultState.JoinClauses.Length, Is.EqualTo(0));
Assert.That(resultState.FromTableAlias, Is.Null);
});
}

[Test]
public void QueryBuilder_AsJoined_PropagatesPrebuiltParams()
{
var dialect = SqlDialect.PostgreSQL;
var builder = new QueryBuilder<TestUser>(dialect, "users", null);
builder.AllocatePrebuiltParams(3);
builder.BindParam(42);

var result = builder.AsJoined<TestOrder>();

Assert.Multiple(() =>
{
Assert.That(result.PrebuiltParams, Is.Not.Null);
Assert.That(result.PrebuiltParams!.Length, Is.EqualTo(3));
Assert.That(result.PrebuiltParams![0], Is.EqualTo(42));
Assert.That(result.PrebuiltParamIndex, Is.EqualTo(1));
});
}

[Test]
public void JoinedQueryBuilder_AsJoined_ReturnsBuilder3WithSameState()
{
var dialect = SqlDialect.PostgreSQL;
var state = new Quarry.Internal.QueryState(dialect, "users", null);
var builder = new JoinedQueryBuilder<TestUser, TestOrder>(state);

var result = builder.AsJoined<TestItem>();

Assert.That(result, Is.InstanceOf<JoinedQueryBuilder3<TestUser, TestOrder, TestItem>>());
}

[Test]
public void JoinedQueryBuilder_AsJoined_DoesNotMutateJoinClauses()
{
var dialect = SqlDialect.PostgreSQL;
var state = new Quarry.Internal.QueryState(dialect, "users", null);
var builder = new JoinedQueryBuilder<TestUser, TestOrder>(state);

var result = builder.AsJoined<TestItem>();

var resultState = result.State;
Assert.Multiple(() =>
{
Assert.That(resultState.JoinClauses.Length, Is.EqualTo(0));
Assert.That(resultState.FromTableAlias, Is.Null);
});
}

[Test]
public void JoinedQueryBuilder_AsJoined_PropagatesPrebuiltParams()
{
var dialect = SqlDialect.PostgreSQL;
var state = new Quarry.Internal.QueryState(dialect, "users", null);
var builder = new JoinedQueryBuilder<TestUser, TestOrder>(state);
builder.AllocatePrebuiltParams(2);
builder.BindParam("hello");

var result = builder.AsJoined<TestItem>();

Assert.Multiple(() =>
{
Assert.That(result.PrebuiltParams, Is.Not.Null);
Assert.That(result.PrebuiltParams!.Length, Is.EqualTo(2));
Assert.That(result.PrebuiltParams![0], Is.EqualTo("hello"));
Assert.That(result.PrebuiltParamIndex, Is.EqualTo(1));
});
}

[Test]
public void JoinedQueryBuilder3_AsJoined_ReturnsBuilder4WithSameState()
{
var dialect = SqlDialect.PostgreSQL;
var state = new Quarry.Internal.QueryState(dialect, "users", null);
var builder = new JoinedQueryBuilder3<TestUser, TestOrder, TestItem>(state);

var result = builder.AsJoined<TestCategory>();

Assert.That(result, Is.InstanceOf<JoinedQueryBuilder4<TestUser, TestOrder, TestItem, TestCategory>>());
}

[Test]
public void JoinedQueryBuilder3_AsJoined_DoesNotMutateJoinClauses()
{
var dialect = SqlDialect.PostgreSQL;
var state = new Quarry.Internal.QueryState(dialect, "users", null);
var builder = new JoinedQueryBuilder3<TestUser, TestOrder, TestItem>(state);

var result = builder.AsJoined<TestCategory>();

var resultState = result.State;
Assert.Multiple(() =>
{
Assert.That(resultState.JoinClauses.Length, Is.EqualTo(0));
Assert.That(resultState.FromTableAlias, Is.Null);
});
}

[Test]
public void JoinedQueryBuilder3_AsJoined_PropagatesPrebuiltParams()
{
var dialect = SqlDialect.PostgreSQL;
var state = new Quarry.Internal.QueryState(dialect, "users", null);
var builder = new JoinedQueryBuilder3<TestUser, TestOrder, TestItem>(state);
builder.AllocatePrebuiltParams(5);
builder.BindParam(100);
builder.BindParam(200);

var result = builder.AsJoined<TestCategory>();

Assert.Multiple(() =>
{
Assert.That(result.PrebuiltParams, Is.Not.Null);
Assert.That(result.PrebuiltParams!.Length, Is.EqualTo(5));
Assert.That(result.PrebuiltParams![0], Is.EqualTo(100));
Assert.That(result.PrebuiltParams![1], Is.EqualTo(200));
Assert.That(result.PrebuiltParamIndex, Is.EqualTo(2));
});
}

[Test]
public void AsJoined_VsAddJoinClause_StateIsDifferent()
{
// AsJoined should NOT add JoinClauses, while AddJoinClause should
var dialect = SqlDialect.PostgreSQL;
var builder = new QueryBuilder<TestUser>(dialect, "users", null);

var asJoinedResult = builder.AsJoined<TestOrder>();
var addJoinResult = builder.AddJoinClause<TestOrder>(CoreJoinKind.Inner, "orders", "cond");

Assert.Multiple(() =>
{
// AsJoined: no join clauses, no alias
Assert.That(asJoinedResult.State.JoinClauses.Length, Is.EqualTo(0));
Assert.That(asJoinedResult.State.FromTableAlias, Is.Null);

// AddJoinClause: has join clause and alias
Assert.That(addJoinResult.State.JoinClauses.Length, Is.EqualTo(1));
Assert.That(addJoinResult.State.FromTableAlias, Is.EqualTo("t0"));
});
}

#endregion

#region Test Entity Classes

private class TestUser
Expand Down
24 changes: 24 additions & 0 deletions src/Quarry/Query/JoinedQueryBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,18 @@ public JoinedQueryBuilder<T1, T2, TResult> AsProjected<TResult>()
return projected;
}

/// <summary>
/// Creates a 3-table joined builder for the prebuilt path, transferring the PrebuiltParams array.
/// Performs only a type conversion without modifying state (no JoinClause/alias mutation).
/// </summary>
public JoinedQueryBuilder3<T1, T2, T3> AsJoined<T3>() where T3 : class
{
var joined = new JoinedQueryBuilder3<T1, T2, T3>(_state);
joined.PrebuiltParams = PrebuiltParams;
joined.PrebuiltParamIndex = PrebuiltParamIndex;
return joined;
}

#endregion

#endregion
Expand Down Expand Up @@ -500,6 +512,18 @@ public JoinedQueryBuilder3<T1, T2, T3, TResult> AsProjected<TResult>()
return projected;
}

/// <summary>
/// Creates a 4-table joined builder for the prebuilt path, transferring the PrebuiltParams array.
/// Performs only a type conversion without modifying state (no JoinClause/alias mutation).
/// </summary>
public JoinedQueryBuilder4<T1, T2, T3, T4> AsJoined<T4>() where T4 : class
{
var joined = new JoinedQueryBuilder4<T1, T2, T3, T4>(_state);
joined.PrebuiltParams = PrebuiltParams;
joined.PrebuiltParamIndex = PrebuiltParamIndex;
return joined;
}

#endregion

#endregion
Expand Down
12 changes: 12 additions & 0 deletions src/Quarry/Query/QueryBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -494,6 +494,18 @@ public QueryBuilder<T, TResult> AsProjected<TResult>()
return projected;
}

/// <summary>
/// Creates a joined builder for the prebuilt path, transferring the PrebuiltParams array.
/// Performs only a type conversion without modifying state (no JoinClause/alias mutation).
/// </summary>
public JoinedQueryBuilder<T, TJoined> AsJoined<TJoined>() where TJoined : class
{
var joined = new JoinedQueryBuilder<T, TJoined>(_state);
joined.PrebuiltParams = PrebuiltParams;
joined.PrebuiltParamIndex = PrebuiltParamIndex;
return joined;
}

#endregion

#region Generated Code Methods for Select
Expand Down
Loading