diff --git a/src/EFCore.Sqlite.Core/Query/Internal/SqliteApplyFlatteningExpressionVisitor.cs b/src/EFCore.Sqlite.Core/Query/Internal/SqliteApplyFlatteningExpressionVisitor.cs
new file mode 100644
index 00000000000..2da5d589a23
--- /dev/null
+++ b/src/EFCore.Sqlite.Core/Query/Internal/SqliteApplyFlatteningExpressionVisitor.cs
@@ -0,0 +1,119 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Microsoft.EntityFrameworkCore.Query.SqlExpressions;
+
+namespace Microsoft.EntityFrameworkCore.Sqlite.Query.Internal;
+
+///
+/// SQLite doesn't support APPLY, but its table-valued functions (e.g. json_each) may reference columns of preceding tables in the
+/// FROM clause, which is what APPLY is generally needed for. This visitor rewrites CROSS/OUTER APPLY over a trivial subquery whose
+/// only table is such a function into a regular CROSS/LEFT JOIN over that function, inlining the subquery's projection into the
+/// containing SELECT.
+///
+///
+/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
+/// the same compatibility standards as public APIs. It may be changed or removed without notice in
+/// any release. You should only use it directly in your code with extreme caution and knowing that
+/// doing so can result in application failures when updating to a new Entity Framework Core release.
+///
+public class SqliteApplyFlatteningExpressionVisitor(ISqlExpressionFactory sqlExpressionFactory) : ExpressionVisitor
+{
+ // OUTER APPLY keeps rows whose function yields nothing, so it becomes a LEFT JOIN that always matches.
+ private readonly SqlExpression _alwaysTrue =
+ sqlExpressionFactory.ApplyDefaultTypeMapping(sqlExpressionFactory.Constant(true))!;
+
+ ///
+ /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
+ /// the same compatibility standards as public APIs. It may be changed or removed without notice in
+ /// any release. You should only use it directly in your code with extreme caution and knowing that
+ /// doing so can result in application failures when updating to a new Entity Framework Core release.
+ ///
+ protected override Expression VisitExtension(Expression node)
+ {
+ // ShapedQueryExpression doesn't support VisitChildren, its parts have to be visited explicitly.
+ if (node is ShapedQueryExpression shapedQuery)
+ {
+ return shapedQuery
+ .UpdateQueryExpression(Visit(shapedQuery.QueryExpression))
+ .UpdateShaperExpression(Visit(shapedQuery.ShaperExpression));
+ }
+
+ var visited = base.VisitExtension(node);
+
+ return visited is SelectExpression select ? TryFlattenApplies(select) : visited;
+ }
+
+ private Expression TryFlattenApplies(SelectExpression select)
+ {
+ List? newTables = null;
+ Dictionary>? inlinedProjections = null;
+
+ for (var i = 0; i < select.Tables.Count; i++)
+ {
+ if (select.Tables[i] is not JoinExpressionBase { Table: SelectExpression applied } join
+ || join is not (CrossApplyExpression or OuterApplyExpression)
+ || !IsFlattenable(applied))
+ {
+ continue;
+ }
+
+ // The subquery is a trivial wrapper over a table-valued function; SQLite can reference the outer tables from the function's
+ // arguments directly, so the wrapper can be removed and the join rewritten.
+ var projectionMap = applied.Projection.ToDictionary(p => p.Alias, p => p.Expression);
+
+ newTables ??= [.. select.Tables];
+ newTables[i] = join is CrossApplyExpression
+ ? new CrossJoinExpression(applied.Tables[0])
+ : new LeftJoinExpression(applied.Tables[0], _alwaysTrue);
+
+ inlinedProjections ??= [];
+ inlinedProjections[applied.Alias!] = projectionMap;
+ }
+
+ if (newTables is null)
+ {
+ return select;
+ }
+
+ var inliner = new ProjectionInliningVisitor(inlinedProjections!);
+
+ return select.Update(
+ newTables,
+ (SqlExpression?)inliner.Visit(select.Predicate),
+ select.GroupBy.Select(g => (SqlExpression)inliner.Visit(g)).ToList(),
+ (SqlExpression?)inliner.Visit(select.Having),
+ select.Projection.Select(p => (ProjectionExpression)inliner.Visit(p)).ToList(),
+ select.Orderings.Select(o => (OrderingExpression)inliner.Visit(o)).ToList(),
+ (SqlExpression?)inliner.Visit(select.Offset),
+ (SqlExpression?)inliner.Visit(select.Limit));
+ }
+
+ ///
+ /// A subquery can be unwrapped only if it does nothing beyond projecting out of a single table-valued function; anything else
+ /// (filtering, ordering, paging, grouping) would change the meaning of the query once lifted into the containing SELECT.
+ ///
+ private static bool IsFlattenable(SelectExpression select)
+ => select is
+ {
+ Tables: [JsonEachExpression],
+ Predicate: null,
+ GroupBy: [],
+ Having: null,
+ Orderings: [],
+ Limit: null,
+ Offset: null,
+ IsDistinct: false
+ };
+
+ private sealed class ProjectionInliningVisitor(
+ IReadOnlyDictionary> inlinedProjections) : ExpressionVisitor
+ {
+ protected override Expression VisitExtension(Expression node)
+ => node is ColumnExpression column
+ && inlinedProjections.TryGetValue(column.TableAlias, out var projections)
+ && projections.TryGetValue(column.Name, out var inlined)
+ ? inlined
+ : base.VisitExtension(node);
+ }
+}
diff --git a/src/EFCore.Sqlite.Core/Query/Internal/SqliteQueryTranslationPostprocessor.cs b/src/EFCore.Sqlite.Core/Query/Internal/SqliteQueryTranslationPostprocessor.cs
index da19cd424eb..85dd20b68c5 100644
--- a/src/EFCore.Sqlite.Core/Query/Internal/SqliteQueryTranslationPostprocessor.cs
+++ b/src/EFCore.Sqlite.Core/Query/Internal/SqliteQueryTranslationPostprocessor.cs
@@ -39,6 +39,11 @@ public SqliteQueryTranslationPostprocessor(
public override Expression Process(Expression query)
{
var result = base.Process(query);
+
+ // SQLite has no APPLY, but its table-valued functions can reference preceding tables in FROM; rewrite what we can as joins
+ // before rejecting the rest.
+ result = new SqliteApplyFlatteningExpressionVisitor(RelationalDependencies.SqlExpressionFactory).Visit(result);
+
_applyValidator.Visit(result);
return result;
diff --git a/test/EFCore.Sqlite.FunctionalTests/Query/Associations/ComplexJson/ComplexJsonProjectionSqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/Query/Associations/ComplexJson/ComplexJsonProjectionSqliteTest.cs
index 2bc50888559..3832d9320cb 100644
--- a/test/EFCore.Sqlite.FunctionalTests/Query/Associations/ComplexJson/ComplexJsonProjectionSqliteTest.cs
+++ b/test/EFCore.Sqlite.FunctionalTests/Query/Associations/ComplexJson/ComplexJsonProjectionSqliteTest.cs
@@ -8,15 +8,6 @@ namespace Microsoft.EntityFrameworkCore.Query.Associations.ComplexJson;
public class ComplexJsonProjectionSqliteTest(ComplexJsonSqliteFixture fixture, ITestOutputHelper testOutputHelper)
: ComplexJsonProjectionRelationalTestBase(fixture, testOutputHelper)
{
- public override Task SelectMany_associate_collection(QueryTrackingBehavior queryTrackingBehavior)
- => AssertApplyNotSupported(() => base.SelectMany_associate_collection(queryTrackingBehavior));
-
- public override Task SelectMany_nested_collection_on_required_associate(QueryTrackingBehavior queryTrackingBehavior)
- => AssertApplyNotSupported(() => base.SelectMany_nested_collection_on_required_associate(queryTrackingBehavior));
-
- public override Task SelectMany_nested_collection_on_optional_associate(QueryTrackingBehavior queryTrackingBehavior)
- => AssertApplyNotSupported(() => base.SelectMany_nested_collection_on_optional_associate(queryTrackingBehavior));
-
public override Task Select_subquery_FirstOrDefault_complex_collection(QueryTrackingBehavior queryTrackingBehavior)
=> AssertApplyNotSupported(() => base.Select_subquery_FirstOrDefault_complex_collection(queryTrackingBehavior));
diff --git a/test/EFCore.Sqlite.FunctionalTests/Query/Associations/OwnedJson/OwnedJsonProjectionSqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/Query/Associations/OwnedJson/OwnedJsonProjectionSqliteTest.cs
index 0a534560a9a..82ccdb90e03 100644
--- a/test/EFCore.Sqlite.FunctionalTests/Query/Associations/OwnedJson/OwnedJsonProjectionSqliteTest.cs
+++ b/test/EFCore.Sqlite.FunctionalTests/Query/Associations/OwnedJson/OwnedJsonProjectionSqliteTest.cs
@@ -8,21 +8,6 @@ namespace Microsoft.EntityFrameworkCore.Query.Associations.OwnedJson;
public class OwnedJsonProjectionSqliteTest(OwnedJsonSqliteFixture fixture, ITestOutputHelper testOutputHelper)
: OwnedJsonProjectionRelationalTestBase(fixture, testOutputHelper)
{
- public override Task SelectMany_associate_collection(QueryTrackingBehavior queryTrackingBehavior)
- => queryTrackingBehavior is QueryTrackingBehavior.TrackAll
- ? Task.CompletedTask // Base test expects "can't track owned entities" exception, but with SQLite we get "no CROSS APPLY"
- : AssertApplyNotSupported(() => base.SelectMany_associate_collection(queryTrackingBehavior));
-
- public override Task SelectMany_nested_collection_on_required_associate(QueryTrackingBehavior queryTrackingBehavior)
- => queryTrackingBehavior is QueryTrackingBehavior.TrackAll
- ? Task.CompletedTask // Base test expects "can't track owned entities" exception, but with SQLite we get "no CROSS APPLY"
- : AssertApplyNotSupported(() => base.SelectMany_nested_collection_on_required_associate(queryTrackingBehavior));
-
- public override Task SelectMany_nested_collection_on_optional_associate(QueryTrackingBehavior queryTrackingBehavior)
- => queryTrackingBehavior is QueryTrackingBehavior.TrackAll
- ? Task.CompletedTask // Base test expects "can't track owned entities" exception, but with SQLite we get "no CROSS APPLY"
- : AssertApplyNotSupported(() => base.SelectMany_nested_collection_on_optional_associate(queryTrackingBehavior));
-
public override Task Select_subquery_FirstOrDefault_complex_collection(QueryTrackingBehavior queryTrackingBehavior)
=> queryTrackingBehavior is QueryTrackingBehavior.TrackAll
? Task.CompletedTask // Base test expects "can't track owned entities" exception, but with SQLite we get "no CROSS APPLY"
diff --git a/test/EFCore.Sqlite.FunctionalTests/Query/JsonQuerySqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/Query/JsonQuerySqliteTest.cs
index d639fbc2334..345b2d56f7f 100644
--- a/test/EFCore.Sqlite.FunctionalTests/Query/JsonQuerySqliteTest.cs
+++ b/test/EFCore.Sqlite.FunctionalTests/Query/JsonQuerySqliteTest.cs
@@ -263,11 +263,17 @@ public override Task Json_predicate_on_byte_array(bool async)
=> Assert.ThrowsAsync(() => base.Json_predicate_on_byte_array(async));
public override async Task Json_collection_in_projection_with_anonymous_projection_of_scalars(bool async)
- => Assert.Equal(
- SqliteStrings.ApplyNotSupported,
- (await Assert.ThrowsAsync(()
- => base.Json_collection_in_projection_with_anonymous_projection_of_scalars(async)))
- .Message);
+ {
+ await base.Json_collection_in_projection_with_anonymous_projection_of_scalars(async);
+
+ AssertSql(
+ """
+SELECT "j"."Id", "o"."value" ->> 'Name' AS "Name", "o"."value" ->> 'Number' AS "Number", "o"."key"
+FROM "JsonEntitiesBasic" AS "j"
+LEFT JOIN json_each("j"."OwnedCollectionRoot", '$') AS "o" ON 1
+ORDER BY "j"."Id", "o"."key"
+""");
+ }
public override async Task Json_collection_in_projection_with_composition_where_and_anonymous_projection_of_scalars(bool async)
=> Assert.Equal(