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
55 changes: 55 additions & 0 deletions src/LinqTests/Operators/group_join_operator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -549,4 +549,59 @@ public async Task GroupJoin_with_First()
}

#endregion

#region QuerySession (QueryOnly storage) Tests

[Fact]
public async Task GroupJoin_left_join_on_id_with_query_session()
{
await SetupData();

// Use QuerySession (QueryOnly storage) where IdColumn.ShouldSelect returns false.
// This verifies that d.id is included in the CTE SELECT list even though
// QueryOnly storage normally excludes it.
await using var querySession = _store.QuerySession();

var results = await querySession.Query<JoinCustomer>()
.GroupJoin(
querySession.Query<JoinOrder>(),
c => c.Id,
o => o.CustomerId,
(c, orders) => new { c, orders })
.SelectMany(
x => x.orders.DefaultIfEmpty(),
(x, o) => new { CustomerName = x.c.Name, OrderStatus = (string?)o.Status })
.ToListAsync();

results.Count.ShouldBe(4); // Alice(2) + Bob(1) + Charlie(1 null)
results.Count(r => r.CustomerName == "Alice").ShouldBe(2);
results.Count(r => r.CustomerName == "Bob").ShouldBe(1);

var charlie = results.Single(r => r.CustomerName == "Charlie");
charlie.OrderStatus.ShouldBeNull();
}

[Fact]
public async Task GroupJoin_inner_join_on_id_with_query_session()
{
await SetupData();

await using var querySession = _store.QuerySession();

var results = await querySession.Query<JoinCustomer>()
.GroupJoin(
querySession.Query<JoinOrder>(),
c => c.Id,
o => o.CustomerId,
(c, orders) => new { c, orders })
.SelectMany(
x => x.orders,
(x, o) => new { x.c.Name, o.Amount })
.ToListAsync();

results.Count.ShouldBe(3);
results.Count(r => r.Name == "Alice").ShouldBe(2);
}

#endregion
}
10 changes: 9 additions & 1 deletion src/Marten/Internal/Storage/DocumentStorage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -460,7 +460,15 @@ private IEnumerable<ISqlFragment> defaultFilters()

internal class DuplicatedFieldSelectClause: ISelectClause, IModifyableFromObject
{
private readonly string[] _selectFields;
private string[] _selectFields;

internal void EnsureColumn(string columnLocator)
{
if (!_selectFields.Contains(columnLocator))
{
_selectFields = [.._selectFields, columnLocator];
}
}
private readonly IDocumentStorage _parent;

public DuplicatedFieldSelectClause(string fromObject, string selector, string[] selectFields, Type selectedType,
Expand Down
29 changes: 29 additions & 0 deletions src/Marten/Linq/CollectionUsage.Compilation.cs
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,11 @@ public Statement CompileGroupJoin(IMartenSession session,
var outerKeyMember = outerCollection.MemberFor(groupJoin.OuterKeySelector.Body);
var innerKeyMember = innerCollection.MemberFor(groupJoin.InnerKeySelector.Body);

// Ensure join key columns are present in CTE SELECT lists (they may be
// excluded when using QueryOnly storage, e.g. d.id is omitted by IdColumn)
EnsureJoinKeyInCte(outerStatement, outerStorage, outerKeyMember.TypedLocator);
EnsureJoinKeyInCte(innerStatement, innerStorage, innerKeyMember.TypedLocator);

// Replace d. prefix with CTE aliases for the ON clause
var outerKeyLocator = outerKeyMember.TypedLocator.Replace("d.", outerCteAlias + ".");
var innerKeyLocator = innerKeyMember.TypedLocator.Replace("d.", innerCteAlias + ".");
Expand Down Expand Up @@ -691,4 +696,28 @@ internal void ProcessSingleValueModeIfAny(SelectorStatement statement, IMartenSe
}
}
}

private static void EnsureJoinKeyInCte(SelectorStatement statement, IDocumentStorage storage, string keyLocator)
{
var selectClause = statement.SelectClause;
var currentFields = selectClause.SelectFields();

if (currentFields.Contains(keyLocator))
{
return;
}

if (selectClause is DuplicatedFieldSelectClause duplicatedClause)
{
duplicatedClause.EnsureColumn(keyLocator);
}
else
{
// When there are no duplicate fields, SelectClauseWithDuplicatedFields returns
// the storage itself. Wrap it in a DuplicatedFieldSelectClause to add the column.
var fields = currentFields.Append(keyLocator).ToArray();
statement.SelectClause = new DuplicatedFieldSelectClause(
selectClause.FromObject, string.Empty, fields, selectClause.SelectedType, storage);
}
}
}
Loading