Skip to content
Draft
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
4 changes: 4 additions & 0 deletions docs/release-notes/.FSharp.Core/11.0.200.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,9 @@

* Correct `FSharpValue.PreComputeTuplePropertyInfo` to report that struct tuples store elements in fields, not properties, and recommend supported tuple readers instead of reporting a valid index as out of range. ([Issue #20801](https://github.com/dotnet/fsharp/issues/20801), [PR #20815](https://github.com/dotnet/fsharp/pull/20815))
* Quotation debug text from `Expr.ToString()`, `Expr.ToString(full)`, and `%A` now prints struct tuple construction as `NewStructTuple`; reference tuples remain `NewTuple`. ([Issue #20800](https://github.com/dotnet/fsharp/issues/20800), [PR #20814](https://github.com/dotnet/fsharp/pull/20814))
* Fix struct tuples in query expressions: selecting one no longer fails with "unreachable", and the items of one that a later operator reads are taken from its fields instead of failing with `ArgumentException: Struct tuples store their elements in fields, not properties`. ([Issue #15522](https://github.com/dotnet/fsharp/issues/15522), [PR #20894](https://github.com/dotnet/fsharp/pull/20894))
* Fix struct tuples in query expressions and in lambdas converted to LINQ expression trees: selecting one no longer fails with "unreachable", and taking one apart no longer fails with a tuple index out of range. ([Issue #15522](https://github.com/dotnet/fsharp/issues/15522), [PR #20894](https://github.com/dotnet/fsharp/pull/20894))
* Fix LINQ providers not recognizing `leftOuterJoin` as a left join once more follows it: the group now reaches the rest of the query as the `GroupJoin` produced it, with `DefaultIfEmpty` applied where the query reads it, as in C#, instead of already defaulted inside the `GroupJoin` result, so two `leftOuterJoin`s in a row become two SQL `LEFT JOIN`s. ([Issue #6552](https://github.com/dotnet/fsharp/issues/6552), [PR #20903](https://github.com/dotnet/fsharp/pull/20903))
* Fix LINQ providers failing on, or evaluating on the client, queries with a `for`, `join`, `groupJoin` or `leftOuterJoin` after two `for`s: the query now continues one flat chain of `SelectMany`, `Join` and `GroupJoin`, as C# builds it, instead of nesting the rest of the query inside the first `SelectMany`, which EF Core could translate only as a lateral join, if at all. ([Issue #8459](https://github.com/dotnet/fsharp/issues/8459), [PR #20902](https://github.com/dotnet/fsharp/pull/20902))

### Changed
28 changes: 28 additions & 0 deletions docs/release-notes/PENDING_BREAKING_CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,34 @@ a.Equals(b) // Was: false, Now: true
// New: if (node.NodeType == ExpressionType.ArrayIndex)
```

### A `for` or join after two `for`s continues the chain

Over an `IQueryable`, a query that goes on after two `for`s no longer nests the rest inside the collection selector of the
first `SelectMany`; like C#, it pairs the elements of the first two sources and continues from the pairs.

```fsharp
query { for c in db.Customers do for o in c.Orders do for l in o.Lines do select (c.Name, l.Id) }
// Was: Customers.SelectMany(c => c.Orders.SelectMany(o => o.Lines, ...), (c, x) => x)
// Now: Customers.SelectMany(c => c.Orders, (c, o) => new AnonymousObject(c, o)).SelectMany(pair => pair.Item2.Lines, ...)
```

**Impact:** Code that walks the expression tree of such a query sees a longer chain and no nested `SelectMany`, `Join`
or `GroupJoin`. The rows are the same.

### `leftOuterJoin` applies `DefaultIfEmpty` where the group is read

```fsharp
query { for c in db.Customers do leftOuterJoin o in db.Orders on (c.Id = o.CustomerId) into orders; for o in orders do select (c, o) }
// Was: Customers.GroupJoin(Orders, ..., (c, orders) => new AnonymousObject(c, orders.DefaultIfEmpty())).SelectMany(t => t.Item2, ...)
// Now: Customers.GroupJoin(Orders, ..., (c, orders) => new AnonymousObject(c, orders)).SelectMany(t => t.Item2.DefaultIfEmpty(), ...)
```

**Impact:** The tree has the shape C# produces for a left join. Code that looked for `DefaultIfEmpty` in the `GroupJoin`
result selector finds it in the `SelectMany` collection selector, or wherever else the query reads the group. The
elements are the same, but the group is no longer one object per joined row: each read makes its own `DefaultIfEmpty`
sequence, so a query that compares the group itself by reference (`select g; distinct`, `groupBy g`,
`obj.ReferenceEquals(g, g)`) over LINQ to Objects gets different rows.

---

## IQueryable Type Preservation 🟡
Expand Down
9 changes: 8 additions & 1 deletion src/FSharp.Core/Linq.fs
Original file line number Diff line number Diff line change
Expand Up @@ -475,7 +475,14 @@ module LeafExpressionConverter =

| TupleGet(arg, n) ->
let argP = ConvExprToLinqInContext env arg
let rec build ty argP n =
let rec build (ty: Type) argP n =
// A struct tuple holds its elements in fields, which PreComputeTuplePropertyInfo cannot return
if ty.IsValueType then
if n < 7 then
Expression.Field(argP, ty.GetField("Item" + string (n + 1))) |> asExpr
else
build (ty.GetGenericArguments().[7]) (Expression.Field(argP, ty.GetField "Rest") |> asExpr) (n - 7)
else
match FSharpValue.PreComputeTuplePropertyInfo(ty, n) with
| propInfo, None ->
Expression.Property(argP, propInfo) |> asExpr
Expand Down
Loading
Loading